تعریف پریفرال‌ها در طبیعت: ARM و CMSIS

در این مقاله می‌خوایم ببینیم که CMSIS چطوری رجیسترهای پریفرال (Peripheral) رو تعریف کرده. برای این منظور، یکی-دو تا از Core Peripheralها رو بررسی می‌کنیم که داخل خیلی از میکروکنترلرهای Cortex-M0 وجود دارن. توجه کنیم که هدف این مقاله، معرفی و بررسی خود این پریفرال‌ها نیست، بلکه ما اینجا فقط قصد داریم تا نحوه‌ی تعریف این پریفرال‌ها و رجیسترهاشون رو بررسی کنیم. این کار می‌تونه به ما، به عنوان مهندس‌های Firmware و Embedded Software و توسعه‌دهنده‌های سطح پایین (Low-level) در درک سورس‌کدهای مختلف و توسعه‌ی سورس‌کدهای جدید کمک کنه.

برای بررسی کدهای مربوط به Core Peripheralها، می‌تونیم مستقیماً به سورس‌کد CMSIS که توسط خود ARM منتشر می‌شه مراجعه کنیم. این سورس‌کد، از طریق آدرس زیر در دسترس ما هست:

https://github.com/ARM-software/CMSIS_5

برای پیدا کردن فایل(های) مربوط به Cortex-M0، وارد فولدر CMSIS، بعدش Core، و درنهایت، Include می‌شیم و فایل core_cm0.h رو باز می‌کنیم.

بررسی تعریف‌های مربوط به تایمر SysTick

خب. برای شروع، می‌ریم سراغ یکی از Core Peripheralها که تایمر SysTick نام داره. اگر SysTick_Type رو داخل فایل هدر مذکور، جست‌وجو کنیم، به چیزی مثل تعریف زیر می‌رسیم:

typedef struct
{
	__IOM uint32_t CTRL;	/* Offset: 0x000 (R/W) SysTick Control and Status Register */
	__IOM uint32_t LOAD;	/* Offset: 0x004 (R/W) SysTick Reload Value Register */
	__IOM uint32_t VAL;		/* Offset: 0x008 (R/W) SysTick Current Value Register */
	__IM  uint32_t CALIB;	/* Offset: 0x00C (R/ ) SysTick Calibration Register */
} SysTick_Type;

حالا ببینیم که این تعریف چی هست و چه معنی‌ای می‌ده. اون رو جزءبه‌جزء بررسی می‌کنیم.

در وهله‌ی اول، کاری به اعضای این struct نداشته باشیم و فقط اون چیزی رو که اطراف این بلاک از کد وجود داره، بررسی کنیم:

typedef struct
{
	/* Struct Members ... */
} SysTick_Type;

در کد بالا، کامنت /* Struct Members ... */ رو جایگزین تعریف اعضای struct کرده‌ایم تا کد، ساده‌تر بشه. اتفاقی که اینجا افتاده، اینه که ما یه struct بدون نام تعریف کرده‌ایم و اومده‌ایم با یه typedef، یه اسم براش گذاشته‌ایم: SysTick_Type. از این به بعد، می‌تونیم که با استفاده از اسم SysTick_Type به نوع داده‌ی این struct بی‌نام دسترسی داشته باشیم.

حالا چرا ممکنه که تعریف یه struct ساده رو اینقدر بپیچونن؟ خلاصه‌ی قضیه اینه که داخل زبان C، هر وقت که بخوایم از یه نوع داده‌ی struct استفاده کنیم و مثلاً یه متغیر یا اشاره‌گر از اون نوع بسازیم، باید علاوه بر اسم اون struct، خودِ کلمه‌ی کلیدی struct رو موقع تعریف اون متغیر یا اشاره‌گر هم دوباره تکرار کنیم. حالا بعضی‌ها از این تکراره خوششون نمیاد و به‌همین‌خاطر، میان موقع تعریف یه struct از typedef هم استفاده می‌کنن تا با معرفی یه اسم جدید، دیگه نیازی به تکرار کلمه‌ی کلیدی struct نداشته باشن. یه همچین ترکیبی از struct و typedef خیلی‌خیلی مرسوم هست و این الگو رو جاهای زیادی می‌بینید. اگر چیزی داخل این تعریف براتون مبهم هست یا دوست دارید مطالعه‌ی بیشتری درباره‌ی اون داشته باشید، می‌تونید به مقاله‌ی بررسی ترکیب struct و typedef مراجعه کنید.

