یک بافر 256 بایتی ساختهاید؛ از کجا میدانید در RAM اصلی قرار گرفته یا در CCM؟ در این درس، مسیر تبدیل نام یک متغیر در زبان C به نشانی واقعی آن را دنبال میکنیم. با نقشهٔ حافظه، فایل راهانداز و Linker Script آشنا میشویم و محل بافر را در خروجی واقعی ساخت بررسی میکنیم. پیشنیاز، درس ساخت و پروگرام برنامه است.
وضعیت اعتبارسنجی: کامپایلشده؛ پروژهٔ واقعی بدون خطا و هشدار ساخته شده است. اجرای روی برد، پروگرام و اندازهگیری فیزیکی هنوز انجام نشدهاند.
مثال برای STM32F303CBT6 با بستهٔ LQFP48 روی برد GebraBit GB100ST V2.0 است. قطعهکدها گزیدهٔ پروژهٔ کاملاند و به تعریفها و فایلهای همراه وابستهاند.
پیشنیاز: درس 6.
نقشهٔ حافظه؛ هر نشانی به کجا میرود؟
برای درک سازوکار حافظه در میکروکنترلر، یک میز کار مهندسی را تصور کنید. حافظه فلش (Flash) مانند کمد بایگانی دائمی شرکت است؛ نقشهها و کتابچهها به صورت ماندگار در آن قرار دارند و حتی با قطع برق نیز پاک نمیشوند. حافظه رم اصلی (SRAM) مانند میز کار بزرگ و اشتراکی اتاق است که مهندسان و دستیاران مختلف میتوانند ابزارهای موقت را روی آن قرار دهند و پس از اتمام کار، محتویات آن تخلیه میشود. در این میان، حافظه CCM RAM (Core Coupled Memory) مانند یک کشوی نزدیک پردازنده است؛ در این تراشه، واحد DMA به آن دسترسی ندارد.
نقشه آدرس فیزیکی تراشه STM32F303CBT6
فضای آدرسدهی پردازندههای 32 بیتی معماری ARM Cortex-M4 به صورت خطی از نشانی 0x00000000 تا 0xFFFFFFFF به پهنای 4 گیگابایت گسترده شده است. سازنده تراشه بخشهای مختلف این فضا را به سختافزارهای گوناگون اختصاص داده است. در تراشه مورد استفاده ما (STM32F303CBT6)، چیدمان فیزیکی بدین شرح است:
- حافظه فلش (128 KB): از نشانی 0x08000000 تا 0x0801FFFF گسترش یافته و محل ذخیره دائم دستورالعملهای برنامه است.
- حافظه رم اصلی (32 KB SRAM): از نشانی 0x20000000 تا 0x20007FFF امتداد دارد و میزبان متغیرها و بافرهای عمومی است.
- حافظه اختصاصی هسته (8 KB CCM RAM): در نشانی جداگانه 0x10000000 تا 0x10001FFF قرار دارد. توجه کنید که رم این تراشه یکپارچه 40 کیلوبایت پیوسته نیست، بلکه به دو جزیره فیزیکی مجزا تقسیم شده است.
- پریفرالها و رجیسترها: از نشانی 0x40000000 به بالا به ثباتهای کنترلی واحدهایی نظیر تایمرها، پورتهای سریال و ورودی/خروجیها متصل است.
ماتریس گذرگاه (Bus Matrix) و انزوای CCM RAM
پردازنده Cortex-M4 از طریق سه گذرگاه مجزا شامل گذرگاه دستورالعمل (I-Code)، گذرگاه داده (D-Code) و گذرگاه سیستم (System Bus) با حافظهها گفتگو میکند. ماتریس گذرگاه به پردازنده و واحد دسترسی مستقیم به حافظه (DMA) اجازه میدهد به صورت همزمان به بخشهای مختلف دسترسی داشته باشند. اما نکته حیاتی اینجاست: حافظه CCM RAM به صورت اختصاصی فقط به گذرگاههای هسته پردازنده متصل است. ماتریس گذرگاه هیچ مسیر فیزیکی برای اتصال کنترلر DMA به محدوده نشانی 0x10000000 ندارد. بنابراین پردازنده میتواند از مسیر اختصاصی هسته به CCM RAM دسترسی داشته باشد، اما واحد DMA در صورت اقدام به خواندن یا نوشتن در این ناحیه به این حافظه دسترسی ندارد؛ بافر انتقال باید در RAM اصلی باشد.

