دسترسی به رجیسترهای پریفرال در زبان C - بخش ۴: struct

در بخش قبلی این مقاله، دیدیم که می‌تونیم رجیسترهای پریفرال میکروکنترلر STM32F030K6T6 رو به‌صورت ماکروهای کاملاً مجزا تعریف کنیم؛ اما تعریف تک‌تک رجیسترها به‌صورت ماکرو، تنها راه نیست. یکی دیگه از روش‌هایی که می‌شه پیش گرفت، استفاده از struct هست. در این مقاله، روش استفاده از struct رو برای دسترسی به رجیسترهای پریفرال سیستم، بررسی می‌کنیم.

در بخش ۳، دیدیم که استفاده از ماکروها برای تعریف رجیسترهای پریفرال، ممکنه که کد تعریف رو خیلی طولانی کنه و تکرار خیلی زیادی رو هم وارد کد بکنه. رجیسترهای یه پریفرال، به‌صورت اجزای جدا از همی تعریف می‌شن که حقیقتاً ربطی به هم ندارن و واقعاً عضو یک مجموعه نیستن. به‌علاوه، تعریف مجزای رجیسترها، هنوز هم ما رو از دست آفست‌ها و وارد کردن دستی اون‌ها، خلاص نمی‌کنه. مثلاً اگر با GPIO سروکار داشته باشیم، باید برای تعریف پورت‌ها، هر بار اسم رجیسترهای پورت رو دوباره بنویسیم. آفست‌ها رو هم که دستی وارد کردیم. اگر بتونیم که فقط‌وفقط یک بار، «ساختار» یه GPIO رو تعریف کنیم و رجیسترها و ترتیبشون رو تعیین کنیم، خیلی خوب می‌شه.

تعریف ساختار رجیسترهای یک پریفرال با استفاده از struct

وقتی صحبت از «ساختار» می‌شه، خودبه‌خود یادمون به struct می‌افته دیگه. با استفاده از struct، به‌خوبی می‌تونیم که ساختار رجیسترهای یه پریفرال رو تعریف کنیم. مثلاً می‌تونیم که ساختار رجیسترهای GPIO رو به‌شکل زیر بنویسیم:

struct GPIO
{
	volatile uint32_t MODER;
	volatile uint32_t OTYPER;
	volatile uint32_t OSPEEDR;
	volatile uint32_t PUPDR;
	volatile uint32_t IDR;
	volatile uint32_t ODR;
	volatile uint32_t BSRR;
	volatile uint32_t LCKR;
	volatile uint32_t AFRL;
	volatile uint32_t AFRH;
	volatile uint32_t BRR;
};

داخل struct بالا ما تعیین کرده‌ایم که یه GPIO، یازده تا رجیستر ۴-بایتی داره و اسم رجیسترها و ترتیبشون رو هم نوشته‌ایم. از این تعریف مشخصه که اولین رجیستر پریفرال ما، MODER هست، دقیقاً در آدرس شروع این struct قرار داره و اندازه‌اش ۴ بایته. بعد از اون، رجیستر OTYPER وجود داره که بدون فاصله‌ی خالی با رجیستر قبلی، در جای دوم قرار داره، آدرسش ۴ بایت بعد از آدرس شروع struct هست (چون قبلش فقط یه دونه عضو ۴-بایتی وجود داره) و اندازه‌اش هم که ۴ بایته. همین‌طور الی آخر، ما تمام رجیسترها رو تعریف کرده‌ایم. چون که اندازه‌ی هر عضو struct، چهار بایت هست و ترتیب اعضا هم درسته، پس ما عملاً آفست رجیسترها رو مشخص کرده‌ایم. الآن کافیه که آدرس پایه‌ی یه پورت رو به اشاره‌گری به ساختار struct GPIO تبدیل کنیم: کامپایلر می‌تونه که آدرس تک‌تک رجیسترها رو با توجه به تعریف این struct، محاسبه کنه؛ چون هم آدرس پایه رو داره و هم آفست رجیسترها رو.