حالا که وضعیت بیرون بلاک struct روشن شد، وارد تعریف اعضای struct می‌شیم و اعضا رو بررسی می‌کنیم. تعریف SysTick_Type به شکل زیر بود:

typedef struct
{
	__IOM uint32_t CTRL;	/* Offset: 0x000 (R/W) SysTick Control and Status Register */
	__IOM uint32_t LOAD;	/* Offset: 0x004 (R/W) SysTick Reload Value Register */
	__IOM uint32_t VAL;		/* Offset: 0x008 (R/W) SysTick Current Value Register */
	__IM  uint32_t CALIB;	/* Offset: 0x00C (R/ ) SysTick Calibration Register */
} SysTick_Type;

تنها چیز عجیبی که داخل کد بالا وجود داره، اون __IM و __IOMهاس. این‌ها ماکروهایی هستن که در اوایل همین فایل هدر تعریف شده‌اند:

/* following defines should be used for structure members */
#define	__IM	volatile const	/* Defines 'read only' structure member permissions */
#define __OM	volatile        /* Defines 'write only' structure member permissions */
#define __IOM	volatile        /* Defines 'read / write' structure member permissions */

همون‌طوری که می‌بینیم، ماکروی __IM به کلمات کلیدی volatile const و ماکروی __IOM هم به‌سادگی به کلمه‌ی کلیدی volatile تبدیل می‌شه. پس اگر این ماکروها رو داخل تعریف SysTick_Type جایگذاری کنیم، داریم:

typedef struct
{
	volatile 		uint32_t CTRL;	/* (R/W) */
	volatile 		uint32_t LOAD;	/* (R/W) */
	volatile 		uint32_t VAL;	/* (R/W) */
	volatile const	uint32_t CALIB;	/* (R/ ) */
} SysTick_Type;

به زبان خیلی ساده، وجود volatile به این خاطر هست که اگر در کد C خودمون، روی یه رجیستر یه عملیاتی انجام دادیم، مثلاً مقداری رو روی اون نوشتیم یا مقدارش رو خوندیم، مطمئن باشیم که کامپایلر، حتماً حتماً اون عملیات رو انجام می‌ده و یه‌وقتی هوس بهینه‌سازی و حذف اون عملیات ما به سرش نمی‌زنه. اگر رجیسترهای خودمون رو به صورت متغیرهای معمولی تعریف کنیم و از volatile استفاده نکنیم، این امکان وجود داره که کامپایلر به صلاحدید خودش، عملیاتی رو که ما روی اون «متغیر معمولی» انجام می‌دیم، بنا به دلایل مختلف حذف کنه یا تغییر بده و این چیزی نیست که ما بخوایم. البته این توضیحِ خیلی ساده‌ای هست و شما می‌تونید برای مشاهده‌ی توضیحات بیشتر درباره‌ی volatile و یکی-دو تا مثال، به بخش بهینه‌سازی‌های کامپایلر در مقاله‌ی دسترسی به رجیسترهای پریفرال، مراجعه کنید. پس دلیل تعریف رجیسترها به صورت volatile مشخص شد.

اما یکی از رجیسترها، CALIB، یه کلمه‌ی کلیدی const هم پشت سر خودش داره؛ چرا؟ چون همون‌طوری که از کامنت‌های جلوی کد هم مشخص هست، CALIB یه رجیستر «Read Only» یا «فقط خواندنی» هست: فقط می‌شه مقدارش رو خوند و نمی‌شه هیچ مقداری روش نوشت. داخل کامنت‌ها، رجیسترهایی که هم می‌شه مقدارشون رو خوند (Read) و هم می‌شه مقداری رو روی اون‌ها نوشت (Write)، با (R/W) مشخص شده‌اند و رجیسترهایی که فقط می‌شه مقدارشون رو خوند، با (R/ ) نشون داده شده‌اند. چنین اطلاعاتی رو درباره‌ی رجیسترها، می‌شه از طریق داکیومنت‌های رسمی و/یا دیتاشیت میکروکنترلرها به‌دست آورد. پس چون رجیستر CALIB فقط باید قابلیت خوندن رو ارائه بده و باید جلوی هرگونه نوشتن رو بگیره، ما به‌سادگی می‌تونیم با اضافه‌کردن کلمه‌ی کلیدی const قبل از تعریف اون، به این هدف برسیم.