بخشهای برنامه و تفاوت نشانی بارگذاری با نشانی اجرا
کامپایلر زبان C کدهای نوشتهشده توسط برنامهنویس را به شش بخش منطقی اصلی تقسیم میکند. شناخت این بخشها مبنای مدیریت صحیح حافظه است:
- بخش .text: حاوی کدهای ترجمهشده ماشین و دستورالعملهای اجرایی توابع برنامه است.
- بخش .rodata: حاوی دادههای فقطخواندنی مانند رشتههای متنی داخل گیومه و متغیرهای تعریفشده با کلمه کلیدی const است.
- بخش .data: دربرگیرنده متغیرهای سراسری (global) و ایستا (static) است که مقدار اولیه غیرصفر دارند.
- بخش .bss: شامل متغیرهای سراسری و ایستایی است که یا مقدار اولیه ندارند یا مقدار آنها صفر تعیین شده است.
- بخش پشته (Stack): حافظه موقتی برای نگهداری متغیرهای محلی درون توابع، آدرس بازگشت از فراخوانیها و محتوای رجیسترها در زمان بروز وقفه.
- بخش هیپ (Heap): فضای رزروشده برای تخصیص پویای حافظه به کمک دستوراتی مانند malloc().
مفهوم نشانی بارگذاری (LMA) و نشانی اجرا (VMA)
یکی از مهمترین مفاهیم در سیستمهای تعبیهشده تفاوت میان دو نشانی است: نشانی بارگذاری (Load Memory Address یا LMA) محلی در حافظه پایدار فلش است که فایل باینری برنامه هنگام پروگرم شدن در آن نوشته میشود. نشانی اجرا (Virtual/Execution Memory Address یا VMA) نشانیای است که متغیر یا دستورالعمل هنگام اجرای زنده پردازنده باید در آن مقیم باشد.
بخشهای .text و .rodata در پروژهٔ این درس در Flash قرار دارند، بنابراین LMA و VMA آنها یکسان است (مثلاً 0x08000500). اما متغیرهای بخش .data وضعیتی دوگانه دارند؛ مقادیر اولیه آنها باید در فلش ذخیره شود تا پس از خاموش شدن میکرو از بین نرود (LMA در محدوده 0x08000000)، اما هنگام اجرا باید در رم قرار گیرند تا مقدار آنها قابل تغییر باشد (VMA در محدوده 0x20000000). برای محتوای صفرشوندهٔ بخش .bss تصویر اولیهای در Flash ذخیره نمیشود، زیرا مقدار تمام اعضای آن صفر است و نیازی به ذخیره تصویر اولیه ندارد؛ این بخش فقط دارای VMA در حافظه رم است.
جهت رشد پشته و هیپ و محاسبه ردپای حافظه
در میکروکنترلرهای مبتنی بر ARM، پشته به صورت نزولی رشد میکند. اشارهگر پشته در ابتدای کار در بالاترین نشانی رم یعنی _estack = 0x20008000 قرار میگیرد و با فراخوانی توابع به سمت پایین حرکت میکند. در نقطه مقابل، هیپ بلافاصله پس از پایان بخش .bss در ابتدای رم آغاز شده و به سمت بالا رشد میکند. اگر عمق فراخوانی توابع یا متغیرهای محلی بیش از حد بزرگ شود، خطر برخورد پشته با متغیرهای سراسری یا هیپ پدید میآید که میتواند دادهها و روند اجرا را خراب کند؛ ریست خودکار نتیجهٔ تضمینشدهٔ این وضعیت نیست.
برای یک مثال ساده، بافر 64 عضوی از نوع 32 بیتی به 256 بایت نیاز دارد:
64 × 4 bytes = 256 bytes
اگر این بافر مقدار اولیه داشته باشد، هم به فضای اجرای RAM و هم به تصویر اولیه در Flash نیاز دارد. بافر صفرشونده، فضای RAM میگیرد ولی برای تکتک صفرهای آن تصویر ذخیره نمیشود. اعداد واقعی بخشها و فاصلههای همترازی را باید از فایل نقشه خواند؛ جمع چند بخش دلخواه، لزوماً کل مصرف Flash را نشان نمیدهد.

