Cortex-M7

MPU در STM32H7 چیست؟ آموزش Memory Protection Unit در Cortex-M7

«چرا برنامه‌ای که کامپایل می‌شود، روی H7 ممکن است به خاطر یک دسترسی اشتباه به حافظه، کل سیستم را به‌هم بریزد؟

MPU در Stm32H7 یکی از مهمترین بخشهای معماری Cortex-m7 میباشد که استفاده صحیح از آن مستقیم بر پرفورمنس میکرو تاثیر میگذارد.

اگر با STM32های معمولی کار کرده باشید، احتمالاً حافظه را خیلی ساده می‌بینید:

Flash برای کد
RAM برای داده
Peripheral برای رجیسترها

اما وقتی وارد دنیای STM32H7 و Cortex-M7 می‌شوید، داستان خیلی جدی‌تر می‌شود.

در H7 فقط این مهم نیست که یک آدرس حافظه وجود دارد یا نه؛ بلکه باید مشخص کنید:

  • چه کسی اجازه خواندن آن را دارد؟
  • چه کسی اجازه نوشتن دارد؟
  • آیا CPU اجازه اجرای کد از آن ناحیه را دارد؟
  • این حافظه Cache باشد یا نباشد؟
  • Shareable باشد یا نه؟
  • دسترسی اشتباه چه اتفاقی ایجاد کند؟

اینجاست که MPU یا Memory Protection Unit وارد بازی می‌شود.

در Cortex-M7، واحد MPU می‌تواند نواحی مختلف Memory Map را با قوانین مستقل مدیریت کند و STM32H7 تا 16 ناحیه مستقل را پشتیبانی می‌کند.

MPU دقیقاً چه کار می‌کند؟

فرض کنید RAM شما از این قسمت‌ها تشکیل شده:

STM32H7 RAM

├── 0x24000000 ── Application Data

├── 0x24010000 ── Network Buffer

├── 0x24020000 ── DMA Buffer

├── 0x24030000 ── Critical Data

└──….

بدون MPU، برنامه می‌تواند در اثر یک Bug چیزی شبیه این انجام دهد:


uint32_t *ptr = (uint32_t *)0x24030000;

*ptr = 0xDEADBEEF;

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

«صبر کن! این حافظه نباید نوشته شود.»


و مشکل در همین نقطه میتواند آغاز شود اما می‌توانیم با MPU بگوییم:

0x24030000

┌──────────────────────┐
│ Critical Data │
│ READ ONLY │
│ EXECUTE NEVER │
└──────────────────────┘

حالا اگر برنامه ما بخواهد در آنجا Write انجام دهد، Cortex-M7 می‌تواند MemManage Fault ایجاد کند.

یعنی MPU عملاً یک لایه دفاعی سخت‌افزاری بین CPU و حافظه ایجاد می‌کند.

اما قسمت جذاب MPU فقط Protection نیست!

یکی از اشتباهات رایج این است که MPU را فقط یک ابزار امنیتی بدانیم.

در STM32H7،  واحد MPU مستقیماً با رفتار Cache و Memory System هم ارتباط دارد.

Cortex-M7 دارای Cache است و MPU می‌تواند برای هر Region ویژگی‌هایی مانند:

  • Cacheable
  • Bufferable
  • Shareable
  • Read/Write Permission
  • Execute/Non-Execute

و اینجاست که MPU از یک قابلیت جانبی تبدیل می‌شود به یکی از بخش‌های مهم معماری STM32H7.

نوشته های مشابه

یک مثال واقعی: DMA + Cache + MPU

فرض کنید STM32H7 شما از ADC داده می‌گیرد:

ADC

│ DMA

RAM Buffer


Cortex-M7


DSP / Filtering

</p>
uint16_t adc_buffer[4096];

ADC با DMA اطلاعات را داخل RAM می‌نویسد.

حالا Cortex-M7 می‌خواهد این داده‌ها را بخواند.

اینجا یک سؤال مهم وجود دارد:

CPU واقعاً آخرین داده‌ای که DMA نوشته را می‌بیند؟


اگر Cache درست مدیریت نشده باشد، لزوماً نه.

ممکن است CPU قبلاً نسخه‌ای از داده را در D-Cache داشته باشد، در حالی که DMA مستقیماً RAM را تغییر داده است.

در نتیجه:

DMA → RAM = جدید

CPU → Cache = قدیمی

و برنامه شما ممکن است داده قدیمی را پردازش کند.

این همان جایی است که در پروژه‌های واقعی STM32H7، بحث‌های:

MPU + Cache + DMA

دیگر یک موضوع تئوری نیستند.

راه‌حل چیست؟

