facebook pixel בלוג: האם אנחנו מתכנתים נכון עם הארדואינו? - www.4project.co.il
Main logo www.4project.co.il
כל הרכיבים לפרוייקט שלכם
עגלת קניות

העגלה ריקה

האם אנחנו מתכנתים נכון עם הארדואינו?


2024-05-27 13:41:19
שאלה קצת פילוסופית, וכנראה שיהיו הרבה דעות לכל מני כיוונים בנושא זה, אבל יש לי (ולא רק לי כנראה) בעיה עם הצורה בה כולם מתרגלים לכתוב את הקוד לארדואינו.

כמי שכותב קוד ב-35 שנים האחרונות, חלק גדול מהם במערכות Embedded ומיקרובקרים, אני מרשה לעצמי להגיד שלא נוח לי עם הצורה בה כותבים את קוד בסביבת ארדואינו.
ניסיתי לייצר תוכנית מסגרת (Framework) ש"תיישר" את סביבת הפיתוח של ארדואינו לכיוון שאני רוצה, ו... לא כל כך הלך לי... מרוב התסכול מחקתי חלק מהקוד המעניין, אבל בפוסט זה אסביר למה אני מתכוון ומה הצלחתי ולא הצלחתי להשיג.

קצת רקע

כולנו מכירים את כרטיסי הארדואינו, במיוחד את ה-Arduino UNO שהפך לדגם הפופולרי ביותר ומאפשר להמון אנשים גישה קלה וזולה יחסית לפיתוח פרוייקטים מגניבים.
המטרה המקורית סביב פרוייקט הארדואינו היתה ועדיין נשארה לספק כלי ללימוד תכנות מיקרובקרים ויצירת פרוייקטים חומרתיים בצורה קלה ונגישה.