حالا برگردیم سر تعریف اعضای struct که بیانگر رجیسترهامون باشن. هر رجیستر مربوط به SysTick در کد بالا در یک خط تعریف شده و در مجموع، چهار تا رجیستر داریم. برای هر رجیستر، یه فضای ۳۲-بیتی (یا ۴-بایتی) مشخص شده (با توجه به uint32_t)، پس ما داخل این ساختار، چهار تا خونه‌ی ۴-بایتی داریم و اندازه‌ی کل struct ما، ۱۶ بایت هست. این نکته خیلی مهمه: اندازه و ترتیب اعضای struct، تعیین می‌کنه که یک عضو، دقیقاً چه بایت‌هایی از این ۱۶ بایت رو به خودش اختصاص می‌ده؛ پس ۴ بایت اول ما، متناظر با عضو CTRL هست و مثلاً عضو VAL هم با ۴ بایت سوم متناظره. درسته؟

تا اینجا اسم، ترتیب، و تعداد بیت‌های هر کدوم از رجیسترهای SysTick تعریف شده، اما این کافی نیست. ما درنهایت، برای دسترسی به این رجیسترها، باید آدرس دقیق اون‌ها رو داخل حافظه داشته باشیم تا بتونیم از روی اون آدرس حافظه یه مقداری رو بخونیم یا اینکه روی اون آدرس حافظه، یه مقداری رو بنویسیم: الآن ما می‌دونیم که رجیستر LOAD داخل ۴ بایت دوم از این ۱۶ بایت قرار داره، ولی هنوز تعیین نکرده‌ایم که خود این ۱۶ بایته کجاست! مثل این می‌مونه که ما فقط پلاک یه ساختمون رو داشته باشیم، ولی ندونیم که توی کدوم کوچه است!

در حقیقت قضیه اینه: فرض کنید که ما الآن بیایم و یه «متغیر» از نوع SysTick_Type تعریف کنیم. بعد به یکی از «رجیستر»ها دسترسی پیدا کنیم و یه مقداری رو، مثلاً 0xFF، روش بنویسیم. کد زیر رو ببینید:

// It does NOT work as intended...
SysTick_Type SysTick;
SysTick.LOAD = 0xFF;

اتفاقی که اینجا رخ می‌ده، اینه که کامپایلر و لینکر میان یه مقداری حافظه به‌اندازه‌ی نوع داده‌ی SysTick_Type درون فضای حافظه‌ی RAM (!!!) درنظر می‌گیرن و مقدار خونه‌های مربوط به LOAD رو برابر با 0xFF قرار می‌دن! کامپایلر، خونه‌های مربوط به LOAD رو چطوری تشخیص می‌ده؟ از روی تعریف SysTick_Type: چهار تا بایت اول، مربوط به CTRL هستن و جای LOAD داخل چهار تا بایت دوم هست. اما مشکل اساسی اینجاست که متغیر ما، داخل محدوده‌ی حافظه‌ی RAM ساخته می‌شه و کلاً کوچکترین ارتباطی با فضای حافظه‌ی مربوط به SysTick و رجیسترهای اون،‌ نداره! در واقع، این متغیر، الآن هیچ ربطی به SysTick نداره و با یه متغیر کاملاً معمولی که هر جای دیگه از برنامه هم ممکنه تعریف بشه، هیچ فرق خاصی نمی‌کنه. در ادامه‌ی مثال بالا، ما پلاک ساختمون رو بلدیم، ولی کوچه رو اشتباهی اومده‌ایم. پس اگر ما می‌خوایم که حقیقتاً به SysTick و رجیسترهاش دسترسی داشته باشیم، نیاز داریم تا یه‌جوری به کامپایلر و لینکر بفهمونیم که به‌جای دسترسی به RAM، باید به آدرس‌های خاصی از حافظه که مخصوص SysTick هست، مراجعه کنن. باید نام و نشون کوچه رو تعیین کنیم!

