نحوه‌ی محاسبه‌ی آدرس رجیسترهای پریفرال در STM32

معمولاً آدرس رجیسترهای پریفرال یه سیستم، به‌صورت آماده در دسترس ما هست و ما، به‌عنوان توسعه‌دهنده‌های Embedded Software یا Firmware، بدون اینکه بدونیم از اون آدرس‌ها استفاده می‌کنیم؛ اما اگر نقش توسعه‌دهنده‌ی فایل‌های هِدِر (header) یا libraryها رو به عهده گرفتیم یا با یه سیستم ناآشنا مواجه شدیم، دیگه ممکنه به اون آدرس‌های آماده و شسته‌رُفته دسترسی نداشته باشیم. مفاهیم مربوط به محاسبه‌ی آدرس‌ها، ممکنه برای افرادی که به‌تازگی وارد برنامه‌نویسی سطح پایین شده‌اند، مبهم باشه. در این مقاله، نحوه‌ی محاسبه‌ی آدرس رجیسترهای پریفرال و مفاهیم مرتبط با اون رو بررسی می‌کنیم.

اگر بخوایم به رجیسترهای پریفرال یه میکروکنترلر دسترسی سطح پایین داشته باشیم، باید آدرس اون رجیسترها رو داخل فضای آدرس میکروکنترلر بدونیم. در معماری‌های به‌نسبت ساده‌تری مثل AVR، معمولاً آدرس کل رجیسترهای پریفرال، داخل یه‌دونه جدول ذکر می‌شه و تمام! اصلاً پدیده‌ای تحت عنوان «محاسبه‌ی» آدرس، داخل اون معماری‌ها وجود نداره. اما داخل STM32 و ARM، برای به‌دست‌آوردن آدرس یه رجیستر، باید تلاش کرد! البته آدرس رجیسترهای مربوط به Core Peripheralها، داخل داکیومنت Programming Manual اومده، ولی آدرس رجیسترهای مربوط به پریفرال‌های خود STM32 رو باید محاسبه کرد.

البته پیازداغش رو زیاد نکنیم: کلمه‌ی «محاسبه» در اینجا، نهایتاً به انجام یه جمع ساده ختم می‌شه و کار سختی نیست، ولی نحوه‌ی پیدا کردن خود اون عددایی که باید با هم جمع بشن، ممکنه که برای افرادی که به‌تازگی وارد این زمینه شده‌اند، کمی مبهم باشه. وجود داکیومنت‌های متعدد و رنگارنگ توی اکوسیستم STM32 و ARM، و پخش‌وپَلا بودن اطلاعات هم قطعاً به رفع این ابهام، کمکی نمی‌کنه! تلاش ما اینه که إن‌شاءالله این ابهامات رو بتونیم کمتر کنیم.

فرض کنیم که می‌خوایم آدرس رجیسترهای GPIOA رو حساب کنیم. (GPIOA رو به‌طور خاص انتخاب کرده‌ایم، چون یک‌سری فرض‌هایی که در طول بحث می‌کنیم، در موردش صادق هست. در انتهای بحث، حالات خاص دیگه‌ای رو هم بررسی می‌کنیم.) اگر نگاهی به داکیومنت Reference Manual (RM) بندازیم، دوتا جدول وجود داره که به ما کمک می‌کنه:

  • داخل بخش 8.4.12 با عنوان GPIO register map، لیست کامل رجیسترهای مربوط به GPIOها رو در جدول ۲۳ می‌بینیم، ولی خبری از آدرس رجیسترها نیست. عجب…
  • داخل بخش 2.2 با عنوان Memory map and register boundary addresses، داخل جدول ۲ اطلاعات جالبی هست. داخل این جدول، یه‌سری آدرس دیده می‌شه و ما رو امیدوار می‌کنه.

