با ظهور STCube، ST تصمیم گرفت که لایه انتزاعی سخت افزاری (HAL) Hardware Abstraction Layer را برای میکروکنترلرهای STM32 خود به طور کامل اصلاح کند. قبل از انتشار STCube HAL، کتابخانه رسمی برای توسعه برنامههای STM32 به مدت طولانی، کتابخانه Standard Peripheral Library بود که میتوانید نمونههای زیادی را در وب با استفاده از این کتابخانه پیدا کنید. STCube HAL یک پیشرفت عالی نسبت به کتابخانه قدیمی Standard Peripheral است. در واقع، کتابخانه قدیمی Standard Peripheral به عنوان اولین کتابخانه توسعه یافته توسط ST، همه بخشهای آن بین خانوادههای مختلف STM32 سازگار نبود و مشکلات زیادی را در نسخههای اولیه آن داشت. این موضوع باعث پیدایش کتابخانههای جایگزین مختلف، برای کتابخانه Standard Peripheral شد.
بنابراین، ST کتابخانه HAL را مجددا طراحی کرد و اگر هنوز هم دارای اشکالات کوچک باشد، بخش پشتیبانی ST آن را رفع خواهد کرد. علاوه بر این، HAL انتقال کد بین خانوادههای STM32 (F0، F1 و غیره) را بسیار ساده میکند و تلاش برای تطبیق برنامه شما با یک MCU متفاوت را کاهش میدهد. ما قبلاً از بسیاری از توابع این ماژول در نمونههای اولیه این کتاب استفاده کردهایم، اما اکنون زمان مناسبی است که همه امکانات ارائه شده توسط یک ابزار جانبی بسیار ساده و پرکاربرد را درک کنیم. با این حال، قبل از شروع توصیف ویژگیهای HAL، بهتر است نگاهی گذرا به نحوه نگاشت تجهیزات جانبی STM32 به آدرسهای منطقی و نحوه نمایش آنها در کتابخانه HAL داشته باشیم.
نقشه واحدهای جانبی و Handlers کنترل کننده های HAL
همانطور که در شکل زیر نشان داده شده است، هر واحد جانبی STM32 توسط چندین گذرگاه به هسته MCU متصل میشود:
هم هسته Cortex-M و هم کنترل کننده DMA1 از طریق یک سری گذرگاه با سایر واحدهای جانبی MCU تعامل دارند. ذکر این نکته مهم است که حافظههای فلش و SRAM نیز اجزای خارج از هسته MCU هستند و بنابراین باید از طریق اتصال باس با یکدیگر تعامل داشته باشند.
BusMatrix دسترسی بین هسته Cortex-M و کنترلر DMA1 را مدیریت میکند. دسترسی به گذزگاه از یک الگوریتم Round Robin استفاده میکند. BusMatrix از دو Master یعنی CPU، DMA و چهار Slave رابط فلش، SRAM، AHB1 ( با پل AHB به گذرگاه محیطی پیشرفته APB) و AHB2 تشکیل شده است. BusMatrix همچنین اجازه می دهد تا به طور خودکار چندین واحد جانبی را به هم متصل کنید.
پل AHB به APB اتصالات سنکرون کامل بین AHB و گذرگاه APB را فراهم میکند، جایی که اکثر تجهیزات جانبی متصل هستند.
همانطور که در آینده خواهیم دید، هر یک از این گذرگاه ها به منابع کلاک متفاوتی متصل می شوند که حداکثر سرعت برای واحدهای جانبی متصل به آن گذرگاه را تعیین میکنند.
واحدهای جانبی به منطقه مشخصی از آدرس 4 گیگابایتی که از x4000 0000 شروع و تا x5FFF FFFF ادامه دارد، نقشهنگاری میشوند. این منطقه به چندین منطقه فرعی تقسیم شده که هر کدام به یک واحد جانبی خاص مربوط میشود:
به عنوان مثال، در یک میکروکنترلر STM32F030، گذرگاه AHB2 در منطقهی حافظه از x4800 0000 تا x4800 17FF به اندازه 6144 بایت طراحی میشود. این منطقه به چندین منطقه فرعی تقسیم شده که هر کدام مربوط به یک واحد جانبی خاص است. واحد GPIOA (که تمام پینهای متصل به PORT-A را مدیریت میکند) از x4800 0000 تا x4800 03FF طراحی شده است که 1KB از حافظه جانبی را اشغال میکند. نحوه سازماندهی این فضا از حافظه به آن واحد جانبی بستگی دارد. جدول زیر طراحی حافظه واحد جانبی GPIO را نشان میدهد:
یک واحد جانبی را زمانی میتوان کنترل کرد که هر رجیستر مربوط به این مناطق را بتوان خواند و یا ویرایش کرد. به عنوان مثال، در واحد جانبی GPIOA، برای فعال کردن پین PA5 به عنوان پایه خروجی، باید رجیستر MODER را به گونهای مقداردهی کنیم که بیتهای [11:10] برابر با 01 شوند (مطابق با General purpose output mode)، همانطور که در شکل بالا نشان داده شده است، برای pull up پین، باید بیت [5] مربوطه را در داخل Output Data Register (ODR) یک کنیم، که در واقع محل حافظه GPIOA + 0x14، یعنی x4800 0000 + 0x14 را تغییر میدهیم.
مثال زیر نحوه استفاده از اشارهگرها را برای دسترسی به حافظه طراحی شده واحد جانبی GPIOA در یک میکروکنترلر STM32F030 نشان میدهد:
int main(void)
{ volatile uint32_t *GPIOA_MODER = 0x0, *GPIOA_ODR = 0x0;
GPIOA_MODER = (uint32_t*)0x48000000; // Address of the GPIOA->MODER register
GPIOA_ODR = (uint32_t*)(0x48000000 + 0x14); // Address of the GPIOA->ODR register
// This ensure that the peripheral is enabled and connected to the AHB1 bus
__HAL_RCC_GPIOA_CLK_ENABLE(); *GPIOA_MODER = *GPIOA_MODER | 0x400; // Sets MODER[11:10] = 0x1
*GPIOA_ODR = *GPIOA_ODR | 0x20;
while(1); // Sets ODR[5] = 0x1, that is pulls PA5 high
}
هر خانواده از میکروکنترلرهای STM32 (F0، F1، و غیره) مجموعه ای از امکانات جانبی را ارائه میدهند که به آدرسهای خاصی نگاشت میشوند. علاوه بر این، نحوه اجرای هر یک از این بخشهای جانبی بین هر سری STM32 متفاوت است.
یکی از نقشهای HAL، تسهیل در دسترسی و کنترل واحدهای جانبی است. این کار با تعریف چندین هندلر handlers برای هر واحد جانبی انجام میشود. یک هندلر handlers چیزی بیشتر از یک ساختار C نیست که از ارجاعات آن برای اشاره به آدرس واقعی واحد جانبی استفاده میشود:
در بخش قبل پین PA5 را با استفاده از کد زیر پیکربندی کردیم:
/*Configure GPIO pin : PA5 */
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
در اینجا، متغیر GPIOA یک اشارهگر از نوع GPIO_TypeDef بوده که به این صورت تعریف شده است:
typedef struct {
volatile uint32_t MODER;
volatile uint32_t OTYPER;
volatile uint32_t OSPEEDR;
volatile uint32_t PUPDR;
volatile uint32_t IDR;
volatile uint32_t ODR;
volatile uint32_t BSRR;
volatile uint32_t LCKR;
volatile uint32_t AFR[2];
volatile uint32_t BRR;
} GPIO_TypeDef;
نشانگر GPIOA به گونهای تعریف شده است که به آدرس x4800 0000 اشاره میکند:
GPIO_TypeDef *GPIOA = 0x48000000;
GPIOA->MODER |= 0x400;
GPIOA->ODR |= 0x20;
تنظیمات GPIO
هر میکروکنترلر STM32 دارای تعداد متغیری I/O قابل برنامه ریزی عمومی است که تعداد دقیق آن بستگی به موارد زیر دارد:
- نوع پکیج انتخابی (LQFP48، BGA176، و غیره).
- خانواده میکروکنترلرها (F0، F1 و …).
- استفاده از کریستالهای خارجی برای HSE و LSE.
GPIOها راهی هستند که یک MCU با دنیای بیرونی ارتباط برقرار میکند. هر برد از تعداد متغیری I/O برای راه اندازی قطعات جانبی خارجی (مانند LED) یا برای تبادل داده از طریق انواع پروتکلهای ارتباطی جانبی (UART، USB، SPI و غیره) استفاده میکند. هر بار که نیاز به تنظیم یک واحد جانبی که از پینهای MCU استفاده میکند، داریم باید GPIO مربوط به آن را با استفاده از ماژول HAL_GPIO پیکربندی کنیم.
همانطور که قبلا دیده شد، HAL به گونه ای طراحی شده است که از نقشه حافظه یک واحد جانبی مشخص الگو برداری میکند. همچنین یک روش عمومی و کاربرپسند را برای پیکربندی واحدهای جانبی ارائه کرده که برنامهنویس مجبور به تنظیم جزئیات در رجیسترهای آن نشود.
برای تنظیم GPIO از تابع HAL_GPIO_Init(GPIO_TypeDef *GPIOx، GPIO_InitTypeDef *GPIO_- Init) استفاده میکنیم. GPIO_InitTypeDef یک ساختار C است که برای پیکربندی GPIO استفاده و به صورت زیر تعریف میشود:
typedef struct {
uint32_t Pin;
uint32_t Mode;
uint32_t Pull;
uint32_t Speed;
uint32_t Alternate;
} GPIO_InitTypeDef;
نقش هر فیلد از این ساختار است:
Pin: عددی است که از 0 شروع میشود، و نمایانگر پین هاییست که میخواهیم پیکربندی کنیم. به عنوان مثال، برای پین PA5 مقدار GPIO_PIN_5 را در نظر میگیرند. میتوانیم از همان نمونه GPIO_InitTypeDef برای پیکربندی چندین پین به طور همزمان استفاده کنیم، و یک OR را به صورت بیتی انجام دهیم (به عنوان مثال، GPIO_PIN_1 | GPIO_PIN_5 | GPIO_PIN_6).
- Mode: حالت عملکرد پین است و میتواند یکی از مقادیر جدول زیر را در برگیرد.
- Pull: طبق جدول زیر، فعالسازی Pull-up یا Pull-Down را برای پینهای انتخاب شده مشخص میکند.
- Speed: سرعت پین را مشخص میکند.
- Alternate: مشخص میکند که کدام واحد جانبی میتواند به پین اختصاص داده شود.
GPIO Mode
میکروکنترلرهای STM32 یک واحد انعطاف پذیر از مدیریت GPIO را ارائه میدهند. شکل زیر ساختار سخت افزاری یک واحد I/O در میکروکنترلر STM32F030 را نشان میدهد.
بسته به فیلد GPIO GPIO_InitTypeDef.Mode، میکروکنترلر نحوه کار سخت افزاری یک I/O را تغییر میدهد. اجازه دهید نگاهی به حالتهای اصلی بیندازیم.
هنگامی که I/O به عنوان GPIO_MODE_INPUT پیکربندی شده است:
- بافر خروجی غیرفعال است. ورودی اشمیت تریگر (Schmitt trigger) فعال است.
- مقاومتهای pull-up و pull-down بسته به مقدار فیلد Pull فعال میشوند.
- دادههای موجود بر روی پین I/O در هر سیکل کلاک AHB در رجیستر داده ورودی نمونه برداری میشود.
- دسترسی خواندن از رجیستر داده ورودی وضعیت I/O فراهم میشود.
هنگامی که پورت I/O به عنوان GPIO_MODE_ANALOG برنامهریزی شده است:
- بافر خروجی غیرفعال است.
- ورودی Schmitt trigger غیرفعال است و zero consumption را برای هر مقدار آنالوگ در پین I/O ارائه میکند.
- مقاومتهای pull-up and pull-down توسط سخت افزار غیرفعال شدهاند.
- خواندن رجیستر داده های ورودی مقدار 0 را دریافت میکند.
هنگامی که پورت I/O به عنوان خروجی برنامهریزی شده است:
- بافر خروجی به صورت زیر فعال میشود:
– اگر حالت GPIO_MODE_OUTPUT_OD باشد: عدد 0 در رجیستر خروجی (ODR) N-MOS را فعال میکند، در حالی که عدد 1 پورت را در Hi-Z قرار میدهد (P-MOS هرگز فعال نمیشود).
– اگر حالت GPIO_MODE_OUTPUT_PP باشد: 0 در ODR N-MOS را فعال میکند؛ در حالی که 1 P-MOS را فعال میکند.
- ورودی Schmitt trigger فعال است.
- مقاومتهای pull-up و pull-down بسته به مقدار فیلد Pull فعال میشوند.
- دادههای موجود بر روی پین I/O در هر سیکل ساعت AHB در رجیستر داده ورودی نمونه برداری میشود.
- خواندن رجیستر دادههای ورودی، وضعیت I/O را دریافت میکند.
- خواندن رجیستر داده خروجی آخرین مقدار نوشته شده را دریافت میکند.
هنگامی که پورت I/O به عنوان عملکرد جایگزین alternate function برنامهریزی میشود:
* بافر خروجی را میتوان در حالت open-drain or push-pull mode پیکربندی کرد.
* بافر خروجی توسط سیگنال هایی که از واحد جانبی میآیند؛ هدایت میشود.
* ورودی اشمیت تریگر فعال است.
* مقاومت های pull-up و pull-down به مقدار pull بستگی دارد.
* دادههای موجود بر روی پین I/O در هر سیکل کلاک AHB در رجیستر داده ورودی نمونه برداری میشود.
* خواندن رجیستر دادههای ورودی، وضعیت I/O را مشخص میکند.
حالت GPIO GPIO_MODE_EVT به مد sleep مربوط میشود. هنگامی که I/O در یکی از این حالت ها پیکربندی شده است ، در صورتی که I/O مربوطه تحریک شود ، CPU بدون ایجاد وقفه مربوطه بیدار میشود (هنگامی که در حالت خواب با دستورالعمل WFE قرار می گیرد). مد GPIO در حالت GPIO_MODE_IT نیز به مدیریت وقفهها مربوط میشوند.
با این حال، به خاطر داشته باشید که این شیوه پیادهسازی میتواند بین خانوادههای STM32، به خصوص برای سریهای کم مصرف، متفاوت باشد. همیشه به دیتاشیت MCU خود مراجعه کنید، زیرا دقیقاً حالتهای I/O و تأثیر آنها بر کارکرد MCU و مصرف توان را توضیح میدهد.
همچنین ذکر این نکته حائز اهمیت است که اگر در برنامه خود به مقاومت pull-up نیاز دارید، نیازی به استفاده از مقاومتهای خارجی و اختصاصی نیست، زیرا GPIO های مربوطه را میتوان با تنظیم GPIO_InitTypeDef.Mode = GPIO_MODE_OUTPUT_PP و GPIO_InitTypeDef.Pull = GPIO_PULLUP پیکربندی کرد. این امر باعث صرفه جویی در فضای PCB شده و BOM را ساده تر میکند.
حالت I/O را میتوان در نهایت با استفاده از ابزار CubeMX پیکربندی کرد، همانطور که در شکل زیر نشان داده شده است. تنظیمات پین را میتوان، با کلیک بر روی دکمه GPIO مشاهده کرد.
GPIO Alternate Function
اکثر GPIOها دارای ” Alternate Function ” هستند، یعنی میتوان آنها را به عنوان پین I/O برای حداقل یک واحد جانبی داخلی استفاده کرد. به خاطر داشته باشید که یک I/O را میتوان تنها به یک واحد جانبی مشخص در یک زمان مرتبط کرد.
برای اطلاع از اینکه کدام واحدهای جانبی را میتوان به یک I/O متصل کرد، میتوانید به دیتاشیت MCU مراجعه کرده یا به سادگی از ابزار CubeMX استفاده کنید. با کلیک بر روی یک پین در Pin View یک منوی pop-up ظاهر میشود. در این منو میتوانیم عملکرد جایگزین مورد نظر را انتخاب کنیم. به عنوان مثال، در شکل بالا میبینید که PA3 میتواند به عنوان USART2_RX استفاده شود (یعنی میتوان از آن به عنوان پین RX برای واحد جانبی USART/UART2 استفاده کرد که برای هر میکروکنترلر STM32 با پکیج LQFP48 امکان پذیر است). CubeMX به طور خودکار کد اولیه مناسب را برای ما ایجاد میکند:
/* Configure GPIO pins : PA2 PA3 */
GPIO_InitStruct.Pin = GPIO_PIN_2|GPIO_PIN_3;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_LOW;
GPIO_InitStruct.Alternate = GPIO_AF1_USART2;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct)
درک سرعت GPIO
یکی از گمراه کنندهترین چیزهای میکروکنترلرهای STM32، پارامتر GPIO_InitTypeDef.Speed است. این فیلد میتواند مقادیر جدول زیر را دربر گیرد و تنها زمانی تأثیر گذار است که GPIO در حالت خروجی پیکربندی شده باشد. متأسفانه ST یک نام مشخص و یکسان برای آن در Cube HALsهای مختلف انتخاب نکرده است.
SPEED در GPIO دقیقاً به چه معناست؟ در اینجا سرعت GPIO به فرکانس سوئیچینگ (یعنی چند بار یک پین از ON به OFF در واحد زمان میرود) مربوط نمیشود. در عوض، پارامتر GPIO_InitTypeDef.Speed، نرخ سرعت slew rate یک GPIO را تعیین میکند، یعنی سرعت آن از سطح 0 ولت به VDD یک و بالعکس.
شکل بالا این موضوع را به وضوح نشان میدهد. موج قرمز همان موجی است که اگر سرعت پاسخ حداکثر بود، دریافت میکردیم و تاخیری در پاسخ وجود نداشت. اما در عمل چیزی که به دست میآوریم همان چیزی است که توسط موج سبز نشان داده شده است.
اما این پارامتر چقدر بر روی slew rate ورودی/خروجی STM32 تأثیر میگذارد؟ اول از همه هر خانواده STM32 ویژگیهای خاصی را برای I/O خود دارد. بنابراین باید دیتاشیت MCU خود را در قسمت Absolute Maximum Ratings بررسی کنید. در مرحله بعد، میتوانیم از یک آزمایش ساده برای اندازهگیری نرخ انحراف slew rate استفاده کنیم (آزمایش بر روی برد Nucleo-F446RE انجام میشود).
int main(void) }
GPIO_InitTypeDef GPIO_InitStruct;
HAL_Init();
__HAL_RCC_GPIOC_CLK_ENABLE();
/* Configure GPIO pin : PC4 */
GPIO_InitStruct.Pin = GPIO_PIN_4;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
/* Configure GPIO pin : PC8 */
GPIO_InitStruct.Pin = GPIO_PIN_8;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
while(1) { GPIOC->ODR = 0x110;
GPIOC->ODR = 0;
}
}
کد بالا واقعاً واضح است. ما دو پین را به عنوان خروجی پیکربندی میکنیم. یکی از آنها PC4 با سرعت GPIO_SPEED_FREQ_LOW و دیگری، PC8، با سرعت GPIO_SPEED_FREQ_VERY_HIGH پیکربندی شده است. شکل زیر تفاوت بین دو پین را نشان میدهد. همانطور که میبینیم، سرعت PC4 حدود 25 مگاهرتز است، در حالی که سرعت پین PC8 حدود 50 مگاهرتز است.
با این حال، به خاطر داشته باشید که راهاندازی یک پین با اعمال شرایط سخت (سرعت بالا)، بر روی انتشار کلی EMI برد شما تأثیر میگذارد. امروزه طراحی حرفهای به گونه ایست تا EMI برد به حداقل برسد. توصیه میشود که پارامتر سرعت GPIO را به صورت پیشفرض روی حداقل سطح بگذارید، مگر اینکه در شرایط خاص نیاز متفاوتی داشته باشد.
فرکانس سوئیچینگ موثر چطور؟ ST در دیتاشیتهای خود ادعا میکند که سریع ترین سرعت toggle شدن یک پین خروجی، دو سیکل کلاک است. گذرگاه AHB1، جایی که واحد جانبی GPIO متصل است، با فرکانس 42 مگاهرتز برای میکروکنترلر STM32F446 کار میکند. بنابراین پین مورد نظر باید با سرعتی در حدود 20 مگاهرتز تغییر کند. با این حال، ما باید یک سربار اضافی مربوط به انتقال حافظه بین رجیستر GPIO->ODR و مقداری که قرار است در داخل آن ذخیره کنیم (x110) اضافه کنیم که یک سیکل دیگر از CPU را به همراه دارد. بنابراین حداکثر سرعت سوئیچینگ مورد انتظار GPIO حدود 14 مگاهرتز است. اسیلوسکوپ نیز همانطور که در شکل زیر نشان داده شده است، این را تأیید میکند:
درایو GPIO
CubeHAL چهار روال را برای خواندن، تغییر و قفل کردن وضعیت یک I/O ارائه میکند. برای خواندن وضعیت یک I/O میتوانیم از تابع زیر استفاده کنیم:
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
که توصیفگر GPIO و شماره پین را میپذیرد و وقتی I/O صفر است GPIO_PIN_RESET و یا وقتی یک است GPIO_PIN_SET را برمیگرداند. برعکس، برای تغییر وضعیت I/O، تابع زیر را داریم:
void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
که توصیفگر GPIO، شماره پین و حالت مورد نظر را میپذیرد. اگر بخواهیم به سادگی حالت I/O را معکوس کنیم، میتوانیم از این روال زیر استفاده کنیم:
void HAL_GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
در نهایت، یکی از ویژگیهای واحد جانبی GPIO این است که میتوانیم تنظیمات یک I/O را قفل کنیم. هر تلاشی برای تغییر تنظیمات آن، تا زمانی که ریست رخ ندهد، ناموفق خواهد بود. برای قفل کردن پیکربندی پین میتوانیم از این روال استفاده کنیم:
HAL_StatusTypeDef HAL_GPIO_LockPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
De-initialize a GPIO
این امکان وجود دارد که یک پین GPIO را به وضعیت پیش فرض آن (که در حالت Input Floating Mode شناور ورودی است) تنظیم کنید:
void HAL_GPIO_DeInit(GPIO_TypeDef *GPIOx, uint32_t GPIO_Pin)
اگر دیگر به یک واحد جانبی خاص نیاز نداشته باشیم، یا برای جلوگیری از اتلاف توان مصرفی زمانی که CPU در حالت Sleep قرار میگیرد، میتوانیم از این تابع استفاده کنیم.