راه‌حل‌های مختلفی برای این مسأله وجود داره. وقتی بحث «آدرس» می‌شه، قاعدتاً یکی از اولین راه‌هایی که به ذهن می‌رسه، استفاده از اشاره‌گر هست. اگر یه اشاره‌گر به نوع SysTick_Type تعریف کنیم، اون‌موقع می‌تونیم بهش بگیم که دقیقاً به چه آدرسی از حافظه اشاره کنه. اگر آدرس درستی رو به اون اشاره‌گر بدیم، اون‌وقت دسترسی‌های ما به اعضای SysTick_Type، با آدرس‌هایی انجام می‌شه که مربوط به رجیسترهای SysTick هست.

اگر نگاهی به داکیومنت Arm Cortex-M0 Devices Generic User Guide بندازیم، داخل بخش 4.1 About the Cortex-M0 Peripherals، جدولی وجود داره که آدرس‌های مربوط به Core Peripheralها رو به‌تفکیک، نوشته. داخل این جدول می‌بینیم که از آدرس 0xE000E010 تا آدرس 0xE000E01F مربوط به SysTick هست و دسترسی به رجیسترهای این پریفرال، از طریق این بازه از آدرس‌ها امکان‌پذیره. به آدرس شروع این بازه، که همون آدرس کوچیکتر (0xE000E010) باشه، می‌گیم آدرس پایه (Base Address). اگر داخل داکیومنت جلوتر بریم، داخل جدول موجود در بخش 4.4 Optional system timer, SysTick، آدرس رجیسترهای SysTick رو به‌تفکیک، می‌بینیم (البته ممکنه اسم اختصاری این رجیسترها با اسمی که توی سورس‌کد هست، فرق داشته باشه که مهم نیست). همون‌طوری که می‌بینید، رجیستر اول در آدرس 0xE000E010، یعنی همون آدرس شروع یا آدرس پایه‌ی ناحیه‌ی SysTick قرار داره. از اون‌جایی که رجیسترها، ۴-بایتی هستن، پس انتظار داریم که رجیستر دوم، ۴ بایت بعد از رجیستر اول و در آدرس 0xE000E014 قرار داشته باشه، که با نگاه کردن به جدول مذکور، می‌بینیم که محاسبه‌ی ما درسته. در حقیقت، آدرس رجیستر اول برابر با آدرس پایه هست، آدرس رجیستر دوم برابر با آدرس پایه به‌علاوه‌ی ۴، آدرس رجیستر سوم برابر با آدرس پایه به‌علاوه‌ی ۸ (یعنی دو تا چهار تا)، و همین‌طور الی آخر.

این مفهوم، یعنی فاصله‌ی یه رجیستر نسبت به آدرس پایه‌ی یک ناحیه از حافظه رو بهش می‌گیم آفْسِتْ (Offset). اگر نگاه دوباره‌ای به تعریف SysTick_Type بندازیم، می‌بینیم که داخل کامنت جلوی هر رجیستر، مقدار آفست اون رجیستر هم ذکر شده:

typedef struct
{
	__IOM uint32_t CTRL;	/* Offset: 0x000 (R/W) SysTick Control and Status Register */
	__IOM uint32_t LOAD;	/* Offset: 0x004 (R/W) SysTick Reload Value Register */
	__IOM uint32_t VAL;		/* Offset: 0x008 (R/W) SysTick Current Value Register */
	__IM  uint32_t CALIB;	/* Offset: 0x00C (R/ ) SysTick Calibration Register */
} SysTick_Type;