ما دنبال محاسبه‌ی «آدرس» هستیم، پس قاعدتاً ستون Boundary address در جدول ۲، توجه ما رو جلب می‌کنه. اگر داخل این ستون رو نگاه کنیم، می‌بینیم که توی هر سطر، دوتا عدد نوشته شده که با خط فاصله از هم جدا شده‌اند. توجه کنید که عنوان این ستون، Boundary address هست، پس متوجه می‌شیم که این عددها، یک بازه و محدوده‌ای از آدرس‌ها رو مشخص می‌کنن؛ مثلاً اگر در ستون Peripheral، به سطر GPIOA نگاه کنیم، می‌بینیم که مقدار Boundary address رو زده: 0x48000000 - 0x480003FF. عدد سمت چپ که عدد کوچیکتر باشه، برابر با 0x48000000 و در حقیقت، آدرس شروع این بازه هست. عدد سمت راست هم که عدد بزرگتر باشه، برابر با 0x480003FF و در واقع، همون آدرس پایان این محدوده است. اینجا یه نکته رو به‌خاطر بسپرید:

به آدرس شروع یک بازه از آدرس‌ها، می‌گیم آدرسِ پایه (Base Address).

حالا قضیه‌ی این بازه از آدرس‌ها چیه؟ همون‌طوری که می‌بینید، داخل جدول ۲ برای هر Peripheral، یه بازه از آدرس‌ها تعیین شده. در واقع، هر پریفرال، بازه‌ی آدرسی مخصوص به خودش رو داره و رجیسترهای اون پریفرال، در اون بازه از آدرس‌ها قرار دارن. اتفاقاً اولین رجیستر پریفرال هم با احتمال خیلی زیادی در همون آدرس شروع بازه یا آدرس پایه قرار داره (بعداً درباره‌ی حالت‌های خاص صحبت می‌کنیم). با این اوصاف، اگر محدوده‌ی آدرسی GPIOA در جدول ۲ برابر با 0x48000000 - 0x480003FF ذکر شده، پس اولین رجیستر GPIOA، در همون آدرس پایه قرار داره؛ یعنی 0x48000000 آدرس اولین رجیستر هست. به عنوان یه مثال دیگه، از جدول ۲ می‌تونیم ببینیم که آدرس پایه برای واحد CRC (و آدرس اولین رجیستر این واحد) برابر با 0x40023000 هست.

خب. حالا برگردیم به جدول ۲۳ و ببینیم که رجیسترهای مربوط به GPIOA چیا هستن. به ستون Register نگاه کنید. در این بحث، برای سادگی، معمولاً اسم رجیسترها رو بدون اون پیشوند GPIOx_ (یا GPIOA_) ذکر می‌کنیم. اولین رجیستر، MODER نام داره. البته می‌بینید که سطر دوم هم، دوباره رجیستر MODER هست! معنیش این نیست که در پورت‌های ما، دوتا رجیستر MODER وجود داره. خیر. دلیل این تکرار، اینه که مقدار اولیه‌ی رجیستر MODER برای پورت A با مقدار اولیه‌ی این رجیستر برای سایر پورت‌ها فرق داره. همون‌طوری که می‌بینید در سطر اول، اسم کامل رجیستر رو به‌صورت GPIOA_MODER زده و مشخص کرده که مقدار اولیه‌ی تعیین‌شده زیرش (همون مقدار Reset value) مخصوص پورت A هست. در سطر دوم، اسم رجیستر رو به‌شکل GPIOx_MODER نوشته و گفته که مقدار x، می‌تونه هر کدوم از حروف B تا F باشه (مثلاً GPIOB_MODER) و مقدار اولیه‌ی متفاوتی رو هم برای این رجیستر در این پورت‌ها تعیین کرده. دلیل این تفاوت در مقدار اولیه، ربطی به بحث ما نداره. اون چیزی که باید مد نظرتون باشه، اینه که همه‌ی پورت‌ها، فقط یه رجیستر به اسم MODER به‌عنوان اولین رجیستر خودشون دارن. پس از این دو سطر رد می‌شیم و رجیستر دوم رو می‌بینیم که OTYPER هست. رجیستر سوم، OSPEEDR نام داره. PUPDR رجیستر چهارم ما هست که اتفاقاً وضعیتی مشابه رجیستر اول داره و دوبار توی جدول تکرار شده، ولی خب دیگه می‌دونیم که باید فقط یک‌بار حسابش کنیم؛ چون تکرارش توی جدول، صرفاً برای بیان تفاوت در مقدار اولیه‌ی این رجیستر در پورت‌های مختلف هست. همین‌طور تا آخر که پیش بریم، می‌بینیم که رجیستر آخر هم اسمش BRR هست.