پیش از main چه اتفاقی میافتد؟
پس از ریست، پردازنده از جدول بردار انتخابشده، مقدار اولیهٔ اشارهگر پشته و نشانی اجرای راهانداز را میخواند. فایل اسمبلی راهانداز پروژهٔ حاضر startup_stm32f303xc.s است؛ نام فایل تولیدشده بهتنهایی ظرفیت حافظهٔ تراشه را تعیین نمیکند. انتخاب تراشه و Linker Script این پروژه مخصوص CB است.
در راهانداز واقعی این پروژه، ابتدا SystemInit فراخوانی میشود؛ سپس تصویر اولیهٔ .data از Flash به RAM کپی و محدودهٔ .bss صفر میشود. پس از راهاندازی کتابخانهٔ C با __libc_init_array، نوبت main است. بنابراین SystemInit پیش از آمادهشدن متغیرهای معمول C اجرا میشود و نباید آنها را از قبل مقداردهیشده فرض کند.

این راهانداز، بخش سفارشی CCM را کپی یا صفر نمیکند. وجود یک بخش در فایل پیونددهنده به معنی اجرای خودکار مقداردهی آن نیست. در این درس، مقداردهی CCM در کد کاربر و پیش از نخستین استفاده انجام میشود؛ این کار را بهاشتباه وظیفهٔ انجامشدهٔ راهانداز معرفی نمیکنیم.
خواندن Linker Script و فایل نقشهٔ ساخت
Linker Script دستور چیدمان برنامه است. فرمان MEMORY نام، نشانی شروع و اندازهٔ ناحیهها را تعریف میکند. فرمان SECTIONS میگوید بخشهای فایلهای ترجمهشده در کدام ناحیه قرار بگیرند. نامهایی مانند RAM و CCMRAM نامهای قراردادی اسکریپت هستند؛ مشخصات واقعی از ORIGIN و LENGTH میآید.
علامت >CCMRAM AT>FLASH یعنی نشانی اجرای بخش در CCM است، ولی تصویر بارگذاری آن در Flash قرار میگیرد. تابع پیونددهندهٔ LOADADDR نشانی تصویر اولیه را در اختیار کد قرار میدهد. نمادهایی مانند _siccmram و _sccmram نشانیاند، نه متغیرهای تنظیمات.

