برای سنجش کارایی یک برنامه و پروژه و دیباگ آن، راههای مختلفی وجود دارد. در قسمتهای پیشین پروژههای سادهای را توسعه دادیم که در این راستا نیز میتوانند کاربرد داشته باشند. اجرای صحیح برنامه چشمکزن همواره یکی از سادهترین آزمایشهایی است که در یک بورد یا پروژه میتوان انجام داد. اما یک راه بسیار کارآمد و ساده برای منظور دیباگ و خطایابی اجرا، نمایش دادن پیامهای خاص روی ترمینال سریال است. پس اگر بتوانیم به روشی ساده با ترمینال سریال ارتباط داشته باشیم، فرایند دیباگ پروژهها، آسان و کارآمدتر خواهد شد. در قسمت قبل از سری آموزش STM32 با توابع HAL، در مورد نحوه دریافت اطلاعات بهوسیله USART صحبت شد. در این قسمت، با طریقه ریدایرکت کردن توابع stdio آشنا میشویم، درواقع میخواهیم دستوراتی مثل printf و scanf را به کمک واحد USART ریتارگت یا ریدایرکت کنیم. با سیسوگ همراه باشید.
در دو قسمت قبل دیدیم که برای تبادل اطلاعات با پورت سریال میتوانیم با دستورات ارسال یا دریافت USART و مشخص کردن طول کلمه و پارامترهای دیگر، عملیات دریافت و ارسال موردنظر را انجام دهیم. اما میدانیم که کارکردن با توابع کتابخانههای ورودی/خروجی استاندارد زبان C، آسان است و قابلیتهای بسیار خوبی به ما میدهد. ریدایرکت کردن توابع stdio با USART، میتوانیم از این امکانات در کار با این واحد، بهره ببریم.
دستورات Printf و Scanf در کتابخانه استاندارد
در کتابخانه استاندارد stdio، توابع مختلفی برای دریافت ورودی و یا چاپ خروجی وجود دارد که شاید پراستفادهترین آنها، دو تابع scanf و printf باشند. تابع printf همانطور که از نام آن نیز مشخص است برای چاپ عبارتهای موردنظر در کنسول یا ترمینال، استفاده میشود.
سینتکس این تابع بهصورت زیر است:
1 | printf("format string",argument_list); |
بخش اول ورودی یعنی format string، شامل نوع متغیرهای موردنظر برای چاپ میشود که میتواند ترکیبی از فرمتهای integer، کاراکتر، رشته، float و … باشد. argument list نیز مشخص میکند که در جایگاه فرمتهای تعریفشده، چه متغیر یا مقدارهایی باید چاپ شود.
تابع scanf نیز برای دریافت ورودی استفاده میشود و سینتکس آن بهصورت زیر است:
1 | scanf("format string",argument_list); |
برنامه زیر یک کاربرد نمونه و ساده این دو تابع را نشان میدهد:
1 2 3 4 5 6 7 8 | #include<stdio.h> int main(){ int number; printf("enter a number:"); scanf("%d",&number); printf("cube of number is:%d ",number*number*number); return 0; } |
نتیجه اجرای این برنامه به این صورت خواهد بود:
ریدایرکت کردن یا ریتارگت کردن توابع stdio به چه معناست؟
در محیط Embedded، اگر هدرفایل stdio.h را اضافه کنیم و سپس مثلاً از دستور printf استفاده کنیم، هیچ نتیجهای مشاهده نخواهیم کرد. زیرا کاربرد این تابع در این محیط تعریفنشده است. برای اینکه بتوانیم از توابع خروجی این کتابخانه بهمنظور دیباگ و چاپ خروجی استفاده کنیم، باید توابع پایه این کتابخانه را به شکل دیگری تعریف کنیم. یکی از راههای استفاده از این توابع، تعریف آنها بهوسیله رابط USART است. به این عمل ریتارگت یا ریدایرکت کردن نیز گفته میشود.
در ادامه روش این کار در محیط STM32CubeIDE توضیح داده میشود. در مرحله اول چگونگی ریدایرکت کردن این توابع با USART و در مرحله (اختیاری) دوم چگونگی ایجاد امکان استفاده از اعداد floating point را یاد میگیریم.
ایجاد پروژه
در این پروژه به یک واحد USART نیاز داریم. پس مثل پروژه قبل واحد USART1 را فعال میکنیم و تنظیمات آن را در همان حالت پیشفرض Receive and Transmit میگذاریم.
اکنون میتوانیم وارد فایل کد شویم.
نوشتن کد و انجام تنظیمات ریدایرکت
در اولین قدم، کتابخانه stdio را به پروژه اضافه میکنیم؛
1 | #include <stdio.h> |
اکنون میخواهیم یک کتابخانه برای عمل retarget کردن بسازیم تا بتوانیم از کد این پروژه بهراحتی در پروژههای بعدی نیز استفاده کنیم. بدین منظور، یک پوشه جدید بانام stdio_usart میسازیم و دو فایل stdio_usart.c و stdio_usart.h را در آن ایجاد میکنیم. اکنون این پوشه را از طریق Drag & Drop به مسیر Core در پروژه اضافه میکنیم و بعد از باز شدن پنجره زیر همان گزینه پیشفرض Copy files and folders را انتخاب و OK میکنیم؛
بعد از این مرحله، مسیر پروژه به صورت زیر در میآید:
اکنون باید مسیر این پوشه جدید را به مسیرهای Include اضافه کنیم. برای این کار باید از منوی Project، گزینه Properties را انتخاب کنیم. در منوی بازشده، از قسمت C/++C Build وارد بخش Settings میشویم. سپس از مجموعه MCU GCC Compiler، Include paths کلید Add () را میزنیم و مسیر پوشه موردنظر را اضافه میکنیم؛
حالا میتوانیم به سراغ نوشتن کد در فایلهای stdio_usart.c و stdio_usart.h برویم. کار را از stdio_usart.h شروع میکنیم و کد زیر را درون این فایل مینویسیم؛
1 2 3 4 5 6 | #include "stm32f1xx_hal.h" #include <stdio.h> void RetargetInit(UART_HandleTypeDef *huart); int _write(int fd, char* ptr, int len); int _read(int fd, char* ptr, int len); |
در این کد، ابتدا کتابخانه stm32f1xx_hal برای دسترسی به توابع مربوط به uart در میکروکنترلر، و در خط دوم کتابخانه stdio برای دسترسی به توابع مربوط به ورودی و خروجی، اضافهشدهاند. در سه خط بعدی نیز توابع موردنظر برای راهاندازی، نوشتن و خواندن، اعلانشده است.
حالا به سراغ فایل stdio_usart.c میرویم تا این توابع را تعریف کنیم. در این فایل، ابتدا stdio_usart.h را اضافه میکنیم و همچنین یک اشارهگر از نوع UART_HandleTypeDef تعریف میکنیم؛
1 2 3 | #include "stdio_usart.h" UART_HandleTypeDef *gHuart; |
اکنون به تعریف توابع میپردازیم. اولین تابع یعنی RetargetInit را بهصورت زیر تعریف میشود؛
1 2 3 4 5 6 7 8 | //Initialization function sets the uart handler void RetargetInit(UART_HandleTypeDef *huart) { gHuart = huart; /* Disable I/O buffering for STDOUT stream, so that * chars are sent out as soon as they are printed. */ setvbuf(stdout, NULL, _IONBF, 0); } |
در خط اول این تابع، uart handler مقداردهی شده است و در خط بعدی، بهوسیله تابع setvbuf، قابلیت I/O buffering برای افزایش سرعت اجرای دستور چاپ اطلاعات، غیرفعال شده است.
تابع بعدی یعنی write_ بهصورت زیر تعریف میشود:
1 2 3 4 5 6 7 8 9 10 | //Definition of the underlying function for std out int _write(int fd, char* ptr, int len) { HAL_StatusTypeDef hstatus; hstatus = HAL_UART_Transmit(gHuart, (uint8_t *) ptr, len, HAL_MAX_DELAY); if (hstatus == HAL_OK) return len; else return -1; } |
همانطور که دیده میشود، در این تابع از تابع استاندارد کتابخانه HAL که برای ارسال اطلاعات معرفی کردیم استفادهشده است.
در آخر، تعریف تابع read_ نیز بهصورت زیر است:
1 2 3 4 5 6 7 8 9 10 | //Definition of the underlying function for std in int _read(int fd, char* ptr, int len) { HAL_StatusTypeDef hstatus; hstatus = HAL_UART_Receive(gHuart, (uint8_t *) ptr, 1, HAL_MAX_DELAY); if (hstatus == HAL_OK) return 1; else return -1; } |
در این مرحله، نوشتن هر دو فایل stdio_uart.h و stdio_uart.c، به اتمام رسیده است و میتوانیم آنها را ذخیره کنیم و به سراغ فایل اصلی برنامه، یعنی main.c، برویم. در این فایل نیز ابتدا stdio_uart.h را اضافه میکنیم؛
1 2 3 | /* USER CODE BEGIN Includes */ #include "stdio_usart.h" /* USER CODE END Includes */ |
سپس درون بدنه تابع int main یک بافر برای ذخیره اطلاعات دریافتی، تعریف میکنیم:
1 | char buf[100]; |
اکنون میتوانیم به صورت زیر، از دستورات Printf و Scanf استفاده کنیم:
1 2 3 | printf("\r\nPlease enter your name: "); scanf("%s", buf); printf("\r\nHello, %s!\r\n", buf); |
حالا از پروژه build میگیریم و کد را روی میکرو دانلود میکنیم. در صورت درست انجام دادن مراحل گفتهشده، و اتصال صحیح میکرو به پورت سریال، میتوانیم نتیجه اجرای برنامه را در ترمینال سریال مشاهده کنیم؛
پس از اجرا، پیام اول در ترمینال چاپشده و برنامه در انتظار وارد شدن نام (یک رشته در ورودی) باقی میماند. پس از واردکردن نام، ادامه برنامه اجرا خواهد شد:
فعال سازی پشتیبانی از Floating Point
اگر در این مرحله از توسعه پروژه، بخواهیم از فرمت float در دستورات ورودی خروجی استفاده کنیم، به مشکل بر خواهیم خورد. برای مشخص شدن این مشکل، قطعه کد زیر که از صفحه مرجع مربوط به تابع printf برداشتهشده است را در برنامه بهکار میبریم (کد نوشته در بدنه while(1) در قسمت قبل را کامنت میکنیم و کد زیر را قبل از while(1) مینویسیم؛
1 2 3 4 5 6 7 8 | printf("Characters: %c %c\n", 'a', 65); printf("Decimals: %d %ld\n", 1977, 650000L); printf("Preceding with blanks: %10d\n", 1977); printf("Preceding with zeros: %010d\n", 1977); printf("Some different radices: %d %x %o %#x %#o\n", 100, 100, 100, 100, 100); printf("floats: %4.2f %+.0e %E\n", 3.1416, 3.1416, 3.1416); printf("Width trick: %*d\n", 5, 10); printf("%s\n", "A string"); |
پس از دانلود برنامه، نتیجه اجرا را در ترمینال مشاهده میکنیم؛
همانطور که در تصویر نشان دادهشده است، دستور مربوط به چاپ مقدار float کارنکرده است. دلیل این اتفاق، فعال نبودن پشتیبانی از فرمت float است. اگر در کد دقت کنیم، مشاهده میکنیم که در خط کد مربوط به چاپ مقدار float، یک warning رخداده است؛
این پیغام، هشدار میدهد که پشتیبانی از فرمت float فعال نیست و باید از مسیر مربوطه فعال شود. اکنون برای فعالسازی به همان قسمت تنظیمات Properties از منوی Project میرویم. بخش Settings را از قسمت C/++C Build انتخاب میکنیم. سپس از تب Tool Settings وارد MCU Settings میشویم. در این پنجره، مانند شکل گزینه “Use float with printf from newlib-nano” را فعال میکنیم و درنهایت Apply and Close را میزنیم:
اکنون مجدداً پروژه را build و روی میکرو دانلود میکنیم و نتیجه اجرای کد را در ترمینال سریال میبینیم:
همانطور که در تصویر دیده میشود، این بار دستور چاپ مقادیر float به عمل کرده است.
توجه به این نکته حائز اهمیت است که فعالسازی پشتیبانی از فرمت float در دستور printf، باعث مصرف بخش قابلتوجه ای از حافظه خواهد شد. این عمل بهطور تقریبی حدود 0.35KB از حافظه RAM و 10.30KB از حافظه Flash را اشغال خواهد کرد که این مقدار حافظه، علاوه بر مقداری است که خود برنامه و تابع printf (بدون پشتیبانی float) مصرف میکند. درنتیجه توصیه میشود در برنامههایی که استفاده از این امکان ضرورت ندارد، آن را فعال نکرده و حافظه را اشغال نکنیم. بهخصوص در سختافزارهایی که حافظه محدودی دارند، توجه به این نکته بیشازپیش اهمیت پیدا میکند.
در این قسمت از سری آموزش STM32 با توابع HAL، نحوه ریدایرکت کردن توابع stdio را بررسی کردیم. در قسمت بعدی در مورد تایمرها و کاربردهای آنها صحبت خواهیم کرد. با ما همراه باشید.
سلام من وقتی از ارسال بصورت عادی یعنی خود تابع اصلی uart استفاده میکنم براحتی کار میکنه ولی به هیچ روشی نمیتونم از printf استفاده کنم هم اموزش شما هم تمام اموزش های خارجی, برنامه ارور نمیده ولی کار نمیکنه.دلیلش چیه واقعا؟
دلیل کار نکردنش چی?
خیلی دلیل میتونه داشته باشه
دقیقا چه اتفاقی نیفتاده ؟
برای من کار نمیکنه؟آیا مشکلی داره؟
آیا خطایی دریافت میکنید ؟
آیا پروژه رو از گیت دانلود کردید ؟
برنامهم رو تبدیل کردم به cpp دیگه printf کار نکرد
سلام
میشه دارک مود سیسوگ رو رونمایی کنید لطفاً؟
طولانی مدت توی سایتیم، همه جا هم دارکه، سوئیچ میکنیم تو سیسوگ باید عینک آفتابی بزنیم 😁 مرسی.
در برنامه اصلی RetargetInit فراخوانی نشده، لذا خروجی نخواهیم داشت.
تشکر از کار زیبای شما