داخل همین جدول ۲۳، جلوی هر رجیستر، ۳۲تا جا برای ۳۲تا بیت وجود داره. رجیسترهای ما ۳۲-بیتی یا ۴-بایتی هستن. دیگه همه‌چی داره روشن می‌شه. فعلاً فرض کنیم که رجیسترهامون پشت سر هم قرار گرفته‌اند و هیچ فاصله‌ی خالی‌ای بینشون نیست (بعداً درباره‌ی این فرض، صحبت بیشتری می‌کنیم): اگر آدرس یه رجیستر رو داشته باشیم، با اضافه کردن عدد ۴ به اون، آدرس رجیستر بعدیش به‌دست میاد؛ چرا؟ چون هر رجیستر، ۴ بایته؛ مثلاً اگر آدرس یه رجیستر، 0x4 باشه: آدرس بایت اولش، خب همین 0x4 هست دیگه. آدرس بایت دوم، می‌شه 0x5. برای آدرس بایت سوم هم یه بایت دیگه می‌ریم جلو و به 0x6 می‌رسیم. نهایتاً، آدرس بایت چهارم و آخر هم می‌شه 0x7 و بایت چهارم از شروع خونه‌ی 0x7، فضا رو به‌اندازه‌ی فقط ۱ بایت اشغال می‌کنه. ۴تا بایت رجیستر ما تموم شد. پس رجیستر بعدی، از آدرس 0x8 شروع می‌شه که دقیقاً برابر با آدرس شروع رجیستر قبلی (0x4) به‌علاوه‌ی ۴ هست.

حالا از قبل، گفتیم که آدرس رجیستر اول GPIOA، یعنی رجیستر MODER با آدرس پایه‌ی این پریفرال برابر هست: 0x48000000. حالا برای محاسبه‌ی آدرس رجیستر دوم که OTYPER باشه، کافیه ۴ تا به این آدرس اضافه کنیم و به عدد 0x48000004 برسیم. اگر دوباره چهارتا به این عدد جدید اضافه کنیم، به مقدار 0x48000008 می‌رسیم که آدرس رجیستر سوم، یعنی OSPEEDR هست. همین روال رو تا آخر طی می‌کنیم و آدرس همه‌ی رجیسترها رو به‌دست میاریم! پس مسأله‌ی محاسبه‌ی آدرس رجیسترها حل شد…

ولی بحث ادامه داره…

اگر خوووب به این روال نگاه کنیم، می‌بینیم که برای محاسبه‌ی آدرس یه رجیستر، حقیقتاً نیازی نیست که هی دونه‌به‌دونه آدرس رجیسترهای قبلیش رو حساب کنیم؛ بلکه اگه رجیسترها پشت سر هم باشن، کافیه بدونیم که:

  1. اون رجیستر، چندمین رجیستر پریفرال هست، و
  2. آدرس پایه‌ی اون پریفرال، چیه.

مثلاً اگر آدرس رجیستر OSPEEDR رو بخوایم، خب می‌دونیم که سومین رجیستر GPIOA هست که یعنی دوتا رجیستر ۴-بایتی دیگه قبل از اون وجود دارن؛ پس کافیه که از آدرس پایه‌ی GPIOA به‌اندازه‌ی دوتا رجیستر ۴-بایتی بریم جلو و در واقع، ۸تا (یعنی دوتا چهارتا) به آدرس پایه اضافه کنیم: با این حساب، آدرس رجیستر OSPEEDR از جمع کردن آدرس پایه‌ی GPIOA یعنی 0x48000000 با عدد ۸ به‌دست میاد که حاصل این جمع می‌شه 0x48000008 و دقیقاً همون مقداری به‌دست میاد که توی پاراگراف قبلی محاسبه کردیم. به‌نوعی می‌تونیم بگیم که «فاصله‌ی» رجیستر OSPEEDR از آدرس پایه، ۸ بایت هست.