همون‌طوری که از کامنت‌ها هم مشخصه، رجیستر اول در آفست صفر قرار داره؛ یعنی فاصله‌ی رجیستر اول تا آدرس پایه‌ی SysTick، صفر هست و در حقیقت، رجیستر اول در همون آدرس پایه قرار داره. آفست رجیستر دوم، مقدار ۴ هست و نشون می‌ده که آدرس این رجیستر، همون آدرس پایه به‌علاوه‌ی ۴ می‌شه. می‌بینید که موقع تعریف اعضای struct، با انتخاب درست ترتیب و نوع داده‌ی اون‌ها، آفست رجیسترها رو به شکل ضمنی تعیین کرده‌ایم؛ اون بحثی که درباره‌ی بایت‌های مورد استفاده‌ی هر عضو struct گفتیم، در حقیقت همین بحث آفست هست (همون مثال پلاک ساختمون). پس هر زمان که آدرس پایه رو داشته باشیم، برای به دست آوردن آدرس یه رجیستر، کافیه که آدرس پایه رو با مقدار آفست جمع کنیم. این مورد رو به‌خاطر داشته باشید. (برای بحث بیشتر، به مقاله‌ی نحوه‌ی محاسبه‌ی آدرس رجیسترهای پریفرال در STM32 مراجعه کنید.)

پس با توجه به چند پاراگراف قبلی، اگر بتونیم که یه اشاره‌گر به نوع SysTick_Type بسازیم و بهش بگیم که به آدرس پایه‌ی ناحیه‌ی مربوط به SysTick، یعنی 0xE000E010 اشاره کنه، اون وقت می‌تونیم به رجیسترهای SysTick دسترسی داشته باشیم. کد زیر رو ببینید. در این کد، ثابت عددی 0xE000E010 به صورت 0xE000E010UL (با یک UL در انتهای مقدار ثابت) نوشته شده تا اطمینان داشته باشیم که کامپایلر، ثابت عددی ما رو به صورت یک عدد Unsigned (بدون علامت) و Long درنظر می‌گیره.

SysTick_Type *SysTick = (SysTick_Type *)(0xE000E010UL);

در این کد، یه اشاره‌گر به نوع SysTick_Type تعریف کرده‌ایم، آدرس پایه‌ی ناحیه‌ی حافظه‌ی SysTick رو در اون قرار داده‌ایم، و برای تبدیل اون عدد ثابت به یه آدرس، از تبدیل نوع (SysTick_Type *) استفاده کرده‌ایم. حالا می‌تونیم به رجیسترها دسترسی داشته باشیم. توجه داشته باشید که چون متغیر ما، یک اشاره‌گر هست، برای دسترسی به اعضای struct، باید از عملگر -> استفاده کنیم.

SysTick->LOAD = 0xFF;

در اینجا، وقتی که مقدار 0xFF رو درون عضو LOAD می‌ریزیم، آدرس حافظه‌ای که این عملیات باید روی اون انجام بشه، به‌درستی محاسبه می‌شه. چطوری؟ بحث آفست و آدرس پایه رو که یادتون هست؟ در حقیقت، هر زمانی که کامپایلر ببینه که ما می‌خوایم توسط متغیر SysTick به اعضای نوع داده‌ی SysTick_Type دسترسی پیدا کنیم، آدرس نهایی عضو موردنظر ما رو از جمع کردن آفست اون عضو و آدرس درون اشاره‌گر، به‌دست میاره. اشاره‌گر ما، آدرس پایه‌ی پریفرال SysTick رو درون خودش داره، پس کامپایلر به‌سادگی می‌تونه با اضافه کردن آفست هر عضو به اون آدرس پایه، آدرس نهایی صحیح رو برای اون عضو به‌دست بیاره. پس تونستیم که به‌کمک یه متغیر اشاره‌گر، به رجیسترها دسترسی پیدا کنیم.

اما راستش تعریف یک «متغیر» اشاره‌گر، خودش ممکنه که یه فضایی از حافظه‌ی RAM رو برای نگهداری محتوای متغیر (یعنی آدرسی که اشاره‌گر بهش اشاره می‌کنه)، اشغال کنه. یه راه مشابه هم وجود داره که از متغیر استفاده نمی‌کنه. یه بار دیگه به این دو خط بالا که نوشتیم، در کنار هم توجه کنید:

SysTick_Type *SysTick = (SysTick_Type *)(0xE000E010UL);
SysTick->LOAD = 0xFF;

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

((SysTick_Type *)(0xE000E010UL))->LOAD = 0xFF;

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

#define SysTick ((SysTick_Type *)(0xE000E010UL))

