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

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

معمولاً در یک میکروکنترلر، مثل همین STM32F030K6T6 که داریم بررسی می‌کنیم، تعداد زیادی رجیستر پریفرال وجود داره و ما می‌خوایم امکان دسترسی به اون‌ها رو در زبان C مهیا کنیم. مثلاً با مراجعه به داکیومنت Reference Manual (RM)، در بخش 8.4، یه نگاهی به رجیسترهای GPIO بندازید. بیشتر پورت‌ها، یازده تا رجیستر دارن. چطوری این ۱۱ تا رجیستر رو تعریف کنیم؟

تعریف رجیسترها به‌صورت ماکروهای مجزا

فرض کنید که می‌خوایم رجیسترهای پورت A رو تعریف کنیم. قاعدتاً اولین راهی که در ادامه‌ی مقاله‌ی قبلی به ذهن می‌رسه، تعریف کردن تک‌تک رجیسترهای پورت A در قالب یک‌سری ماکرو هست. کد زیر رو ببینید:

#define GPIOA_MODER 	(*((volatile uint32_t *)(0x48000000UL)))
#define GPIOA_OTYPER 	(*((volatile uint32_t *)(0x48000004UL)))
#define GPIOA_OSPEEDR 	(*((volatile uint32_t *)(0x48000008UL)))
#define GPIOA_PUPDR 	(*((volatile uint32_t *)(0x4800000CUL)))
#define GPIOA_IDR 		(*((volatile uint32_t *)(0x48000010UL)))
#define GPIOA_ODR 		(*((volatile uint32_t *)(0x48000014UL)))
#define GPIOA_BSRR 		(*((volatile uint32_t *)(0x48000018UL)))
#define GPIOA_LCKR 		(*((volatile uint32_t *)(0x4800001CUL)))
#define GPIOA_AFRL 		(*((volatile uint32_t *)(0x48000020UL)))
#define GPIOA_AFRH 		(*((volatile uint32_t *)(0x48000024UL)))
#define GPIOA_BRR 		(*((volatile uint32_t *)(0x48000028UL)))

داخل کد بالا، ما اومده‌ایم تمامی رجیسترهای پورت A رو به‌صورت چند تا ماکرو تعریف کرده‌ایم. هر ماکرو، یک رجیستر رو تعریف می‌کنه و آدرس هر رجیستر رو داخل تعریفش آورده‌ایم.

خب این راه، خوبیش اینه که به‌شدت ساده است! واقعاً مگه ساده‌تر از این هم می‌شه؟! از دیدن این تعریف‌ها، به‌راحتی متوجه می‌شیم که با رجیسترهای پورت A سروکار داریم. همین‌طور، اگر بخوایم که فقط چند تا از رجیسترهای یک پریفرال رو تعریف کنیم، خب راحت می‌تونیم فقط ماکروهای مدنظر خودمون رو بیاریم و از نوشتن تعریف رجیسترهایی که برامون کاربردی نیست، خودداری کنیم. ممکنه که بخوایم یه معماری جدید رو بررسی کنیم و ممکنه که به فایل‌های هِدِر اون معماری دسترسی نداشته باشیم یا استفاده ازشون برامون دردسر داشته باشه، یا بخوایم که سریع یه کد رو تست کنیم؛ توی این حالت‌ها، استفاده از ماکروها به این شکلی که بیان شد، ممکنه خیلی راه خوبی باشه.

اما قطعاً یه ضعف‌هایی هم داخل این راه‌حل، دیده می‌شه. مثلاً ما اومده‌ایم آدرس‌ها رو دستی محاسبه کرده‌ایم و اون‌ها رو داخل تعریف‌ها نوشته‌ایم. این کار، امکان خطا رو به‌شدت افزایش می‌ده. حداقل، می‌شه آدرس‌ها رو دستی محاسبه نکرد و کار جمع آفست و آدرس پایه رو به Preprocessor و کامپایلر سپرد. (توجه داشته باشید که Preprocessor و کامپایلر، کار خودشون رو قبل از اجرای برنامه انجام می‌دن و دیگه قرار نیست که این عمل‌های جمع، توسط میکروکنترلر انجام بشه، پس سرباری برای میکروکنترلر به‌وجود نمیاد.) تکرار آدرس پایه داخل هر تعریف هم که باز، کار اشتباهیه؛ پس می‌شه که اون رو هم به‌شکل یه ماکرو دربیاریم. داخل کد زیر، اول آدرس پایه رو به اسم GPIOA_BASE تعریف کرده‌ایم و بعدش، کار پردازش ماکروها و جمع آدرس پایه و آفست رو به Preprocessor و کامپایلر سپرده‌ایم:

#define GPIOA_BASE 		(0x48000000UL)

#define GPIOA_MODER 	(*((volatile uint32_t *)(GPIOA_BASE + 0x00UL)))
#define GPIOA_OTYPER 	(*((volatile uint32_t *)(GPIOA_BASE + 0x04UL)))
#define GPIOA_OSPEEDR 	(*((volatile uint32_t *)(GPIOA_BASE + 0x08UL)))
#define GPIOA_PUPDR 	(*((volatile uint32_t *)(GPIOA_BASE + 0x0CUL)))
#define GPIOA_IDR 		(*((volatile uint32_t *)(GPIOA_BASE + 0x10UL)))
#define GPIOA_ODR 		(*((volatile uint32_t *)(GPIOA_BASE + 0x14UL)))
#define GPIOA_BSRR 		(*((volatile uint32_t *)(GPIOA_BASE + 0x18UL)))
#define GPIOA_LCKR 		(*((volatile uint32_t *)(GPIOA_BASE + 0x1CUL)))
#define GPIOA_AFRL 		(*((volatile uint32_t *)(GPIOA_BASE + 0x20UL)))
#define GPIOA_AFRH 		(*((volatile uint32_t *)(GPIOA_BASE + 0x24UL)))
#define GPIOA_BRR 		(*((volatile uint32_t *)(GPIOA_BASE + 0x28UL)))

می‌شه حتی اون تبدیل نوع و عملگر یگانی * رو هم به‌شکل یه ماکرو نوشت. مثلاً اگر ماکروی زیر رو درنظر بگیریم:

#define IO_ADDRESS(address)	(*((volatile uint32_t *)(address)))

اون‌وقت می‌شه که تعریف‌ها رو ساده‌تر هم کرد:

#define IO_ADDRESS(address)	(*((volatile uint32_t *)(address)))

#define GPIOA_BASE 		(0x48000000UL)

#define GPIOA_MODER 	(IO_ADDRESS(GPIOA_BASE + 0x00UL))
#define GPIOA_OTYPER 	(IO_ADDRESS(GPIOA_BASE + 0x04UL))
#define GPIOA_OSPEEDR	(IO_ADDRESS(GPIOA_BASE + 0x08UL))
#define GPIOA_PUPDR 	(IO_ADDRESS(GPIOA_BASE + 0x0CUL))
#define GPIOA_IDR 		(IO_ADDRESS(GPIOA_BASE + 0x10UL))
#define GPIOA_ODR 		(IO_ADDRESS(GPIOA_BASE + 0x14UL))
#define GPIOA_BSRR 		(IO_ADDRESS(GPIOA_BASE + 0x18UL))
#define GPIOA_LCKR 		(IO_ADDRESS(GPIOA_BASE + 0x1CUL))
#define GPIOA_AFRL 		(IO_ADDRESS(GPIOA_BASE + 0x20UL))
#define GPIOA_AFRH 		(IO_ADDRESS(GPIOA_BASE + 0x24UL))
#define GPIOA_BRR 		(IO_ADDRESS(GPIOA_BASE + 0x28UL))

البته همه‌ی این تعریف‌ها، کاملاً معادل هم هستن و ما صرفاً داریم با تعریف ماکروهای مختلف، میزان تکرار رو کم می‌کنیم یا تعریف‌ها رو ساده‌تر می‌کنیم و کارها رو به Preprocessor می‌سپریم.

همه‌ی این‌ها، خوب و متین! اما حالا فرض کنید که بخوایم رجیسترهای پورت B رو هم تعریف کنیم:

#define IO_ADDRESS(address)	(*((volatile uint32_t *)(address)))

#define GPIOA_BASE 		(0x48000000UL)