دیگه اینجا دوباره اون روالی رو که داخل بخش‌های قبلی این مقاله طی کردیم (تعریف متغیر اشاره‌گر، دسترسی مستقیم، و نهایتاً تعریف ماکرو)، تکرار نمی‌کنیم و یه‌راست، ماکرو تعریف می‌کنیم. البته روش‌های دیگه‌ای هم وجود داره، ولی ما به تعریف ماکرو اکتفا می‌کنیم. برای تعریف ماکرو، حداقل دو تا راه وجود داره.

راه اول، اینه که ماکرو رو به‌صورت یه نوع اشاره‌گر به struct GPIO تعریف کنیم. مثلاً در کد زیر، ما یه ماکرو برای GPIOA تعریف می‌کنیم و بعدش، به یکی از رجیسترهای پورت A دسترسی پیدا می‌کنیم:

#define GPIOA ((struct GPIO *)(0x48000000UL))

GPIOA->MODER = 1;

در ماکروی بالا، ما اومده‌ایم مقدار آدرس پایه‌ی پریفرال رو به یه آدرس به نوع داده‌ی struct GPIO تبدیل کرده‌ایم. حالا می‌تونیم به رجیسترهای این پریفرال دسترسی پیدا کنیم. توجه می‌کنیم که چون ماکروی GPIOA در کد بالا توسط Preprocessor به یک «آدرس» به نوع داده‌ی struct GPIO تبدیل می‌شه، پس لازمه که برای دسترسی به اعضای struct از عملگر -> استفاده کنیم؛ درست مثل زمانی که از متغیرهای اشاره‌گر به یه struct استفاده می‌کنیم.

راه دوم، اینه که مقدار آدرس پایه‌ی پریفرال رو به آدرسی به نوع داده‌ی struct GPIO تبدیل کنیم (مثل راه اول) و بعدش همون‌جا داخل تعریف ماکرو، اون آدرس رو با استفاده از عملگر یگانی *، dereference هم بکنیم! توی این حالت، دیگه نیازی به استفاده از عملگر -> نیست و می‌شه که از عملگر . استفاده کرد؛ درست مثل زمانی که از یه متغیر معمولی از نوع یه struct استفاده می‌کنیم. کد زیر کاملاً معادل کد قبلی هست، اما چون ماکروی خودمون رو با یه عملگر * دیگه تعریف کرده‌ایم، برای دسترسی به رجیسترها باید از عملگر . استفاده کنیم:

#define GPIOA (*((struct GPIO *)(0x48000000UL)))

GPIOA.MODER = 1;

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

#define GPIOA ((struct GPIO *)(0x48000000UL))
#define GPIOB ((struct GPIO *)(0x48000400UL))

پس بدون توجه به این که کدوم یک از این دو راه رو انتخاب کنیم، به هدفمون از تعریف struct رسیده‌ایم:

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

درنظر گرفتن آدرس‌های بلااستفاده در پریفرال

داخل داکیومنت Reference Manual (RM) به رجیسترهای واحد CRC نگاه کنید (بخش 5.4.5 با عنوان CRC register map). سه تا رجیستر اول، در آدرس‌های پشت سر هم قرار دارن و هیچ آدرس بلااستفاده‌ای بینشون نیست؛ اما به آفست رجیستر آخر (INIT) نگاه کنید. بین رجیستر قبلی (CR) با آفست ۸ (0x08) و این رجیستر با آفست ۱۶ (0x10)، به‌میزان ۸ بایت فاصله وجود داره. ۴ بایت از این فاصله که با خود رجیستر CR پر شده، پس به اندازه‌ی یه رجیستر ۴-بایتی آدرس بلااستفاده داریم. این مسأله، حتماً باید در تعریف struct نشون داده بشه، تا محاسبه‌ی آفست با اشکال مواجه نشه.

اگر struct رو به‌اشتباه، به‌شکل زیر تعریف کنیم:

// THIS DEFINITION OF THE CRC PERIPHERAL IS INCORRECT

struct CRC_INCORRECT_DEFINITION
{
	volatile uint32_t DR;
	volatile uint32_t IDR;
	volatile uint32_t CR;
	volatile uint32_t INIT;
};