اون‌وقت می‌تونیم به‌سادگی، همون کد دلخواه خودمون رو بنویسیم:

SysTick->LOAD = 0xFF;

SysTick در اینجا یک ماکرو هست و دیگه ما متغیر اشاره‌گر نداریم و نگرانی ما بابت استفاده از RAM برطرف می‌شه.

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

#define SCS_BASE	 (0xE000E000UL)		   /* System Control Space Base Address */
#define SysTick_BASE (SCS_BASE + 0x0010UL) /* SysTick Base Address */
#define NVIC_BASE    (SCS_BASE + 0x0100UL) /* NVIC Base Address */
#define SCB_BASE     (SCS_BASE + 0x0D00UL) /* System Control Block Base Address */

#define SCB     ((SCB_Type *) SCB_BASE)   		/* SCB configuration struct */
#define SysTick	((SysTick_Type *) SysTick_BASE) /* SysTick configuration struct */
#define NVIC    ((NVIC_Type *) NVIC_BASE)   	/* NVIC configuration struct */

سه ماکروی آخری، یعنی SCB، SysTick، و NVIC، درحقیقت همون Core Peripheralهامون هستن و ما می‌تونیم با استفاده از اون‌ها، به رجیسترهای مربوطه دسترسی پیدا کنیم. ماکروهای SysTick_BASE، NVIC_BASE، و SCB_BASE هم برابر با مقدار آدرس پایه‌ی پریفرال متناظر، تعریف شده‌اند. ماکروی SCS_BASE هم آدرس پایه‌ی کل بلاک‌های مربوط به Core Peripheralهاست. به‌عنوان مثال، SysTick_BASE از مجموع مقدار SCS_BASE و 0x0010UL برابر با 0xE000E010UL به‌دست میاد، که دقیقاً همون آدرس پایه‌ی SysTick هست.

بررسی تعریف‌های مربوط به SCB

حالا که با نگاه به تعریف تایمر SysTick، خیلی از مفاهیم رو بررسی کرده‌ایم، یه نگاهی به تعریف یکی دیگه از Core Peripheralها، یعنی SCB یا System Control Block هم می‌اندازیم تا ببینیم که آیا نکته‌ی جدیدی برامون داره یا نه. اگر در همون فایل هدر دنبال SCB_Type بگردیم، تعریفی مشابه کد زیر رو پیدا می‌کنیم:

typedef struct
{
	__IM  uint32_t CPUID; 		/* Offset: 0x000 (R/ ) */
	__IOM uint32_t ICSR;  		/* Offset: 0x004 (R/W) */
		  uint32_t RESERVED0;
	__IOM uint32_t AIRCR;  		/* Offset: 0x00C (R/W) */
	__IOM uint32_t SCR;  		/* Offset: 0x010 (R/W) */
	__IOM uint32_t CCR;  		/* Offset: 0x014 (R/W) */
    	  uint32_t RESERVED1;
	__IOM uint32_t SHP[2U]; 	/* Offset: 0x01C (R/W) [0] is RESERVED */
	__IOM uint32_t SHCSR; 		/* Offset: 0x024 (R/W) */
} SCB_Type;

اول از همه در اطراف بلاک، همون ساختار ترکیبی typedef و struct رو می‌بینیم. وقتی سراغ اعضای این struct بریم، توجه می‌کنیم که مثل قبل، رجیسترهای ۳۲-بیتی داریم که همه‌شون به‌کمک ماکروهای __IOM و __IM، به‌ترتیب به صورت volatile و volatile const تعریف شده‌اند. اما دو تا چیز جدید وجود داره: یکی- وجود دو تا عضو تحت عنوان RESERVED0 و RESERVED1، و دومی- تعریف عضو SHP، به صورت یه آرایه. قضیه‌ی این‌ها چیه؟