فایل .map نتیجهٔ تصمیمهای پیونددهنده است. نام بافر را جستوجو کنید؛ بخش، نشانی و اندازهٔ آن را بخوانید و با محدودهٔ MEMORY تطبیق دهید. فایل ELF اطلاعات بخشها و نمادها را نیز دارد و اسکریپت همراه، همین شواهد را استخراج میکند. گزارش ساخت مشخص میکند فایل کجا پیوند خورده است؛ مشاهدهٔ مقدار آن هنگام اجرای واقعی به دیباگر نیاز دارد.
بودجههای _Min_Heap_Size و _Min_Stack_Size فقط در جا شدن حداقل فضای درخواستی به پیونددهنده کمک میکنند. این اعداد، بیشینهٔ مصرف پشته را اندازه نمیگیرند و نگهبان سرریز در زمان اجرا نیستند.
قرار دادن بافر در CCM و مقداردهی درست آن
در پروژه، بافر main_buffer در RAM اصلی است. دو بافر 64 عضوی نیز در CCM قرار داده شدهاند: ccm_initialized با تصویر اولیه در Flash، و ccm_zeroed در بخش سفارشی .ccm_bss با صفت NOLOAD. هرکدام 256 بایت هستند.
برای بافر اول، کد کاربر تصویر اولیه را از نشانی بارگذاری به نشانی اجرا کپی میکند. برای بافر دوم، کد کاربر محدوده را صریحاً صفر میکند. این عملیات در main.c، USER CODE BEGIN 1 و پیش از HAL_Init و نخستین استفاده انجام میشود. تعریفها و تابع کمکی نیز در بلوکهای مشخصشدهٔ USER CODE قرار دارند. قسمتهای واقعی پیونددهنده و راهانداز را در فایلهای همراه بخوانید؛ آنها کد تولیدشده از حافظه نیستند.

بخش تولیدشدهٔ .ccmram از قبل وجود دارد؛ بخش .ccm_bss زیر، تغییر دستی لینکِر است. فایل Startup تولیدشده تغییر نکرده است.
.ccm_bss (NOLOAD) :
{
. = ALIGN(4);
_sccm_bss = .;
*(.ccm_bss .ccm_bss.*)
. = ALIGN(4);
_eccm_bss = .;
} >CCMRAM
Reset_Handler:
ldr sp, =_estack /* Atollic update: set stack pointer */
/* Call the clock system initialization function.*/
bl SystemInit
/* Copy the data segment initializers from flash to SRAM */
ldr r0, =_sdata
ldr r1, =_edata
ldr r2, =_sidata
movs r3, #0
b LoopCopyDataInit
قطعههای دقیق پروژهٔ کامپایلشده؛ محل تعریفها USER CODE PV، تابع در USER CODE 4 و فراخوانی نخست در USER CODE 1 فایل Core/Src/main.c است. اینها برنامهٔ مستقل نیستند.
__attribute__((section(".ccmram"), aligned(4)))
volatile uint32_t ccm_initialized[64] = {0x11223344U, 0x55667788U};
__attribute__((section(".ccm_bss"), aligned(4)))
volatile uint32_t ccm_zeroed[64];
static void CCM_Initialize(void)
{
uintptr_t source = (uintptr_t)&_siccmram;
uintptr_t target = (uintptr_t)&_sccmram;
const uintptr_t end = (uintptr_t)&_eccmram;
while (target < end)
{
*(volatile uint8_t *)target++ = *(const uint8_t *)source++;
}
target = (uintptr_t)&_sccm_bss;
while (target < (uintptr_t)&_eccm_bss)
{
*(volatile uint8_t *)target++ = 0U;
}
__DSB();
}
CCM این خانواده امکان اجرای کد را هم دارد، ولی مثال حاضر فقط جابهجایی داده را آموزش میدهد. انتقال یک تابع، علاوه بر پیوند درست، به کپی کد و رعایت شرایط اجرای آن نیاز دارد و صرف تغییر یک صفت کافی نیست. برای بافر UART یا ADC که DMA به آن دسترسی دارد، RAM اصلی را انتخاب کنید؛ پروژهٔ این درس آزمایش انتقال نامعتبر DMA اجرا نمیکند.
آزمایش؛ ابتدا فایل نقشه، سپس دیباگر
ابتدا بدون اتصال برد، فایل نقشه و گزارش نمادهای همراه را بررسی کنید. در خروجی واقعی پیوند، ccm_initialized در نشانی 0x10000000 و ccm_zeroed در نشانی 0x10000100 قرار گرفتهاند. نشانی دوم 256 بایت بعد از اولی است. اینها شواهد ساخت همین نسخهاند؛ با تغییر برنامه ممکن است نشانی نمادهای دیگر تغییر کند.

