در قسمت قبل آموزش برنامه نویسی C به دستورات کنترلی زبان C برای امبدد سیستم ها پرداختیم. در این قسمت به آموزش گرفتن دستور اجرا از دکمه، کنترلرهای حلقه، ضدالگوها در امبدد C می پردازیم.
گرفتن دستور اجرا از دکمه
حالا که نحوه استفاده از دستورات شرطی را یاد گرفتیم، برنامهای مینویسیم که بر اساس تنها منبع ورودی که برد توسعه ما به طور پیشفرض دارد (یک کلید آبی) تصمیم بگیرد. برنامه ما از تنها خروجیای که میدانیم چگونه آن را کنترل کنیم استفاده میکند یعنی LED.
حالا در ادامه برد توسعه خود را به یک لامپ کامپیوتری کوچک تبدیل کنیم.
System Workbench for STM32 را راهاندازی کنید و یک پروژه تعبیهشده جدید را شروع کنید. فایل main.c باید به شکل زیر باشد:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | /** ********************************************************************* * @file main.c * @author Steve Oualline * @version V1.0 * @date 11-April-2018 * @brief چشمک بزند LED - دکمه را فشار دهید ********************************************************************* */ #include "stm32f0xx.h" #include "stm32f0xx_nucleo.h" int main(void) { تعریف ساختارهای مقداردهی اولیه برای LED و دکمه// 1. GPIO_InitTypeDef GPIO_LedInit; // LED مقداردهی اولیه برای GPIO_InitTypeDef GPIO_ButtonInit; // مقداردهی اولیه برای دکمه GPIO_PinState result; // نتیجه خواندن پین // مقداردهی اولیه سختافزار HAL_Init(); // 2. LED(clock) فعالکردن کلاک LED2_GPIO_CLK_ENABLE(); // 3. LED مقداردهی اولیه GPIO_LedInit.Pin = LED2_PIN; GPIO_LedInit.Mode = GPIO_MODE_OUTPUT_PP; GPIO_LedInit.Pull = GPIO_PULLUP; GPIO_LedInit.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(LED2_GPIO_PORT, &GPIO_LedInit); // فعالکردن کلاک دکمه USER_BUTTON_GPIO_CLK_ENABLE(); پیکربندی پین GPIO برای دکمه// 4. GPIO_ButtonInit.Pin = USER_BUTTON_PIN; GPIO_ButtonInit.Mode = GPIO_MODE_INPUT; GPIO_ButtonInit.Pull = GPIO_PULLDOWN; GPIO_ButtonInit.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(USER_BUTTON_GPIO_PORT, &GPIO_ButtonInit); // حلقه اصلی برنامه for (;;) { // خواندن وضعیت فعلی دکمه result = HAL_GPIO_ReadPin(USER_BUTTON_GPIO_PORT, USER_BUTTON_PIN); // بررسی وضعیت دکمه if (result == GPIO_PIN_SET) { // LED روشنکردن HAL_GPIO_WritePin(LED2_GPIO_PORT, LED2_PIN, GPIO_PIN_SET); } else { // LED خاموشکردن HAL_GPIO_WritePin(LED2_GPIO_PORT, LED2_PIN, GPIO_PIN_RESET); } } } |
بیایید به طور مفصل این کد را مرور کنیم.
مقداردهی اولیه
برای شروع برنامه، از مقدار زیادی کد استفاده میکنیم که توسط hardware abstraction layer (HAL) تعریف شده است. در چند فصل بعدی، هر یک از این قطعات را یاد خواهید گرفت.
ابتدا، یک متغیر جدید به نام GPIO_LedInit از نوع GPIO_InitTypeDef تعریف میکنیم.
۱. نوع GPIO_InitTypeDef یک نوع استاندارد C نیست: این نوع توسط فایلهایinclude شده که نامشان شامل HAL است , به پروژه اضافه شده است. در این مرحله، جزئیات مهم نیستند. ما به متغیر نیاز داریم تا نحوه پیکربندی پین LED را تعریف کنیم. (در فصلهای بعدی در مورد تعریف انواع متغیر یاد خواهید گرفت.)
به طور مشابه، متغیر دیگری به نام GPIO_ButtonInit را برای تعریف نحوه پیکربندی پین GPIO متصل به کلید آبی رنگ و یک متغیر برای نگهداری وضعیت آن (GPIO_PinState) تعریف میکنیم.
در داخل تابع اصلی، اولین کاری که انجام میدهیم فراخوانی HAL_Init برای راهاندازی سختافزار است، همان کاری که در برنامه چشمکزن در فصل ۳ انجام دادیم. شما نیاز دارید که HAL_Init را در ابتدای هر برنامه STM32 فراخوانی کنید.سپس، کلاک LED2 (user LED) را روشن میکنیم
۲. کلاک کنترل میکند که چگونه انتقال دادههای نوشته شده برای GPIOpin به پین واقعی منقل میشوند. بدون این خط، نوشتن روی پین LED کار نمیکند. اگرچه به نظر میرسد فراخوانی یک تابع به نام LED2_GPIO_CLK_ENABLE است، اما در واقع یک ماکرو پیشپردازنده (preprocessor macro) است که بعداً آن را مطالعه خواهیم کرد.اکنون به قسمتی میرسیم که در آن متغیر GPIO_LedInit را مقداردهی میکنیم.
۳. یک نوع ساختار با بخشهای زیادی است که باید بهصورت جداگانه اختصاص داده شود. بعداً جزئیات آنچه در اینجا اتفاق میافتد را یاد خواهید گرفت. در قسمت 4 کد مشابهی پین استفاده شده که کلید ابی رنگ را مقداردهی اولیه میکند، به جز اینکه حالت پین روی GPIO_MODE_INPUT تنظیم شده است؛ زیرا ما پین را برای دریافت وضعیت کلید میخوانیم و ورودی سیستم ما است.
انتخاب مدار Pulldown
توجه داشته باشید که در ۴ فیلد Pull را روی GPIO_PULLDOWN تنظیم کردهایم، نه GPIO_PULLUP. فیلد Pull برای CPU نوع مدار pullup/pulldown که باید استفاده کند را تعیین میکند. یک پین ورودی میتواند یکی از سه حالت شناور(floating)، بالاکش(pullup) و پایینکش(pulldown) داشته باشد. شکل ۵-۱ مدار مربوط به یک ورودی شناور را نشان میدهد.
وقتی سوئیچ SW1 باز است، ولتاژ به User_Button_Pin اعمال نمیشود؛ بنابراین، میتواند بالا (حدود ۳ ولت یا بیشتر) یا پایین (کمتر از حدود ۳ ولت) یا جایی بین این دو باشد. این میتواند توسط هر نویز الکتریکی سرگردانی که در اطراف آن است تنظیم شود. نکته کلیدی، هیچ راهی برای دانستن مقدار این سیگنال وجود ندارد، مگر اینکه واقعاً به زمین یا برق اتصال کوتاه شود.
حالا بیایید نگاهی به یک ورودی با مدار pullup بیندازیم (شکل ۵-۲ را ببینید).
وقتی SW1 باز است، ولتاژ از طریق مقاومت R1 جریان مییابد و User_Button_Pin را به VCC یا سطح مثبت بالا میکشد (یا pullup میکند). هنگامی که SW1 بسته است، پین به زمین (Gnd) اتصال کوتاه می شود. R1 یک مقاومت بسیار بزرگ است، بنابراین جریانی که از آن عبور میکند ناچیز است و ولتاژ روی پین به صفر میرسد.
مدار pulldown مشابه است، به جز اینکه R1 به زمین و SW1 به VCC وصل شده است، بنابراین اگر SW1 باز باشد، User_Button_Pin به زمین میرود (یعنی به صفر pulldown میشود)
در تراشه STM32، مدارها ارزان و پینها گران هستند؛ بنابراین، سازندگان تراشه میخواستند تاحدامکان از هر پین استفاده کنند. برای هر پین GPIO، یک مقاومت pullup، یک مقاومت pulldown و ترانزیستورهایی برای اتصال این مقاومتها بسته به نحوه پیکربندی پین وجود دارد. وجود اینها کار را آسان میکند، زیرا ما مجبور نیستیم خودمان این مقاومتها را روی برد قرار دهیم. بااینحال، دشواریهایی نیز به همراه دارد، زیرا باید آنها را برنامهریزی کنیم. شکل ۵-۴ سیمکشی داخلی یک پین GPIO واحد روی STM32 را نشان میدهد. (حتی این یک نسخه ساده شده است.) نکته کلیدی این است که مقاومتهای داخلی pullup (RPU) و pulldown (RPD) وجود دارند که میتوان آنها را روشن و خاموش کرد.
ما تصمیم گرفتیم از یک مدار pulldown استفاده کنیم؛ زیرا طرف دیگر دکمه به ۵+ ولت متصل است، بنابراین زمانی که دکمه فشرده نشده و سوئیچ باز است، مقاومت pulldown ما وارد مدار میشود و پین GPIO مقدار ۰ را میگیرد. هنگامی که دکمه فشرده میشود، 5 ولتی که از دکمه میآید باعث میشود پین GPIO مقدار 1 داشته باشد. (مقدار کمی جریان نیز از طریق مقاومت جریان خواهد داشت، اما این مقدار جریان ناچیز است.)
خواندن وضعیت دکمه
حالا به حلقه اصلی خود میرسیم. دستور for برای همیشه یا تا زمانی که دستگاه را راهاندازی مجدد کنیم اجرا میشود. داخل حلقه، اولین دستور متغیری به نام result از نوع GPIO_PinState (یک نوع غیراستاندارد تعریف شده توسط فایلهای شامل HAL) را با نتیجه فراخوانی تابع HAL_GPIO_ReadPin مقداردهی اولیه میکند.
HAL_GPIO_ReadPin پین GPIO متصل به دکمه را میخواند. به طور دقیقتر، پورت GPIO ۳۲ بیتی USER_BUTTON_GPIO_PORT را میخواند و سپس مقدار USER_BUTTON_PIN را آزمایش میکند. (بخش زیادی از دستکاری بیتهایی که در فصل قبل بررسی کردیم در داخل تابع HAL_GPIO_ReadPin اتفاق میافتد.)
حالا با مقایسه result با سمبل GPIO_PIN_SET (ثابتی که توسط کد HAL تعریف شده است) بررسی میکنیم که آیا پین تنظیم شده است، و سپس در صورت تنظیمشدن پین دکمه، پین LED را روشن میکنیم. در غیر این صورت، پین LED را خاموش میکنیم. (کد انجام این کار در فصل ۳ برسی میشود.)
اجرای برنامه
هنگامی که برنامه را اجرا میکنیم، LED روشن میشود. دکمه کاربر را فشار دهید و LED خاموش میشود. دکمه را رها کنید و LED دوباره روشن میشود و به همین ترتیب ادامه مییابد. هرچند عملیاتی ساده است، اما مراحل زیادی پشت سر گذاشتیم تا به اینجا برسیم.
متأسفانه، ما یک چراغقوه بسیار پیچیده با یک دکمه ساختهایم که بهجای روشنکردن، چراغقوه را خاموش میکند. خبر خوب این است که چراغقوه تحت کنترل رایانه است، بنابراین میتوانیم آن را با نرمافزار برطرف کنیم. من این را به عهده شما میگذارم.
کنترلرهای حلقه
نمونه برنامهنویسی ما استفاده اولیهای از حلقه را نشان داد، اما C به شما چندین راه برای کنترل حلقههایتان ارائه میکند. دودو استیتمنت اصلی این کارbreak و continue هستند.
عبارت break
عبارتbreak به شما امکان میدهد زودتر از یک حلقه خارج شوید. برای مثال، برنامه کوتاه زیر را در نظر بگیرید که به دنبال یک عدد در آرایه میگردد. اگر عدد وجود داشته باشد، برنامه آن را چاپ میکند:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | /* * عدد کلیدی را در آرایه پیدا کنید. */ #include <stdio.h> #include <stdbool.h> #define ARRAY_SIZE 7 // اندازه آرایه برای جستجو int main() { // آرایه برای جستجو int array[ARRAY_SIZE] = {4, 5, 23, 56, 79, 0, -5}; static const int KEY = 56; // عدد برای جستجو for (unsigned int index = 0; index < ARRAY_SIZE; ++index) { if (array[index] == KEY) { printf("Key (%d) found at index %d\n",KEY, index); break; } } return (0); } |
این برنامه یک آرایه را برای یک مقدار کلیدی جستجو میکند. هنگامی که مقدار کلیدی را پیدا کردیم، کارمان تمام شده است. ما نمیخواهیم بقیه حلقه را طی کنیم، بنابراین برای خروج از حلقه از عبارت break استفاده میکنیم.
عبارت continue
عبارت کنترل دیگر، continue، اجرای حلقه را از ابتدا شروع میکند به عبارتی از اجرا ادامه خطوط برنامه داخل حلقه صرف نظر میکند و از ابتدای حلقه شروع به کار میکند.
برنامه زیر لیستی از کامندها را چاپ میکند و از کامندهایی که با نقطه شروع میشوند، صرفنظر میکند. هنگامی که با یکی از این موارد برخورد میکنیم، با کامندcontinue به ابتدای حلقه میپریم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | /* * کامند کلیدی را در آرایه پیدا کن. */ #include <stdio.h> #define COMMAND_COUNT 5 // Number of commands //مخفی هستند کامندهایی که با نقطه شروع میشوند commands[COMMAND_COUNT][4] = { "help", "exec", ".adm", "quit" }; int main() { // چاپ متن راهنما for (unsigned int index = 0; index < COMMAND_COUNT; ++index) { 1 if (commands[index][0] == '.') { // Hidden command 2 continue; } printf("%s\n", commands[index]); } return (0); } |
در این برنامه، نکته کلیدی بررسی وجود نقطه (1) در ابتدای هر فرمان است. اگر نقطه وجود داشته باشد، با استفاده از کامند continue (2) به ابتدای حلقه پرش میکنیم. به این ترتیب، از چاپ آن کامند خاص (با printf) و همچنین بررسی باقی کامندها در حلقه صرفنظر میکنیم.
ضدالگوها
درحالیکه یاد میگیرید چگونه از حلقهها استفاده کنید، باید بیاموزید که چه زمانی از حلقهها استفاده نکنید. چندین الگوی برنامهنویسی وارد صنعت برنامهنویسی شدهاند که بهجای ترویج برنامهنویسی خوب، باعث سردرگمی میشوند، به همین دلیل است که آنها را ضدالگو مینامند. من دو مورد از آنها را به شما نشان میدهم.
حلقه while خالی
اولین ضدالگو، حلقه while خالی است. کد زیر را در نظر بگیرید:
1 2 3 4 5 6 7 | while (GPIO_PIN_SET == HAL_GPIO_ReadPin(USER_BUTTON_GPIO_PORT, USER_BUTTON_PIN)); { // ... do something } |
احتمالا حدس میزنید که این کد تا زمانی که دکمه کاربر فشار داده شود، عمل خاصی را تکرار میکند. اما اینطور نیست. حلقه while تنها بر روی یک استیتمنت واحد تأثیر میگذارد. ممکن است تصور کنید که استیتمنت داخل حلقه while همان استیتمنتی است که در آکولاد قرار گرفته است، اما قبل از آکولاد نیز یک استیتمنت وجود دارد. این یک استیتمنت بسیار کوتاه و بهراحتی قابلچشمپوشی است، زیرا یک استیتمنت خالی است. ما میتوانیم با وجود یک سیمیکالن بعد از استیتمنت، بفهمیم که وجود دارد:
1 2 3 4 5 6 7 | while (GPIO_PIN_SET == HAL_GPIO_ReadPin(USER_BUTTON_GPIO_PORT, USER_BUTTON_PIN)); { // ... do something } |
این نقطهویرگول را بهراحتی میتوان نادیده گرفت. به همین دلیل است که این نوع کدنویسی بد در نظر گرفته میشود.
ما میتوانیم این حلقه while را بهصورت زیر بازنویسی کنیم و با افزودن یک بلاک خالی و افزودن توضیحات لازم کد را خوانا تر کنیم:
1 2 3 4 5 6 7 | while (GPIO_PIN_SET == HAL_GPIO_ReadPin(BTN_PORT, BTN_PIN)) { // Loop intentionally left empty to wait for button release } |
حالا کاملاً مشخص است که حلقه while هیچ دستوری درون بلاک خود ندارد و تنها منتظر پایان شرط خود در اینجا فشردن کلید است .
انتصاب در حلقه while
دومین ضدالگو، انتساب در حلقه while است:
1 2 3 4 5 | while ((result = ReadPin(BTN_PORT, BTN_PIN)) == GPIO_PIN_SET) { // ... دستورات } |
این استیتمنت به طور همزمان دو کار انجام میدهد. اول، تابع ReadPin را فراخوانی کرده و نتیجه را به متغیر result اختصاص میدهد. دوم، مقدار متغیر result را بررسی میکند تا ببیند که آیا تنظیم شده است (تساوی با GPIO_PIN_SET).
درک و نگهداری برنامهها بسیار آسانتر خواهد بود اگر کارهای کوچک و ساده را تکتک انجام دهند. این میانبر در ازای کاهش خوانایی برنامه، چند خط جدید را برای تایپکردن ذخیره میکند. این کد بهسادگی میتوانست بهصورت زیر بازنویسی شود:
1 2 3 4 5 6 7 8 9 10 11 | while (1) { result = ReadPin(BTN_PORT, BUTTON_PIN); if (result != GPIO_PIN_SET) break; // ... دستورات } |
هدف ما باید برنامهنویسی تاحدامکان ساده و خوانا باشد، نه اینکه آنها را تا حد ممکن فشرده و هوشمندانه بنویسیم.
جمعبندی
حالا دو جنبه کلیدی از محاسبات را فراگرفتهایم: اعداد و نحوه تصمیمگیری بر اساس آن اعداد. تصمیمات منفرد را میتوان با استیتمنت if گرفت، درحالیکه استیتمنتهای while و for به ما امکان میدهند تصمیمات تکراری را اتخاذ کنیم. کلیدواژههای break و continue به ما کنترل بیشتری بر روی این تصمیمات میدهند.
استیتمنتهای تصمیمگیری به ما این امکان را دادند تا یک برنامه کوچک بنویسیم که با لمس یک دکمه، LED را چشمکزن کند. اگرچه این برنامه ساده است، ما ورودی گرفتیم، آن را پردازش کردیم و خروجی تولید کردیم که این اساس تعداد زیادی از برنامههای امبدد است. در چند فصل بعدی، یاد خواهید گرفت که چگونه با دادههای پیچیدهتر و روشهای پیچیدهتر برای پردازش آنها برخورد کنید که همه بر اساس اصول اولیهای که در اینجا آموختهاید، بنا شده است.
- برنامهای بنویسید که جدولضربی از 0 × 0 تا 9 × 9 را تولید کند.
- برنامهای بنویسید که تعداد بیتهای تنظیم شده در یک عدد صحیح از نوع uint32_t را بشمارد. برای مثال، عدد x0000A00، دو بیت تنظیمشده دارد.
- برنامهای بنویسید که یک الگو را روی LED چشمک بزند. برای کنترل تأخیر روشن و خاموششدن LED از یک آرایه از اعداد صحیح استفاده کنید. الگو را تکرار کنید.
- برنامهای بنویسید که حرف “H” را به کد مورس با استفاده از LED چشمک بزند. هنگامی که دکمه فشار داده میشود، “E” را چشمک میزند. اگر ادامه به فشاردادن دکمه کنید، تمام کلمه “HELLO WORLD” را بهصورت کد مورس دریافت میکنید.
- برنامهای بنویسید که 10 عدد اول را محاسبه کند.
- برنامهای بنویسید که بزرگترین و کوچکترین عناصر مجموعه را پیدا کند.
- برنامهای ایجاد کنید که یکرشته را بررسی کرده و فقط حروف صدادار را چاپ کند.