مورد اول- اعضایی که اسمشون با RESERVED شروع می‌شه، اعضایی هستن که در یک معماری خاص (یا در یک میکروکنترلر خاص)، با هیچ رجیستری متناظر نیستن. مثلاً عضو RESERVED0 رو درنظر بگیرید. این عضو، بین رجیسترهای ICSR و AIRCR قرار داره و برای نشون دادن یه آدرس خالی و بلااستفاده بین این دو تا رجیستر درنظر گرفته شده. به بیان بهتر، داخل معماری Cortex-M0، یه فضای آدرس اضافی و بلااستفاده، به اندازه‌ی ۴ بایت بین رجیسترهای ICSR و AIRCR وجود داره که هیچ رجیستری در اون آدرس قرار نگرفته. برای تحقیق این مسأله، دوباره به داکیومنت Arm Cortex-M0 Devices Generic User Guide نگاه می‌کنیم. جدولی که در بخش 4.3 System Control Block وجود داره، رجیسترهای این پریفرال رو لیست کرده. رجیستر ICSR در آدرس 0xE000ED04 قرار داره. رجیسترهای ما، ۴-بایتی هستن، پس رجیستر ICSR، از آدرس 0xE000ED04 تا قبل از آدرس 0xE000ED08 رو به خودش اختصاص می‌ده (یعنی بایت‌های موجود در آدرس 0xE000ED04 تا پایان 0xE000ED07، که جمعاً ۴ بایت هستن) و انتظار داریم که رجیستر بعدی، در آدرس 0xE000ED08 قرار داشته باشه؛ اما رجیستر بعدی، AIRCR هست و در آدرس 0xE000ED0C قرار گرفته. چون بین آدرس 0xE000ED08 و 0xE000ED0C، به‌اندازه‌ی ۴ بایت فاصله هست، پس در فضای ۴-بایتی بین ICSR و AIRCR، هیچ رجیستری وجود نداره. این مسأله، داخل کامنت‌های کد بالا هم در قالب آفست رجیسترها، نشون داده شده. آفست رجیستر ICSR برابر با 0x004 و آفست رجیستر AIRCR مساوی با 0x00C هست و با توجه به ۴-بایتی بودن رجیسترها، در آفست 0x008 رجیستری وجود نداره.

حالا سؤال اینه که اگه در این آفست، رجیستری وجود نداره، پس چرا یه عضو ۴-بایتی RESERVED0 براش تعریف می‌شه؟ خب، فرض کنیم که اون عضو رو تعریف نکنیم. حالا اون چیزی که کامپایلر می‌بینه، اینه که رجیستر AIRCR دقیقاً ۴ بایت بعد از رجیستر ICSR و در آفست 0x008 قرار داره! این آفست برای رجیستر AIRCR، اشتباهه؛ پس کامپایلر، آدرس رجیستر AIRCR (و تمام رجیسترهای بعدیش) رو اشتباه محاسبه می‌کنه. پس تعریف کردن این عضو RESERVED0، یه «فضای خالی» ۴-بایتی داخل آفست رجیسترها ایجاد می‌کنه که محاسبه‌ی آدرس صحیح رو برای کامپایلر، ممکن می‌کنه.

این اعضای RESERVED، هیچ کاربرد عملی دیگه‌ای ندارن و صرفاً جاهای خالی و بلااستفاده رو در فضای آدرس پر می‌کنن تا بتونیم آدرس سایر رجیسترها رو درست محاسبه کنیم. پس هیچ‌وقت قرار نیست که به اون‌ها دسترسی پیدا کنیم. به‌همین‌خاطر، نیازی هم نداره که اون‌ها رو به‌صورت volatile تعریف کنیم. اما طول نوع داده‌ی انتخابی برای تعریف اون‌ها، قطعاً اهمیت داره؛ مثلاً در اینجا، چون فاصله‌ی «خالی» ما، ۴ بایت بود، ما هم برای تعریف RESERVED0 از نوع داده‌ی uint32_t استفاده کردیم. اگر می‌خواستیم که یه فضای ۱-بایتی یا ۲-بایتی رو کنار بذاریم، باید به‌ترتیب، از uint8_t یا uint16_t استفاده می‌کردیم. همین‌طور، اگر فضای آدرس «خالی» ما، مثلاً به‌اندازه‌ی ۵ تا رجیستر ۳۲-بیتی بود، می‌تونستیم یه آرایه‌ی uint32_t با طول ۵ تعریف کنیم.