خب درواقع، بیان کرده‌ایم که رجیستر INIT، درست بعد از رجیستر CR قرار داره و هیچ آدرس بلااستفاده‌ای بین این دو رجیستر وجود نداره. این درست نیست. الآن، آفست رجیستر INIT برابر با ۱۲ محاسبه می‌شه (چون طبق این تعریف، فقط ۳ تا رجیستر ۴-بایتی قبلش وجود دارن)؛ می‌دونیم که آفست واقعی این رجیستر، ۱۶ هست و مقداری که با تعریف بالا به‌دست میاد، اشتباهه. پس باید تعریف واحد CRC رو طوری تغییر بدیم که آفست رجیستر آخر رو به‌درستی منعکس کنه. برای این کار، کافیه که به‌نوعی نشون بدیم که رجیستر INIT بلافاصله بعد از رجیستر CR نیست، بلکه به اندازه‌ی ۴ بایت فضای بلااستفاده بین این دو وجود داره. می‌تونیم یک یا چند تا عضو بیخودی با اندازه‌ی مناسب، بین CR و INIT قرار بدیم تا بیانگر اون آدرس‌های بلااستفاده باشن. ما ۴ بایت فضای بلااستفاده داریم که می‌تونیم با یه عضو ۴-بایتی نشونش بدیم:

struct CRC
{
	volatile uint32_t DR;
	volatile uint32_t IDR;
	volatile uint32_t CR;
			 uint32_t _RESERVED_;
	volatile uint32_t INIT;
};

داخل تعریف بالا، از یه عضو بیخودی به نام _RESERVED_ استفاده کرده‌ایم تا وجود ۴ بایت آدرس بلااستفاده رو بین CR و INIT نشون بدیم. در این تعریف، دیگه INIT بلافاصله بعد از CR قرار نداره، بلکه به اندازه‌ی ۴ بایت فضای بلااستفاده (_RESERVED_) بین این دو رجیستر وجود داره و رجیستری به اون آدرس اختصاص داده نشده. در این تعریف، فاصله‌ی INIT تا شروع struct، به‌اندازه‌ی چهار تا عضو ۴-بایتی هست و آفست ۱۶ رو برای رجیستر INIT نتیجه می‌ده که مقدار صحیح و مدنظر ماست.

تنها کاربرد این عضو _RESERVED_، تنظیم آفست رجیستر INIT هست و هیچ‌وقت قرار نیست که داخل کد ما، دسترسی بهش صورت بگیره، پس نیازی هم نداره که به‌شکل volatile تعریف بشه. نکته‌ی بعدی این که اسم این عضو، کوچکترین اهمیتی برای کامپایلر نداره و هر اسم مُجازی رو می‌تونیم براش انتخاب کنیم؛ بهتره اسمی رو براش بذاریم که با دیدنش متوجه بشیم که این عضو از struct، یه رجیستر نیست و یه فضای بلااستفاده است. به‌عنوان نکته‌ی آخر هم توجه داشته باشیم که نوع استفاده‌ی ما از struct CRC طوری هست که عضو _RESERVED_، هیچ فضایی رو در حافظه هَدَر نمی‌ده؛ چون ما هیچ‌وقت متغیری از نوع struct CRC داخل RAM ایجاد نمی‌کنیم و اعضای این ساختار، هیچ‌وقت فضایی داخل RAM به خودشون اختصاص نمی‌دن، بلکه تنها کاربرد این ساختار برای ما، تعریف اسم و محاسبه‌ی آفست رجیسترهای پریفراله و وجود _RESERVED_ هم فقط‌وفقط برای تصحیح آفست عضو INIT هست.

نکته‌ها

اول- هرچند که ما کلمه‌ی کلیدی volatile رو روی هر عضو از struct قرار داده‌ایم، اما این امکان هم وجود داره که اصلاً از volatile داخل تعریف اعضای struct استفاده نکنیم و درعوض، موقع نوشتن ماکروی پریفرال، اشاره‌گر رو به یه volatile struct GPIO یا volatile struct CRC (یا حالا volatile هر پریفرال دیگه‌ای) تعریف کنیم؛ مثلاً کد زیر رو ببینید:

struct CRC
{
	uint32_t DR;
	uint32_t IDR;
	uint32_t CR;
	uint32_t _RESERVED_;
	uint32_t INIT;
};

#define CRC ((volatile struct CRC *)(0x40023000))

می‌بینید که داخل این کد، ما اعضای struct CRC رو volatile نذاشته‌ایم و درعوض، موقع تعریف ماکروی CRC، اون رو در قالب اشاره‌گری به volatile struct CRC معرفی کرده‌ایم. در این حالت، کامپایلر با تمام اعضایی که از طریق اون ماکروی اشاره‌گر CRC بهشون دسترسی پیدا کنیم، به‌شکل مقادیر volatile برخورد می‌کنه؛ پس نتیجه‌ی نهایی، فرقی نداره و آخرش رجیسترهای ما volatile هستن. این مسأله، بیشتر یه تفاوت داخل style و سبک کد نویسی هست: ممکنه یه نفر بخواد که volatile بودن اعضا (رجیسترها) رو به شکل صریح، در زمان تعریف هر عضو struct تعیین کنه و در مقابل، یه نفر دیگه، اختصار روش دوم رو ترجیح بده و تکرار کلمه‌ی کلیدی volatile رو دوست نداشته باشه. انتخاب با شماست.

دوم- همون‌طوری که در مثال‌های این مقاله هم مشخصه، در زبان C باید هم موقع تعریف یه struct و هم در زمان استفاده ازش (مثلاً تعریف یه اشاره‌گر به اون نوع) از کلمه‌ی کلیدی struct استفاده بشه. این قضیه باعث شده که بعضی از افراد به فکر ساده‌سازی استفاده از structها در زبان C باشن و بخوان که این تکرار کلمه‌ی کلیدی رو حذف کنن. یکی از روش‌هایی که خیلی به کار برده می‌شه، ترکیب struct و typedef هست که برای توضیح کاملش، یه مقاله‌ی جدا بهش اختصاص داده شده. حتی اگر خودتون مشکلی با تکرار کلمه‌ی کلیدی struct ندارید، باز هم اون‌قدر این روش رو می‌بینید که توصیه می‌شه باهاش آشنا بشید.

جمع‌بندی

در این مقاله، استفاده از ماکروها محدودتر شد و درعوض، ساختار پریفرال‌های خودمون رو به کمک struct تعریف کردیم. با این کار، دیگه نیازی نیست که آفست رجیسترها رو دستی وارد کنیم. به‌علاوه، موقع ایجاد پریفرال‌های هم‌نوع، لازم نداریم که اسم رجیسترها رو تکرار کنیم.

در این سری از مقاله‌ها، تونستیم که با استفاده از ماکروها و همین‌طور به‌کمک struct، به رجیسترهای پریفرال سیستم خودمون دسترسی پیدا کنیم. اگر نگاهی به تعریف رجیسترهای پریفرال در CMSIS بندازیم یا حتی فایل‌های هِدِر سیستم‌های نسبتاً ساده‌تری رو مثل AVR بررسی کنیم، کاربرد خیلی از روش‌های معرفی‌شده رو می‌بینیم. هرچند که توسعه‌دهنده‌های Embedded Software و Firmware، معمولاً از همین فایل‌های هِدِر آماده استفاده می‌کنن، اما زمانی که خودمون نقش توسعه‌دهنده‌ی فایل‌های هدر و libraryها رو داشته باشیم یا بخوایم به پریفرال‌های یه سیستم جدید یا ناآشنا دسترسی پیدا کنیم، لازمه که به فنون مختلف برای تعریف رجیسترهای پریفرال، مجهز باشیم؛ در این صورت، إن‌شاءالله خودمون می‌تونیم تولیدکننده‌ی زیرساخت‌های نرم‌افزاری باشیم. به‌علاوه، چنین دانشی به ما کمک می‌کنه تا سورس‌کدهای دیگران رو هم بهتر درک کنیم و به یاری خدا، توسعه‌دهنده‌ی قوی‌تری بشیم.