יחד עם הכרטיס החומרתי מגיעה גם סביבת פיתוח כדי להידור (To compile בעברית) ולהעלות את התוכנית לכרטיס.
הסביבה מספקת גם ספריות וקוד ה"עוטף" את היכולות החומרתיות של הכרטיס במחלקות ופונקציות קלות יותר לעיקול המתחילים (digitalRead, Serial וכו').

עם הזמן התפתחה קהילה מאוד גדולה של משתמשים שמייצרים ספריות (קוד מוכן) לדברים נוספים, מפרסמים את הקוד שהם כתבו ופרוייקטים שיצרו.

עד כאן הכל נפלא. אני בעד כל התקדמות והנגשה בתחום.

אז איפה הבעיה?

נעזוב את הצד הפחות טוב של זה שיש המון קוד מוכן ברשת. המתחילים בתחום במקום ללמוד ולהבין, מחפשים איפה להעתיק את מה שהם צריכים בלי להבין טיפה מה הקוד או הספריה שהם מצאו עושים. ובסוף מקבלים דברים לא עובדים או באגים בספריה שאיזה שהוא מתחיל אחר פרסם ברשת. 

גם אני משתמש בקוד מדוגמאות או פרוייקטים שמישהו פרסם, אבל אני תמיד עובר עליו כדי להבין מה עשו שם. אני מאוד נזהר עם ספריות מוכנות ובודק אותם ב-7 עיניים.

במהלך הניסיון שלי לייצר Framework שיעבוד בצורה שאני רוצה (פרטים בהמשך), הגעתי לעמוד הספריות של אנשים באתר של ארדואינו... עברתי קצת על הקוד של חלק מהספריות שעניינו אותי כדי לראות אם יש כבר מישהו שעשה את מה שאני מחפש... ובואו נגיד שלא התרשמתי לטובה... לכן אני מעדיף לא להשתמש בספריות הלא רשמיות.

זהו עמוד הספריות באתר של ארדואינו. יש בו פירוט של ספריות הרשמיות שנתמכות ע"י המפתחים של הסביבה (כנראה) ואלפי ספריות של אנשים אחרים המחולקים לקטגוריות:
"רשימת ספריות ארדואינו"

נעבור לצד הטכני של מה שאני ממש לא אוהב... ובמיוחד את הפונקציה ()delay שנאלצים להשתמש בגלל שחסר משהו: הטיימרים!

אני רגיל לכך שהתוכניות שאני כותב מגיבות לאיזה שהוא אירוע, מבצעות משהו והולכות "לישון" עד שקורה אירוע נוסף וכך הלאה...
זוהי גישה נוחה במיוחד למימוש של התוכנית בצורה של מכונת מצבים (State-Machine). זהו נושא לפוסט נפרד, לא רוצה להיכנס לזה עכשיו, אבל אם מישהו רוצה ללמוד עוד על השיטה, אפשר למצוא הרבה הסברים טובים, הנה זה של ויקיפדיה.

רוב הדוגמאות של תכנות בסביבת הארדאוינו מציגות גישת תכנות קצת שונה, טורית, ביצוע של דברים אחד אחרי השני עם קריאה לפונקציית ()delay איפה שצריך לחכות.

הנה קוד של דוגמת ה-BLINK הפופולרית:
קוד: בחר הכל

void setup()
{
    // initialize digital pin LED_BUILTIN as an output.
    pinMode(LED_BUILTIN, OUTPUT);
}

// the loop function runs over and over again forever
void loop()
{
    digitalWrite(LED_BUILTIN, HIGH); // turn the LED on (HIGH is the voltage level)
    delay(1000);                     // wait for a second
    digitalWrite(LED_BUILTIN, LOW);  // turn the LED off by making the voltage LOW
    delay(1000);                     // wait for a second
}


הקוד מאוד פשוט, תדליק לד, חכה 1000 מילישניות, כבה לד, חכה 1000 מילישניות וחזור על זה שוב ושוב.
נכון, אני זוכר, כל זה נוצר כדי לאפשר למתחילים להיכנס לעולם המיקרובקרים ולבנות דברים, וצריך לפשט את העניינים כמה שיותר...
אבל התוכניות בדרך כלל לא עד כדי כך פשוטות, מה קורה אם רוצים להוסיף עוד רכיבים לפרוייקט?

בואו נמשיך עם הדוגמאות...
אני רוצה לחבר כפתור שידליק לד כשלוחצים עליו. גם משהו פשוט. וגם לזה יש דוגמה בסביבת הפיתוח, איזה יופי!
קוד: בחר הכל

// constants won't change. They're used here to set pin numbers:
const int buttonPin = 2; // the number of the pushbutton pin
const int ledPin = 13;   // the number of the LED pin

// variables will change:
int buttonState = 0; // variable for reading the pushbutton status

void setup()
{
    // initialize the LED pin as an output:
    pinMode(ledPin, OUTPUT);
    // initialize the pushbutton pin as an input:
    pinMode(buttonPin, INPUT);
}

void loop()
{
    // read the state of the pushbutton value:
    buttonState = digitalRead(buttonPin);

    // check if the pushbutton is pressed. If it is, the buttonState is HIGH:
    if (buttonState == HIGH)
    {
        // turn LED on:
        digitalWrite(ledPin, HIGH);
    }
    else
    {
        // turn LED off:
        digitalWrite(ledPin, LOW);
    }
}


שימו לב שכאן משום מה אין קריאה ל-()delay בלולאה (פונקציית ()loop). כי אנחנו רוצים שהדברים יקרו מהר, נכון? לוחצים ומיד נדלק הלד.

ועכשיו... אני מקבל בטחון ורוצה לשלב את שתי הדוגמאות האלה יחד כדי שיהיה לד אחד שיהבהב ולד אחר שיגיב ללחיצת הכפתור. 
קוד: בחר הכל

// constants won't change. They're used here to set pin numbers:
const int buttonPin = 2;    // the number of the pushbutton pin
const int buttonLedPin = 3; // the number of the LED pin
const int ledPin = 13;      // the number of the LED pin

// variables will change:
int buttonState = 0; // variable for reading the pushbutton status

void setup()
{
    // initialize the LED pin as an output:
    pinMode(ledPin, OUTPUT);
    pinMode(buttonLedPin, OUTPUT);
    // initialize the pushbutton pin as an input:
    pinMode(buttonPin, INPUT);
}

void loop()
{
    digitalWrite(ledPin, HIGH); // turn the LED on (HIGH is the voltage level)
    delay(1000);                // wait for a second
    digitalWrite(ledPin, LOW);  // turn the LED off by making the voltage LOW
    delay(1000);

    // read the state of the pushbutton value:
    buttonState = digitalRead(buttonPin);

    // check if the pushbutton is pressed. If it is, the buttonState is HIGH:
    if (buttonState == HIGH)
    {
        // turn LED on:
        digitalWrite(buttonLedPin, HIGH);
    }
    else
    {
        // turn LED off:
        digitalWrite(buttonLedPin, LOW);
    }
}


מקמפל, מעלה על הכרטיס, ו... הלד מהבהב, אבל הכפתור לא ממש מגיב. לפעמים כן, לפעמים לא. אם אני לוחץ הרבה אז יותר...

מה לא בסדר? ה-()delay!
משחק קצת עם המספרים, רואה שאפשר לקבל תגובה טובה יותר אם אני מקטין את המספרים בקריאה ל-()delay, אבל אז ההבהוב הוא לא פעם בשניה...

מחפש עוד קצת ברשת ומוצא דוגמה נוספת ל-BLINK בלי ה-()delay! מושלם!
קוד: בחר הכל

// constants won't change. Used here to set a pin number:
const int ledPin = LED_BUILTIN; // the number of the LED pin

// Variables will change:
int ledState = LOW; // ledState used to set the LED

// Generally, you should use "unsigned long" for variables that hold time
// The value will quickly become too large for an int to store
unsigned long previousMillis = 0; // will store last time LED was updated

// constants won't change:
const long interval = 1000; // interval at which to blink (milliseconds)

void setup()
{
    // set the digital pin as output:
    pinMode(ledPin, OUTPUT);
}

void loop()
{
    // here is where you'd put code that needs to be running all the time.

    // check to see if it's time to blink the LED; that is, if the difference
    // between the current time and last time you blinked the LED is bigger than
    // the interval at which you want to blink the LED.
    unsigned long currentMillis = millis();

    if (currentMillis - previousMillis >= interval)
    {
        // save the last time you blinked the LED
        previousMillis = currentMillis;

        // if the LED is off turn it on and vice-versa:
        if (ledState == LOW)
        {
            ledState = HIGH;
        }
        else
        {
            ledState = LOW;
        }

        // set the LED with the ledState of the variable:
        digitalWrite(ledPin, ledState);
    }
}


שמים לב איך הקוד הפשוט יחסית מתחיל להסתבך? אדלג על הוספת טיפול בכפתור ולד נוסף לקוד הזה, כי הפעם זה יעבוד, גם אם פשוט נדביק את שתי הדוגמאות יחד.

אגב, רואים את המשתנה ledState? זו בדיוק מכונת המצבים שהזכרתי קודם, במקרה הזה יש לה 2 מצבים: HIGH ו-LOW. מחליפים את המצבים כל שניה ומפעילים את הלד בהתאם למצב. בגלל שלמכונה יש רק 2 מצבים, אז מספיק רק משתנה אחד. מימוש קצת יותר רציני של מכונת מצבים יוכל לטפל בפרוייקטים יותר מסובכים, כמו למשל רמזור, או רובוט.

אז מה הבעיה שלי עם הקוד הזה? שרוב השורות שם מטפלות "זרימה של הקוד" (control flow) ולא בלוגיקה שאנחנו רוצים לממש.
תחשבו לרגע איך יראה הקוד אם נוסיף עוד דברים, כמו חיישנים, מנועים... זה רק ילך ויסתבך... במקום לטפל בדברים הלוגיים של התוכנית (הדלקת לד כל פרק זמן כלשהו, מה עושים כשלוחצים על כפתור), צריך לטפל בכל מה שמסביב כדי שהדברים שאנחנו צריכים יתבצעו בזמן הנכון...

בואו נראה איך ה-()delay ממומש:
קוד: בחר הכל
void delay(unsigned long ms)
{
    uint32_t start = micros();

    while (ms > 0)
    {
        yield();
        while (ms > 0 && (micros() - start) >= 1000)
        {
            ms--;
            start += 1000;
        }
    }
}


זו למעשה לולאה שמסתובבת ומחכה שיעבור הזמן שסיפקנו לפונקציה כפרמטר...

עכשיו תסתכלו שוב על הדוגמה הראשונה של BLINK עם ה-()delay ובלי ה-()delay. בגדול מה שהם עשו זה להעביר את הלולאה של המתנה מתוך ה-()delay לתוך הקוד בפונקציית ()loop

נחזור למה שהתחלתי ממנו... אני רגיל לעבוד עם אירוע כלשהו שמעיר את הבקר, לבצע את מה שצריך, והולך לישון שוב עד לאירוע הבא. התעוררות של הבקר בדרך כלל מתרחשת כשמגיעה פסיקה (Interrupt) ונקראת פונקציה מיוחדת שמטפלת בפסיקה (ISR - Interrupt Service Routine).

אילו אירועים יש בדוגמה הפשוטה שניסיתי להדגים לכם? 1 - לחיצה על כפתור, 2 - זמן.

אפשר לקבל פסיקה ברגע שלוחצים על כפתור שמחובר לכניסה דיגיטלית. ראו ()attachInterrupt. ל-UNO יש שתי כניסות שמייצרות פסיקות כתוצאה של שינוי מצב.

אבל מה עם הזמן? אני רוצה לקבל פסיקה כל שניה כדי לשנות את מצב הלד!

איפה השעון שלי?

ובכן, אני לא מכיר מיקרובקר או מחשב שאין לו שעון (Timer) שאפשר לדרוך כדי לקבל פסיקה או חיווי אחר של אירוע אחרי שפוקע הזמן שביקשתי... אז למה לארדואינו אין את זה?

חיפשתי במדריך שפת התכנות של סביבת ארדואינו, בפיסקה של זמן יש רק את פונקציות ה-()delay וכאלה שמחזירות את הזמן שעבר במיקרו ומילישניות. הסתכלתי על הספריות הרשמיות - אין. תרשו לי לדלג על הספריות הלא רשמיות מהסיבות שהזכרתי קודם.

לא יכול להיות שלמיקרובקר של ארדואינו UNO אין טיימרים... פותח את המפרט של ATMega328, ורואה שיש לו 3 טיימרים פנימיים. 2 של 8 ביט ואחד של 16 ביט. יש גם Watchdog timer שאפשר לנצל לכל מני דברים.

חופר קצת ברשת ומגלה שכל שלושת הטיימרים כבר בשימוש ע"י דברים שונים.

Timer0

זהו טיימר של 8 ביט, עם מנגנון חלוקת תדר עד 1024 פולסים, כלומר הוא יכול לספור עד 1024x255 פולסים. בתדר של 16MHz בו פועל הבקר של UNO אפשר לקבל מטיימר כזה פסיקה עם זמן מקסימלי של 16.32 מילישניות. 

טיימר זה משמש את הסביבה לספירת הזמן שעבר כדי שפונקציות הזמן ()micros ו-()millis יעבדו. פונקציות ה-()delay מתבססות על הערכים של פונקציות אלה.

נשמע חשוב, אבל אם עוברים לעבוד עם פסיקות לפי זמן ולא לשבת ולחכות שיעבור זמן כלשהו, אז לא באמת צריכים את המנגנון הזה. רק ש-16mSec לא כל כך שימושי למקרה בו אנחנו רוצים להבהב בלד כל שניה.

Timer1

זהו טיימר של 16 ביט, גם עם מנגנון חליקת תדר עד 1024 פולסים, מה שאומר שהוא יכול לספור עד 1024x65535 פולסים, שזה 4.19 שניות. אלה כבר ערכים הגיוניים לצרכים שלנו. זהו המועמד לבדיקה איך אפשר מנצל אותו לטובת "שינה" של הבקר בזמן שלא צריכים אותו.

טיימר זה בשימוש ע"י ספריית Servo. משהו שאפשר כנראה לוותר עליו ולממש בצורה אחרת אם אצליח לקבל את הסביבת התכנות שאני רוצה.

Timer2

זהו טיימר של 8 ביט, כמו Timer0, כך שגם הוא קצר מדי לטובת השימוש שאני מחפש. טיימר זה בשימוש ע"י פקודת ()tone.

בנוסף לשימושים שהזכרתי, כל שלושת הטיימרים משמשים ליצירת פולסים על קווי ה-PWM ה"חומרתיים" של הבקר. אם אשתמש ב-Timer1 לטובת המזימות שלי, קווי ה-PWM מספר 9 ו-10 יפסיקו לעבוד. טוב, נו... יש עוד 4 יציאות אחרות...

תכנון ה-Framework

Framework אולי נשמעת כמו מילה בומבסטית, אבל כל מה שאני רוצה זה שפונקציות שלי יקראו כשיש אירוע כמו שינוי מצב של הכניסות הדיגיטליים, כשפוקע טיימר, כשיש נתונים שאפשר לקרוא בערוצי התקשורת השונים (UART, I2C, SPI).

קוד/ספריה/מחלקה כלשהי, שאני יכול לספק לה מצביע לפונקציה שלי, שתקרא במקרה של אירוע.
לגבי פסיקות על הקווים הדיגיטליים זה לא אמור להיות מסובך כי זה כבר קיים עם פונקציית ()attachInterrupt.

לגבי ערוצי תקשורת עדיין לא בדקתי לעומק. די בטוח שיש פסיקה חומרתית שקופצת כשמגיע משהו דרך הערוץ, אבל גם די בטוח שהסביבה כבר התלבשה על הפסיקה הזו כדי לקרוא את הנתונים שהגיעו ולהעתיק אותם לחוצץ (buffer) זמני, כך שלא בטוח אם אפשר יהיה להשתמש בפסיקה... אוףףף...

בינתיים הייתי סקרן לראות אם אני יכול "להרדים" את הבקר בזמן שאין בו צורך ולהעיר אותו עבור זמן מה עם טיימר מספר 1. את הקוד שמטפל בזה כבר מחקתי מרוב יאוש, אבל נעזרתי במדריך המעולה של Nick Gammon שמצאתי ברשת, שהוא גורו של תכנות מיקרובקרים.

כתבתי את הקוד שמנהל מערך של טיימרים, מאפשר רישום של פונקציה שצריך לקרוא כשהטיימר פוקע (Callback function), מחשב את הזמן המינימלי שאפשר לישון לפני שהשעון הקרוב ביותר צריך לייצר אירוע. כל זה התלבש על הפסיקה של Timer1 ועבד בצורה יפה מאוד עם כמה טיימרים שבדקתי.

הגעתי לשלב שצריך להרדים את הבקר בזמן שלא צריכים אותו. שוב בדקתי במדריך שפת התכנות של סביבת הארדואינו - שום דבר שמתייחס למצבי שינה של הבקר. ולא שחסרו לו מצבים כאלה. בדקתי ברשימת הספריות הרשמיות - כלום. חיפשתי עוד קצת והגעתי למדריך שנראה רשמי, מאוד מושקע בנושא זה ומשם לספריית LowPower שנראת כמו ספריה רשמית, אבל מופיעה ברשימות של ספריות נוספות בנושא Device Control. שמחתי לרגע... אבל... הספריה מיועדת למיקרובקרים ממשפחת SAMD ו-NRF52, מה שאומר שהיא לא מתאימה לכרטיסי Arduino UNO שמבוססים על מיקרובקר ATMega328. די מאכזב...

לא נורא, יש את המדריך המעולה של Nick Gammon שאפשר להסתמך עליו.

השתמשתי במכשיר החמוד הזה שמראה בזמן אמת את צריכת הזרם דרך חיבור USB:

הרצתי את הקוד על כרטיס Arduino UNO PLUS של חברת WaveShare שמצאתי במגירה:

בריצה רגילה של הקוד (שכבר מחקתי, לכן לא יכול להראות מה הרצתי), שמשתמש במנגנון הטיימרים שלי, מדדתי צריכה לשל 68mA.
הכנסתי את הבקר למצב שינה SLEEP_MODE_IDLE ואני רואה שהבקר לא נרדם וממשיך להדפיס דברים דרך ה-Serial. מוזר...
נזכרתי ש-Timer0 שמשמש לספירת הזמן רץ כל הזמן, אז הוא מן הסתם מעיר את הבקר ממצב השינה מאוד מהר. אין בעיה. עצרתי את Timer0 לצורך הבדיקה. עכשיו הבקר נרדם כמו שציפיתי והמכשיר מראה צריכה של 60mA. חסכון של 11.7%... נחמד...

הגיעה הזמן לנסות להכניס את הבקר למצב שינה עמוק וחסכוני יותר, SLEEP_MODE_PWR_SAVE שאמור להשאיר את הטיימרים רצים ולהתעורר ממצב שינה זה כשטיימר פוקע.
צורב, מריץ... לא עובד... בודק כל מה שאפשר... קורא שוב את המפרט בצורה יותר מרוכזת ורואה שרק Timer2 יכול להעיר את הבקר ממצב שינה זה... אוףףף... כל העבודה הולכת לפח. אין יותר מדי הגיון להרדים את הבקר למקסימום של 16.32 מילישניות כדי לנסות לחסוך משהו.

באותה הזדמנות רציתי לראות מה תהיה הצריכה אם הבקר ישן במצב הכי עמוק: SLEEP_MODE_PWR_DOWN. המכשיר הראה 45mA. חסכון, אבל לא ברמה שאפשר להריץ את זה ככה מסוללות לאורך זמן. לדים, מייצבי מתח ודברים נוספים על המעגל צורכים לא מעט זרם, כך שבמצב הזה אני לא רואה הגיון להתעקש ולהמשיך לנסות להוסיף מצבי שינה ב-Framework שאני מנסה לייצר. זו הנקודה שמחקתי את הקוד שניסה להשתמש ב-Timer1 לטובת מצבי שינה של הבקר.

אבל עוד לא אבדה תקוותי!
יש עוד אפשרות אחת שלא ניסיתי, ה-Watchdog timer! לפי המפרט של ATMega328 הטיימר הזה יעיר את הבקר מכל מצב שינה, גם מהעמוק ביותר. המגבלה היא שאפשר להפעיל את הטיימר הזה לפרקי זמן מאוד קבועים:
"טבלת זמנים של Watchdog timer"

עדיין אפשר יהיה להרדים את הבקר לזמנים ארוכים יחסית, אולי בכמה מחזורים ולא בניסיון אחד. צריך לא לשכוח לטפל בספירת הזמן ש-Timer0 מעדכן. כשהבקר ישן, הטיימר הזה לא אמור לרוץ, כך שברגע ההתעוררות צריך יהיה לעדכן את המשתנים שהיו מתעדכנים אם הטיימר הזה היה רץ. ההתעוררות יכולה לקרות לא רק בגלל פקיעת ה-Watchdog timer, אלא גם בגלל אירועים נוספים, כך שצריך יהיה לחשב כמה זמן הבקר באמת ישן כדי לעדכן את הנתונים. כל זה כדי שהקוד שמשתמש בפונקציות הזמן ()micros ו-()millis ימשיך לעבוד. לא להשתמש ב-()delay, זוכרים?!

מה עם הצריכת הזרם הגבוהה של הכרטיס עצמו? מי שירצה פרוייקט חסכוני באמת יוכל להשתמש בכרטיסים מינימליסטיים יותר, כמו ה-Pro Mini:

או לבנות פרוייקט על בסיס שבב המיקרובר לבדו, כמו ש-Nick Gammon עשה במדריך שלו:


האם זה הסוף לניסיון שלי לייצר את ה-Framework שאני רגיל אליו? ממש לא! אני עדיין רוצה לקבל סביבה בה יקראו לפונקציית callback שלי כשקורה אירוע שנרשמתי אליו. 

טיימרים כבר יש לי.

טיפול בפסיקות משינוי של כניסות דיגיטליות לא תהיה בעיה להוסיף. ידעתם של-UNO יש רק 2 קווים כאלה שתומכים בפסיקות? קווים 2 ו-3.
אם כבר ויתרתי על מצבי שינה לבינתיים, אז אני יכול להוסיף תמיכה דומה גם לקווים אחרים! פשוט לדגום את הקו הרצוי עם ()digitalRead ולהשוות לדגימה הקודמת. אפשר להוסיף גם תמיכה לזיהוי שינוי בכניסה אנלוגית באותה הצורה! כל זה חבוי מהעין ולא דורש טיפול כלשהו. אפשר יהיה לספק פונקציה שתקרא כשיש שינוי גדול יותר מהאחוז שציינתם בכניסה אנלוגית כלשהי... יותר פשוט מזה?

עבודה עם פסיקות

עוד תחום אחד שהקוד שאני כותב יטפל - עבודה נכונה עם פסיקות.

בכל מקום שיסביר על העבודה עם פסיקות במיקרובקר יהיה רשום שאסור לבצע תהליכים ארוכים בתוך הפסיקה. צריך לעשות משהו מהיר, כמו שינוי משתנה גלובלי, העתקה של מידע למקום כלשהו ולצאת מפונקציית ה-ISR כמה שיותר מהר. למה? כי ברגע שאתם בתוך ה-ISR שום פסיקה אחרת לא יכולה להתבצע, מה שאומר שתהליכים שבדרך כלל מתבצעים ברקע לא יתבצעו. 

אחת הדוגמאות היא כתיבה ל-Serial. בסביבת Arduino יש buffer בגודל של 64 בתים לטובת תקשורת טורית. כשאתם כותבים משהו לערוץ התקשורת עם ()Serial.print או פונקציות אחרות של מחלקה זו, הנתונים שאתם מעבירים מועתקים ל-buffer הזה ומופעל מנגנון ששולח את הנתונים שיש ב-buffer לחומרה של ערוץ התקשורת byte אחרי byte. בתהליך הזה מגיעה פסיקה אחרי משלוח של כל byte וכך הבקר יודע שהוא צריך לשלוח byte הבא.
אם אתם כותבים ל-Serial מתוך ה-ISR שלכם, הפסיקה של מנגנון התקשורת לא מטופלת וה-buffer לא מפונה, כך שבשלב מסויים הוא יכול להתמלא והמידע ילך לאיבוד.

ה-Framework יכלול מנגנון שיהיה מסוגל להעביר הודעות מפונקציות ISR לריצה הרגילה של התוכנית בצורה יעילה. המנגנון הוא בסך הכל חוצץ מעגלי (Circular Buffer) שאפשר יהיה להכניס לתוכו הודעות בפונקציית ISR ולשלוף אותם בפונקציה של ה-Framework שתקרא מתוך ה-()loop של התוכנית הרגילה כדי לבצע את התהליכים שלה.
ההודעות יסמנו איזו פונקציית Callback צריך לקרוא בעקבות האירוע שקרה.

סיכום

אז מה היה לנו?

אכזבה מזה שאין טיימרים בסביבת ארדואינו. ולא שאין תמיכה חומרתית לזה, פשוט החליטו להשתמש בהם לדברים אחרים (כמו ספריית Servo). היו יכולים לייצר את המנגנון שאני כותב עכשיו אם כבר...

אכזבה שאין תמיכה רשמית במצבי שינה של מיקרובקר, לפחות לא של ה-UNO הפופולרי ביותר.

הקוד שיש לי כרגע מספק תשתית של טיימרים, מעבר הודעות בין פונקציות המטפלות בפסיקות (ISR) לבין הקוד שרץ במצב הרגיל, מספק סביבת פיתוח שמבוסס על אירועים, שקוראות לפונקציה שסיפקתם כדי לטפל באירוע.

ככה נראת תוכנית ה-BLINK עם טיימרים:
קוד: בחר הכל
#include <Arduino.h>
#include "EBF_Logic.h"

enum {
    LED_TIMER = 0,

    NUM_OF_TIMERS
};

EBF_Logic ebf;
uint8_t ledState;

void onLedTimer()
{
    ledState = !ledState;
    digitalWrite(LED_BUILTIN, ledState);

    ebf.StartTimer(LED_TIMER, 1000);
}

void setup()
{
    ebf.Init(2, NUM_OF_TIMERS);

    ebf.InitTimer(LED_TIMER, onLedTimer);
    ebf.StartTimer(LED_TIMER, 1000);

    pinMode(LED_BUILTIN, OUTPUT);
    digitalWrite(LED_BUILTIN, LOW);
    ledState = LOW;
}

void loop()
{
    ebf.Process();
}


שזה קצת יותר קוד בהשוואה ל-BLINK עם ה-()delay.

בהמשך אוסיף טיפול בשינויים על הקווים הדיגיטליים (גם מבוססי פסיקות וגם בצורה של polling על הקווים). אולי גם לקווים האנלוגיים כדי להשלים את התמונה. ואז הקוד עם לד מהבהב וטיפול בכפתור שמדליק לד נוסף יראה כך:
קוד: בחר הכל
#include <Arduino.h>
#include "EBF_Logic.h"

enum {
    LED_TIMER = 0,

    NUM_OF_TIMERS
};

const int buttonPin = 2;    // the number of the pushbutton pin
const int buttonLedPin = 3; // the number of the LED pin

EBF_Logic ebf;
uint8_t ledState;

void onLedTimer()
{
    ledState = !ledState;
    digitalWrite(LED_BUILTIN, ledState);

    ebf.StartTimer(LED_TIMER, 1000);
}

void onButtonChange()
{
    uint8_t buttonState;

    buttonState = digitalRead(buttonPin);

    digitalWrite(buttonLedPin, buttonState);
}

void setup()
{
    ebf.Init(2, NUM_OF_TIMERS);

    ebf.InitTimer(LED_TIMER, onLedTimer);
    ebf.StartTimer(LED_TIMER, 1000);

    ebf.SetDigitalInputChange(buttonPin, onButtonChange);

    pinMode(buttonLedPin, OUTPUT);
    pinMode(buttonPin, INPUT);

    pinMode(LED_BUILTIN, OUTPUT);
    digitalWrite(LED_BUILTIN, LOW);
    ledState = LOW;
}

void loop()
{
    ebf.Process();
}


לי זה נראה הרבה יותר נקי מהדוגמה של BLINK בלי ()delay. סוף סוף אפשר להתרכז רק בלוגיקה של התוכנית ולא כל הבקרה שמסביב.

אגב, השם שנתתי לזה, ה-EBF הוא Event Based Framework. וזה בעצם בסיס לרעיון קצת יותר גדול שיוכל לפשט את מימוש הפרוייקטים הפשוטים, שכנראה אלה רוב הפרוייקטים שמשתמשים בכרטיסי Arduino. בתקוה שזה יעזור עוד קצת למתחילים לא להסתבך עם תוכניות שהם קצת יותר גדולות מהבהוב הלד. 

אחרי עוד קצת עבודה אפרסם את הקוד של הסביבה הזו ל-GitHub כדי שגם אחרים יוכלו להשתמש בה.

מה דעתכם על זה? האם צורת כתיבת קוד זו פשוטה יותר להבנה ומימוש? אשמח לתגובתכם לפוסט בפייסבוק.

הודעות נוספות:

  • בלוג כמה עולה לייצר כרטיס Arduino UNO?

    כמה עולה לייצר כרטיס Arduino UNO?

    2024-05-05 12:27:26

    הייתי סקרן לבדוק כמה עולה לייצר כרטיס פופולרי כמו Arduino UNO R3...
    כמובן שאין לי את כל המידע, כמו כמה זמן העבודה מושקע בייצור, עלות שעת עבודה, כמה חשמל צריך כדי לייצר כרטיס, כמה מהכרטיסים יוצאים תקינים (מה שנקרא yield), עלות החומרים השונים כמו בדיל, בלאי של המכונות ועוד הרבה מאוד דברים אחרים... אבל מה שכן אפשר למצוא זה עלות הרכיבים, לפחות כדי לקבל סדרי גודל...

  • בלוג שיפור משמעותי באתר - סינון מוצרים

    שיפור משמעותי באתר - סינון מוצרים

    2024-03-15 14:43:06

    אני גהה להציג שיפור (מאוד) משמעותי באתר - סינון מוצרים!
    הפיתוח נמצא בשלבי סיום סופיים, אבל הכל נראה עובד ואפילו מאוד יעיל להפתעתי...

  • בלוג תוכניות למערכת בית חכם DIY

    תוכניות למערכת בית חכם DIY

    2023-12-17 16:16:30

    זהו פוסט תיאורטי, סוג של "הצהרת כוונות", או רשימת TODO של מה שיושב לי בראש במשך שנים לגבי מערכת בית חכם שאני רוצה לבנות. כותב את זה בעיקר בשבילי כדי לא לשכוח דברים כי בזמן האחרון אני מרגיש שאני קצת מתפזר בין כמה פרויקטים, אבל גם מי שעוקב אחרי הפוסטים שלי יוכל ללמוד, להגיב ואולי גם לשנות את כיוון המחשבה שלי.

  • בלוג dual boot - הצלחה

    dual boot - הצלחה

    2023-10-05 18:17:07

    זהו פוסט רביעי בדרך ל-dual boot ואפשרות לעדכן תוכנה מרחוק. פוסט זה מסיים את סדרת ה-dual-boot אחרי שהצריבה הצליחה והכל עובד.

  • בלוג המסע לחיפוש ערכת אלקטרוניקה

    המסע לחיפוש ערכת אלקטרוניקה

    2023-08-14 09:13:50

    מאז שאני זוכר את עצמי, תמיד פירקתי דברים כדי לראות איך הם בנויים. הייתי אוגר חלקים ודברים שימושיים בארון שהיה לנו במרפסת. הייתי אומר שאפשר לקרוא לי מייקר (Maker) מגיל מאוד מאוד צעיר...
    אם ישאלו אותי מה משך אותי לעולם האלקטרוניקה, בלי לחשוב אפילו שניה אני יכול להצביע בבירור על ערכת אלקטרוניקה שהיתה לי איפה שהוא בגיל 10-14...