مورد دوم- چرا عضو SHP به‌صورت یه آرایه از جنس uint32_t و با طول ۲ تعریف شده؟ اگر نگاهی به داکیومنت ARM بندازیم، می‌بینیم که اصلاً رجیستر SHP، با این نام وجود نداره و به جای اون، رجیسترهای ۳۲-بیتی SHPR2 و SHPR3 وجود دارن. در حقیقت، ما اینجا دو تا رجیستر داریم که ماهیت کارکردی و عملیاتی یکسانی دارن: تنظیم اولویت چند تا از exception handlerها؛ به‌نوعی، می‌شه گفت که این دو تا رجیستر، انگار یک رجیستر با اندازه‌ی بزرگتر هستن. در چنین حالتی، تصمیمی که CMSIS گرفته اینه که این‌جور رجیسترها رو به‌صورت جدا-جدا تعریف نکنه و به‌جاش، یه آرايه با طول مناسب تعریف کنه. واضحه که طول آرایه، باید با تعداد رجیسترهای واقعی ما، برابر باشه؛ اینجا ما SHP رو با اندازه‌ی ۲ تعریف کرده‌ایم تا جای ۲ تا رجیستر SHPR2 و SHPR3 رو بگیره. چون رجیسترهای ما ۳۲-بیتی هستن، پس اعضای آرایه هم (که بیانگر رجیسترها هستن) باید ۳۲-بیتی باشن. حالا چون با آرایه طرف هستیم، برای دسترسی به SHPR2 باید از SHP[0] استفاده کنیم و دسترسی به SHPR3 هم به‌صورت SHP[1] امکان‌پذیر هست؛ چون SHPR2 در آفست کوچکتر قرار داره، پس با ایندکس صفر بهش دسترسی پیدا می‌کنیم. توجه داریم که این آرایه طولش ۲ هست و اعضاش هم uint32_t هستن، پس دقیقاً به‌اندازه‌ی دوتا عضو از جنس uint32_t فضا در struct درنظر گرفته می‌شه؛ به‌همین‌خاطر، این نوع تعریف رجیسترها درقالب آرایه، تغییری داخل آفست رجیسترها و محاسبه‌ی آدرس ایجاد نمی‌کنه و همه‌چی مثل همون حالتی هست که ما رجیسترها رو در قالب دو تا عضو جدا تعریف کنیم.

جمع‌بندی

در این مقاله، تعریف Core Peripheralهای ARM رو در CMSIS بررسی کردیم. این جور مطالعات موردی به ما کمک می‌کنه تا تکنیک‌های جدیدی رو یاد بگیریم و همزمان، کاربرد روش‌هایی رو ببینیم که تا الآن برامون بیشتر حالت تئوری داشته‌اند. چنین مطالعات و تجربیاتی می‌تونه برای یه توسعه‌دهنده‌ی سطح پایین و سیستمی، خیلی ارزشمند باشه و شناخت عمیق‌تری رو از نرم‌افزار، Toolchains، و سخت‌افزار ارائه بده. اگر توسعه‌دهنده‌ی Embedded Software یا Firmware باشیم، داشتن اطلاعات سطح پایین‌تر می‌تونه به ما کمک کنه تا راه‌حل‌های بهینه‌تری ارائه کنیم، کد بهتری توسعه بدیم، و در زمان مواجهه با خطا هم راحت‌تر متوجه مشکل بشیم و اون رو حل کنیم. اگر هم توسعه‌دهنده‌ی library و زیرساخت‌های نرم‌افزاری هستیم یا با سیستم‌های جدید و ناآشنا سروکار داریم، علاوه بر سایر مزایایی که گفتیم، ممکنه که خیلی از این روش‌ها به طور مستقیم به کارمون بیان. کافیه که با توکل بر خدا و تلاش مستمر، مطالعه کنیم، برنامه بنویسیم، مسأله حل کنیم، طراحی و تجزیه‌وتحلیل انجام بدیم، آزمایش و تجربه کنیم، و در کنار همه‌ی این‌ها، با قدم گذاشتن توی دل طبیعت، کدهای موجود در دنیای واقعی رو هم بررسی کنیم و دانش فنی خودمون رو عمیق‌تر کنیم یا گسترش بدیم؛ با این کار، إن‌شاءالله می‌تونیم توسعه‌دهنده‌ی بهتری بشیم.