#define GPIOA_MODER 	(IO_ADDRESS(GPIOA_BASE + 0x00UL))
#define GPIOA_OTYPER 	(IO_ADDRESS(GPIOA_BASE + 0x04UL))
#define GPIOA_OSPEEDR 	(IO_ADDRESS(GPIOA_BASE + 0x08UL))
#define GPIOA_PUPDR 	(IO_ADDRESS(GPIOA_BASE + 0x0CUL))
#define GPIOA_IDR 		(IO_ADDRESS(GPIOA_BASE + 0x10UL))
#define GPIOA_ODR 		(IO_ADDRESS(GPIOA_BASE + 0x14UL))
#define GPIOA_BSRR 		(IO_ADDRESS(GPIOA_BASE + 0x18UL))
#define GPIOA_LCKR 		(IO_ADDRESS(GPIOA_BASE + 0x1CUL))
#define GPIOA_AFRL 		(IO_ADDRESS(GPIOA_BASE + 0x20UL))
#define GPIOA_AFRH 		(IO_ADDRESS(GPIOA_BASE + 0x24UL))
#define GPIOA_BRR 		(IO_ADDRESS(GPIOA_BASE + 0x28UL))

#define GPIOB_BASE 		(0x48000400UL)

#define GPIOB_MODER 	(IO_ADDRESS(GPIOB_BASE + 0x00UL))
#define GPIOB_OTYPER 	(IO_ADDRESS(GPIOB_BASE + 0x04UL))
#define GPIOB_OSPEEDR 	(IO_ADDRESS(GPIOB_BASE + 0x08UL))
#define GPIOB_PUPDR 	(IO_ADDRESS(GPIOB_BASE + 0x0CUL))
#define GPIOB_IDR 		(IO_ADDRESS(GPIOB_BASE + 0x10UL))
#define GPIOB_ODR 		(IO_ADDRESS(GPIOB_BASE + 0x14UL))
#define GPIOB_BSRR 		(IO_ADDRESS(GPIOB_BASE + 0x18UL))
#define GPIOB_LCKR 		(IO_ADDRESS(GPIOB_BASE + 0x1CUL))
#define GPIOB_AFRL 		(IO_ADDRESS(GPIOB_BASE + 0x20UL))
#define GPIOB_AFRH 		(IO_ADDRESS(GPIOB_BASE + 0x24UL))
#define GPIOB_BRR 		(IO_ADDRESS(GPIOB_BASE + 0x28UL))

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

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

#define IO_ADDRESS(address)			(*((volatile uint32_t *)(address)))

#define GPIOx_MODER(GPIOx_BASE)		(IO_ADDRESS(GPIOx_BASE + 0x00UL))
#define GPIOx_OTYPER(GPIOx_BASE)	(IO_ADDRESS(GPIOx_BASE + 0x04UL))
#define GPIOx_OSPEEDR(GPIOx_BASE)	(IO_ADDRESS(GPIOx_BASE + 0x08UL))
#define GPIOx_PUPDR(GPIOx_BASE) 	(IO_ADDRESS(GPIOx_BASE + 0x0CUL))
#define GPIOx_IDR(GPIOx_BASE)		(IO_ADDRESS(GPIOx_BASE + 0x10UL))
#define GPIOx_ODR(GPIOx_BASE)		(IO_ADDRESS(GPIOx_BASE + 0x14UL))
#define GPIOx_BSRR(GPIOx_BASE)		(IO_ADDRESS(GPIOx_BASE + 0x18UL))
#define GPIOx_LCKR(GPIOx_BASE)		(IO_ADDRESS(GPIOx_BASE + 0x1CUL))
#define GPIOx_AFRL(GPIOx_BASE)		(IO_ADDRESS(GPIOx_BASE + 0x20UL))
#define GPIOx_AFRH(GPIOx_BASE)		(IO_ADDRESS(GPIOx_BASE + 0x24UL))
#define GPIOx_BRR(GPIOx_BASE)		(IO_ADDRESS(GPIOx_BASE + 0x28UL))

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

#define IO_ADDRESS(address)			(*((volatile uint32_t *)(address)))