به فاصله‌ی یک رجیستر تا آدرس پایه، می‌گیم آفْسِتْ (Offset).

به ستون اول جدول ۲۳ نگاه کنید. در کنار هر رجیستر، آفست اون رجیستر هم ذکر شده. آفست رجیستر اول، 0x00 هست که برای ما خبر خوبیه و به ما می‌گه که رجیستر اول، فاصله‌ای با آدرس پایه نداره و آدرسش، حقیقتاً با آدرس پایه، برابره (بعداً درباره‌ی حالت خاص دیگه‌ای صحبت می‌کنیم). آفست رجیستر دوم، برابر با 0x04 هست که مطابق انتظارمونه، چون دقیقاً بعد از رجیستر اول قرار داره و ۴ تا با آدرس پایه فاصله داره. آفست رجیستر سوم، برابر با 0x08 هست. انتظار داریم که آفست رجیستر چهارم، ۱۲ باشه که با نگاه کردن به جدول، می‌بینیم که برامون عدد ۱۲ رو به صورت 0x0C نوشته و انتظارمون رو برآورده می‌کنه.

حالا یه راه خیلی سرراست واسه‌ی محاسبه‌ی آدرس یه رجیستر پیدا کردیم: آدرس رجیستر = آدرس پایه + آفست رجیستر.

Register_Address = Base_Address + Register_Offset

یکی-دوتا ریزه‌کاری مهم

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

  • آفست صفر، به رجیستر اول تعلق داره.
  • در آفست چهار، هیچ رجیستری وجود نداره.
  • و آفست هشت، متعلق به رجیستر دوم هست.

مثلاً یه نگاهی به جدول ۱۴ در بخش 4.5.4 با عنوان CRC register map بندازید. رجیسترهای اول و دوم و سوم، در آفست‌های پشت سر هم، به‌ترتیب در صفر و ۴ و ۸ قرار دارن، که با توجه به ۴-بایتی بودن رجیسترها کاملاً طبیعیه و می‌بینیم که هیچ فاصله‌ی خالی‌ای بین این‌ها وجود نداره. اگر رجیستر چهارم هم فاصله‌ای با رجیستر سوم نداشته باشه، باید در آفست ۱۲ قرار بگیره (یعنی ۴تا بعد از آفست ۸ رجیستر سوم)؛ اما در جدول مشخصه که رجیستر چهارم در آفست ۱۶ (0x10) قرار گرفته که با آفست مورد انتظار ما، ۴ بایت فاصله داره. این یعنی که بین رجیستر سوم و چهارم، به‌اندازه‌ی یک رجیستر ۴-بایتی، فضای آدرس بلااستفاده وجود داره.

داخل یه خونواده از میکروکنترلرها، ممکنه که اعضای مختلف خونواده، امکانات متفاوتی داشته باشن و یکی از اعضا یه امکانی رو داشته باشه که اون یکی عضو خونواده، اون امکان رو نداره؛ پس یکی‌شون ممکنه یه رجیستری رو داشته باشه که اون‌یکی نداره. یا حتی داخل یه میکروکنترلر، ممکنه که پریفرال‌های هم‌نوع، مثلاً پورت A و پورت F که هر دو GPIO هستن، امکانات یکسانی نداشته باشن و یکیشون یه رجیستری رو داشته باشه و اون یکی، رجیستر مورد نظر رو نداشته باشه. چنین شرایطی، معمولاً باعث می‌شه که جای یه رجیستر داخل یه پریفرال، خالی باشه و بین رجیسترها، فضای آدرس بلااستفاده به‌وجود بیاد.

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

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

جمع‌بندی

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