یکی از روش‌ها این است که Buffer مربوط به DMA را در یک MPU Region مناسب قرار دهیم.

Region 0
────────────────────
Flash
Executable
Cacheable
Read Only
────────────────────

Region 1
────────────────────
Normal RAM
Read / Write
Cacheable
────────────────────

Region 2
────────────────────
DMA Buffer
Read / Write
Non-Cacheable
────────────────────

Region 3
────────────────────
Peripheral
Device Memory
Execute Never
────────────────────

به این ترتیب می‌توانیم برای هر بخش حافظه رفتار متفاوتی تعریف کنیم.

ST نیز در مستندات MPU مثال‌هایی برای تعریف Flash، SRAM و Peripheral با Attributeهای متفاوت ارائه کرده است.

XN؛ یکی از قابلیت‌های خیلی مهم MPU

یکی از بیت‌های مهم MPU، گزینه XN یا Execute Never است.

یعنی:

CPU اجازه ندارد از این ناحیه به عنوان کد اجرا کند.

فرض کنید RAM شما این است:

0x24000000

├── Variables
├── Buffers
├── Network Data
└── User Data

طبیعتاً انتظار نداریم CPU از داخل این RAM کد اجرا کند.

پس می‌توانیم بگوییم:

RAM

├── Read ✓
├── Write ✓
└── Execute ✗

اگر CPU تلاش کند instruction را از چنین ناحیه‌ای Fetch کند، MPU می‌تواند Fault ایجاد کند.

در رجیستر RASR، بیت XN دقیقاً برای همین موضوع استفاده می‌شود.

این موضوع در طراحی Firmwareهای مقاوم‌تر و همچنین کاهش برخی مسیرهای اجرای کد ناخواسته اهمیت دارد؛ ST نیز استفاده از MPU برای تعریف SRAM به‌صورت Execute Never را به‌عنوان یک کاربرد امنیتی مطرح کرده است.

حالا بیایید یک MPU واقعی طراحی کنیم:

فرض کنید یک سیستم صنعتی با STM32H743 داریم:

stm32h7_memory_map

حالا می‌توانیم حافظه را منطقی تقسیم کنیم:

stm32h7_MPU

این یعنی MPU فقط یک «تنظیم چند رجیستر» نیست؛ بلکه بخشی از معماری نرم‌افزار سیستم است.

;MPU و FreeRTOS ترکیب بسیار مهم

فرض کنید FreeRTOS رو در پروژمون داریم:

FreeRTOS

├── Sensor Task
├── Network Task
├── Control Task
└── Logger Task

و فرض کنید Sensor Task به یک Buffer دسترسی دارد:


sensor_buffer[]

و Control Task نباید بتواند آن را تغییر دهد.

در یک طراحی ساده ممکن است فقط با قرارداد نرم‌افزاری بگوییم:

«Control Task لطفاً این Buffer را تغییر نده!»

اما مشکل اینجاست:

Software Contract ≠ Hardware Protection

با MPU می‌توان محدودیت‌های دسترسی حافظه را سخت‌افزاری اعمال کرد. این یکی از کاربردهای کلاسیک MPU در سیستم‌های دارای RTOS است.

حالا برسیم به خود رجیسترهای MPU

اگر واقعاً بخواهید MPU را در سطح پایین بفهمید، باید RBAR و RASR را بشناسید.

در Cortex-M7 رجیستر RASR شامل موارد مهمی مثل:

RASR

├── XN
├── AP
├── TEX
├── S
├── C
├── B
├── SRD
└── SIZE

مثلاً: XN

Execute Never

آیا CPU می‌تواند از این Region کد اجرا کند؟

AP

Access Permission

چه کسی اجازه Read/Write دارد؟

برای مثال می‌توان Region را به حالت‌هایی مانند:

No Access
Read Only
Read/Write
Privileged Only

تنظیم کرد.

TEX / C / B / S

اینجا وارد بحث جدی‌تر Cortex-M7 می‌شویم.

این بیت‌ها برای تعریف Memory Type و Memory Attributes استفاده می‌شوند و در سیستم دارای Cache اهمیت زیادی دارند.

یعنی وقتی MPU را درست یاد می‌گیرید، در واقع دارید بخشی از معماری داخلی Cortex-M7 را هم یاد می‌گیرید.

یک مثال ساده با HAL

مثلاً می‌توانیم یک Region را برای یک Buffer تعریف کنیم:

MPU_Region_InitTypeDef MPU_InitStruct = {0};

HAL_MPU_Disable();

MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER2;
MPU_InitStruct.BaseAddress = 0x24020000;
MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;

MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;

MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1;
MPU_InitStruct.SubRegionDisable = 0x00;

HAL_MPU_ConfigRegion(&MPU_InitStruct);

HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);

نکته مهم اینجاست که این کد را نباید کورکورانه برای هر Buffer کپی کرد.

مقادیر TEX/C/B/S، اندازه Region، محل قرارگیری Buffer، Cache و نحوه استفاده DMA باید بر اساس معماری واقعی سیستم انتخاب شوند.

حتی اندازه Region نیز محدودیت دارد؛ در Cortex-M7 اندازه MPU Region به توان‌های دو تنظیم می‌شود و حداقل Region پشتیبانی‌شده 32 بایت است.

چرا MPU در STM32H7 از STM32های ساده‌تر مهم‌تر می‌شود؟

چون H7 دیگر صرفاً یک MCU با CPU سریع‌تر نیست.

وقتی با امکاناتی مثل:

Cortex-M7
+
I/D Cache
+
DMA
+
Ethernet
+
USB
+
FreeRTOS
+
External Memory
+
DSP

کار می‌کنید، Memory System خودش تبدیل به یک موضوع طراحی می‌شود.

و اگر Memory را درست مدیریت نکنید، ممکن است با Bugهایی مواجه شوید که بسیار عجیب به نظر می‌رسند:

“DMA کار می‌کند ولی داده اشتباه است!”

“گاهی سیستم هنگ می‌کند!”

“بعد از فعال کردن Cache مشکل ایجاد شد!”

“Ethernet گاهی Packet خراب می‌دهد!”

“LCD با DMA بعضی فریم‌ها را خراب می‌کند!”

“بعد از اجرای یک Task، Task دیگر رفتار عجیبی دارد!”

و یکی از جاهایی که باید بررسی کنید:

MPU
Cache
DMA
Memory Region
Memory Attribute

یک نکته مهم: MPU جادو نمی‌کند!

MPU قرار نیست Bugهای نرم‌افزار را خودکار حل کند.

بلکه به شما اجازه می‌دهد قوانین مشخصی برای Memory تعریف کنید.

مثلاً:

“این قسمت فقط خواندنی است.”

“این قسمت نباید Execute شود.”

“این Buffer مربوط به DMA است.”

“این Memory باید Non-Cacheable باشد.”

“این Peripheral باید Device Memory باشد.”

“این Task نباید به این ناحیه دسترسی داشته باشد.”

و این دقیقاً تفاوت بین:

برنامه‌ای که فقط کار می‌کند

و

Firmwareی که معماری شده است

می‌باشد.

اگر فقط HAL_MPU_ConfigRegion را بلد باشید، هنوز MPU را یاد نگرفته‌اید!

چون در پروژه واقعی، مسئله این نیست که:

HAL_MPU_ConfigRegion(...)

را بلد باشید.

مسئله این است که بدانید:

چه Regionهایی بسازیم؟

چرا این Region Cacheable باشد؟

چرا آن یکی Non-Cacheable باشد؟

چرا XN را فعال کنیم؟

چرا DMA با Cache مشکل پیدا می‌کند؟

چطور Memory Layout را طراحی کنیم؟

چطور MPU را کنار FreeRTOS استفاده کنیم؟

و چه زمانی باید به جای تغییر MPU، Cache Maintenance انجام دهیم؟

اینجاست که MPU از یک کد آماده در CubeMX تبدیل می‌شود به یک ابزار واقعی در طراحی سیستم‌های مبتنی بر STM32H7.

و این دقیقاً یکی از چیزهایی است که در STM32H7 باید عملی یاد بگیرید.

در یک پروژه واقعی، ما می‌توانیم چیزی شبیه این معماری داشته باشیم:

معماری نرم افزار در stm32h7

وقتی این معماری را بفهمید، موضوعاتی مثل Cache، DMA، Ethernet، SDRAM، LTDC، FreeRTOS و Performance Optimization دیگر جزیره‌های جدا از هم نیستند؛ به یک سیستم واحد تبدیل می‌شوند.

اگر با STM32H7 کار می‌کنید، MPU را دست‌کم نگیرید

MPU شاید در نگاه اول یکی از گزینه‌های CubeMX به نظر برسد:

اما پشت همین گزینه، مفاهیمی قرار دارد که مستقیماً به معماری Cortex-M7، Cache، DMA، امنیت و RTOS مربوط می‌شوند.

STM32H7 برای پروژه‌های ساده ساخته نشده است؛ وقتی قرار است از قدرت واقعی آن استفاده کنید، باید بدانید CPU چگونه Memory را می‌بیند و چگونه به آن دسترسی پیدا می‌کند.

و MPU یکی از کلیدی‌ترین ابزارهای این کار است.

 

دیدگاهتان را بنویسید

دکمه بازگشت به بالا