#define GPIOx_MODER(GPIOx_BASE) 	(IO_ADDRESS(GPIOx_BASE + 0x00UL))
#define GPIOx_OTYPER(GPIOx_BASE) 	(IO_ADDRESS(GPIOx_BASE + 0x04UL))
#define GPIOx_OSPEEDR(GPIOx_BASE) 	(IO_ADDRESS(GPIOx_BASE + 0x08UL))
#define GPIOx_PUPDR(GPIOx_BASE) 	(IO_ADDRESS(GPIOx_BASE + 0x0CUL))
#define GPIOx_IDR(GPIOx_BASE) 		(IO_ADDRESS(GPIOx_BASE + 0x10UL))
#define GPIOx_ODR(GPIOx_BASE) 		(IO_ADDRESS(GPIOx_BASE + 0x14UL))
#define GPIOx_BSRR(GPIOx_BASE) 		(IO_ADDRESS(GPIOx_BASE + 0x18UL))
#define GPIOx_LCKR(GPIOx_BASE) 		(IO_ADDRESS(GPIOx_BASE + 0x1CUL))
#define GPIOx_AFRL(GPIOx_BASE) 		(IO_ADDRESS(GPIOx_BASE + 0x20UL))
#define GPIOx_AFRH(GPIOx_BASE) 		(IO_ADDRESS(GPIOx_BASE + 0x24UL))
#define GPIOx_BRR(GPIOx_BASE) 		(IO_ADDRESS(GPIOx_BASE + 0x28UL))

#define GPIOA_BASE 		(0x48000000UL)

#define GPIOA_MODER 	(GPIOx_MODER(GPIOA_BASE))
#define GPIOA_OTYPER 	(GPIOx_OTYPER(GPIOA_BASE))
#define GPIOA_OSPEEDR 	(GPIOx_OSPEEDR(GPIOA_BASE))
#define GPIOA_PUPDR 	(GPIOx_PUPDR(GPIOA_BASE))
#define GPIOA_IDR 		(GPIOx_IDR(GPIOA_BASE))
#define GPIOA_ODR 		(GPIOx_ODR(GPIOA_BASE))
#define GPIOA_BSRR 		(GPIOx_BSRR(GPIOA_BASE))
#define GPIOA_LCKR 		(GPIOx_LCKR(GPIOA_BASE))
#define GPIOA_AFRL 		(GPIOx_AFRL(GPIOA_BASE))
#define GPIOA_AFRH 		(GPIOx_AFRH(GPIOA_BASE))
#define GPIOA_BRR 		(GPIOx_BRR(GPIOA_BASE))

#define GPIOB_BASE 		(0x48000400UL)

#define GPIOB_MODER 	(GPIOx_MODER(GPIOB_BASE))
#define GPIOB_OTYPER 	(GPIOx_OTYPER(GPIOB_BASE))
#define GPIOB_OSPEEDR 	(GPIOx_OSPEEDR(GPIOB_BASE))
#define GPIOB_PUPDR 	(GPIOx_PUPDR(GPIOB_BASE))
#define GPIOB_IDR 		(GPIOx_IDR(GPIOB_BASE))
#define GPIOB_ODR 		(GPIOx_ODR(GPIOB_BASE))
#define GPIOB_BSRR 		(GPIOx_BSRR(GPIOB_BASE))
#define GPIOB_LCKR 		(GPIOx_LCKR(GPIOB_BASE))
#define GPIOB_AFRL 		(GPIOx_AFRL(GPIOB_BASE))
#define GPIOB_AFRH 		(GPIOx_AFRH(GPIOB_BASE))
#define GPIOB_BRR 		(GPIOx_BRR(GPIOB_BASE))

جمع‌بندی

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

اما بدون درنظر گرفتن سلیقه، اون چیزی که داخل کدهای بالا واضحه، طولانی بودن کدهاست و این که با وجود معرفی ماکروهای جدید، باز هم تکرار خیلی‌خیلی زیادی وجود داره. ما تلاش کرده‌ایم تا با نام‌گذاری همه‌ی رجیسترهای پورت A با پیشوند GPIOA_، نشون بدیم که این رجیسترها یه ربطی به هم دارن و همگی مال یه پریفرال هستن؛ اما واقعیت اینه که این ماکروها، کاملاً مجزا هستن و هیچ ارتباطی به هم ندارن! به همین خاطر، لازمه که برای هر پورت، ما دوباره یه دور اسم هر رجیستر رو بنویسم و بهش بگیم که آدرس پایه‌اش چیه و از چه تعریف عمومی‌ای باید استفاده کنه (تعریف‌هایی که پیشوند GPIOx_ دارن). تازه، با وجود این که آفست‌ها رو فقط یه بار وارد کرده‌ایم، اما آخرش هم اون‌ها رو دستی نوشته‌ایم!

اگر این مسائلی که مطرح شد، از نظرمون ایراد داشته باشه، باید دنبال یه راه‌حلی بگردیم که در اون:

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

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