برای مرحلهٔ سختافزاری، فایل پروژه را طبق README بسازید، با ST-LINK بارگذاری کنید و پیش از مقداردهی CCM و پس از آن توقف بگذارید. مقادیر پس از مقداردهی را با مقدارهای تعریفشدهٔ پروژه مقایسه کنید؛ محتوای RAM پیش از مقداردهی مبنای اتکا نیست. در کاربرگ، نشانی، مقدار پس از کپی، مقدار پس از صفرکردن و وضعیت اجرای برنامه را ثبت کنید. این مشاهدهٔ فیزیکی هنوز انجام نشده است.
در نسخهٔ فعلی، گزارش ناحیهها 1856 بایت از RAM اصلی و 512 بایت از CCM را نشان میدهد؛ عدد Flash برابر 8732 بایت است. این گزارش شامل بودجههای پیوند و همترازی است و میزان مصرف لحظهای پشته را نشان نمیدهد. برای مدل و استخراج دوبارهٔ اعداد، اسکریپت همراه و گزارش همان نسخه را بخوانید.
تابع Memory_Check مقدارهای اولیه را بررسی و نتیجه را در initialization_ok ثبت میکند؛ در صورت نتیجهٔ غیرصفر، LED روی PB6 روشن میشود. observed_sum جمع جدول ثابت است و در اجرای صحیح باید 100 باشد. اینها نتیجهٔ پیشبینیشدهٔ مسیر کدند؛ مقدار واقعی را پس از اجرای برد ثبت کنید.
عیبیابی بر اساس مرحلهٔ وقوع مشکل
| نشانه | بررسی و اقدام |
|---|---|
| خطای overflow هنگام پیوند | بخش و ناحیهٔ گزارششده را در map پیدا کنید؛ اندازهٔ بافرها، بودجهٔ پشته و فضای CCM را جداگانه حساب کنید. ساخت فایل اجرایی نهایی موفق نشده است. |
| بافر در RAM اصلی مانده است | صفت بخش در تعریف متغیر و قاعدهٔ متناظر SECTIONS را با فایلهای همین پروژه مقایسه کنید؛ صرف نام متغیر محل آن را تغییر نمیدهد. |
| محل CCM درست است، مقدار اولیه درست نیست | ترتیب اجرای مقداردهی USER CODE 1، نمادهای ابتدا و انتها و نشانی بارگذاری را بررسی کنید. پیوند درست، شاهد کپیشدن در زمان اجرا نیست. |
| نگرانی دربارهٔ بافر DMA | نشانی بافر انتقال را در RAM اصلی نگه دارید. در پروژهٔ حاضر DMA راهاندازی نشده و پرچم انتقالی برای نمایش وجود ندارد. |
در بررسی روی تراشهٔ واقعی، بازنگری سیلیکون را نیز ثبت و محدودیتهای ES0204 را با آن تطبیق دهید؛ از جمله محدودیت دسترسی SRAM در Rev Z و رفتار حفاظت نوشتن CCM. این پروژه حفاظت CCM را فعال نمیکند. افزایش عدد حداقل پشته، جای تحلیل مصرف واقعی و خطایابی را نمیگیرد.
پرسشهای خودآزمایی
1. آیا 40 KiB حافظهٔ کاری تراشه یک ناحیهٔ پیوسته است؟
خیر؛ 32 KiB حافظهٔ اصلی و 8 KiB حافظهٔ CCM در دو بازهٔ جدا قرار دارند.
2. چرا .data هم Flash و هم RAM مصرف میکند؟
مقدار اولیه در تصویر بارگذاری Flash میماند و در آغاز اجرا به محل قابلتغییر RAM کپی میشود.
3. آیا راهانداز تولیدشده CCM را مقداردهی میکند؟
در پروژهٔ حاضر خیر؛ کد کاربر تصویر بخش مقداردهیشده را کپی و بخش صفرشوندهٔ سفارشی را صفر میکند.
4. آیا فایل map مقدار زمان اجرای متغیر را ثابت میکند؟
خیر؛ محل پیوند را نشان میدهد. بررسی مقدار هنگام اجرا به اجرای برنامه و مشاهده با دیباگر نیاز دارد.
5. چرا بافر DMA را به CCM نمیبریم؟
در STM32F303CB واحد DMA به CCM دسترسی ندارد؛ بافر انتقال باید در حافظهٔ قابلدسترسی آن قرار گیرد.
6. آیا حداقل پشته در اسکریپت از سرریز جلوگیری میکند؟
خیر؛ این بودجه فقط برای بررسی جا شدن حداقل فضای درخواستی هنگام پیوند است.
تمرینها
1. پایه؛ دنبالکردن یک نماد
محل و اندازهٔ main_buffer و دو بافر CCM را در map و گزارش ELF پیدا کنید.
معیار بررسی: هر نماد با ناحیهٔ تعریفشدهٔ خودش سازگار باشد؛ گزارش ساخت را با مشاهدهٔ روی برد اشتباه نگیرید.
2. متوسط؛ تغییر اندازهٔ بافر
اندازهٔ یکی از بافرها را افزایش دهید، دوباره بسازید و تغییر مصرف ناحیهها را بررسی کنید.
معیار بررسی: تغییر اندازه و همترازی را توضیح دهید؛ ظرفیت محدود CCM و کپی یا صفرکردن کامل بخش حفظ شود.
3. پیشرفته؛ تحلیل مسیر مقداردهی
مسیر ریست تا main و سپس مقداردهی CCM را از فایلهای راهانداز و main رسم کنید.
معیار بررسی: کپی .data، صفرکردن .bss، راهاندازی کتابخانه و مقداردهی CCM به ترتیب واقعی تفکیک شده باشند.
منابع و نسخهٔ ساخت
| مرجع | کاربرد |
|---|---|
| DS9118 Rev 14 | ظرفیت دقیق تراشه، پایهها و مشخصات حافظه |
| RM0316 Rev 10 | نقشهٔ حافظه و سازوکار واحدهای خانواده؛ بخشهای مرتبط بررسی شدهاند |
| ES0204 Rev 11 | محدودیتهای وابسته به بازنگری سیلیکون |
| GebraBit GB100ST V2.0 | مدار برد، پایهها و انتخاب بوت |
| GNU ld: MEMORY | تعریف ناحیهها و محدودیت فضای پیوند |
| GNU ld: Output Section LMA | تفاوت نشانی بارگذاری و اجرا و مسئولیت مقداردهی |
| فایلهای راهانداز و پیونددهندهٔ پروژه | نسخهٔ بسته؛ فایل تولیدشده و تغییرهای دقیق در دانلود همین درس آمدهاند |
ساخت واقعی با CubeMX 6.14.0 و DB 6.0.140، بستهٔ CubeF3 V1.11.6 و GCC 13.2.1 در مسیر CMake انجام شده است. گزینههای Debug شامل -O0 -g3 -Wall -Werror هستند. شناسهٔ بسته 663d2ba و HAL 953955a است؛ شناسههای کامل و گزارشها در فایلهای همراه آمدهاند. رابط گرافیکی IDE و اجرای فیزیکی برد آزمایش نشدهاند.
دانلود فایلهای همراه درس 31
بسته شامل پروژهٔ واقعی با IOC و سورس، خروجیهای ELF/MAP و گزارش ساخت، مدل محاسباتی، کاربرگ آزمایش، مجوزها و 6 تصویر همراه اسکریپت قابل ویرایش است. ابتدا ZIP را استخراج و README را بخوانید. اجرای روی برد و اندازهگیریها هنوز انجام نشدهاند.