אז הנה עוד דוגמה מהימים האחרונים...
בפרוייקט שאני עובד עליו בימים אלה הייתי צריך לבדוק שערוצי התקשורת (I2C, SPI ו-UART) עובדים כמו שצריך.
לקחתי את מסך הטקסט הזה של SparkFun, שאפשר לתקשר איתו דרך כל שלושת הערוצים:
הכל עבד כמו שצריך וכשלב הבא הייתי צריך לכתוב "דרייבר" למסך זה בסביבה שלי (לא נכנס לפרטים למה), אז פתחתי את הקוד של הספריה שהם מספקים כדי לראות מה הם עשו שם ולהעתיק את הקוד לסביבה שלי...
זה קוד שמסופק ע"י היצרן של המוצר - SparkFun בכבודו ובעצמו, לא איזה מישהו מתחיל, כן?
הכל עובד כאמור, השאלה האם הספריה עושה את מה שהיא צריכה לעשות בצורה יעילה?
בואו נראה...
פונקציה הבסיסית לניקוי מסך:
- קוד: בחר הכל
/*
* Send the clear command to the display. This clears the
* display and forces the cursor to return to the beginning
* of the display.
*/
void SerLCD::clear()
{
command(CLEAR_COMMAND);
delay(10); // a little extra delay after clear
}
פונקציה מאוד פשוטה, שולחת פקודת CLEAR דרך פונקציה נוספת, ו... תוקעת את הכל ל-10mSec... למה?!
יכול להיות שהמסך צריך זמן לבצע את הפעולה, אני יכול להבין את זה. אבל כל כך הרבה זמן? ולמה לתקוע את הכל?
בזמן הזה הבקר היה יכול לעשות דברים אחרים שלא קשורים למסך.
מה גם, המסך הזה למעשה כולל מיקרובקר ATmega328P שאיתו מתקשרים דרך ערוצי התקשורת השונים, והוא מתפעל את התצוגה דרך בקר LCD מדגם HD44780.
היה אפשר לעשות תור של פקודות בתוך ה-ATmega328P, לחכות שם אם צריך ולבצע פקודה הבאה אם שלחו אותה לפני שהתצוגה מוכנה. אני גם בטוח שבקר LCD ה-HD44780 כולל איזה שהוא מנגנון חומרתי שמציין מתי הוא מוכן לקבל פקודה הבאה... כמה שניות של רפרוף במפרט הטכני שלו והנה, בבקשה:

אבל רגע, זה עוד לא הכל. בוא נראה מה פונקצייה
()command
שקוראים לה מה-()clear
עושה:- קוד: בחר הכל
/*
* Send a command to the display.
* Used by other functions.
*
* byte command to send
*/
void SerLCD::command(byte command)
{
beginTransmission(); // transmit to device
transmit(SETTING_COMMAND); //Put LCD into setting mode
transmit(command); //Send the command code
endTransmission(); //Stop transmission
delay(10); //Hang out for a bit
}
נו באמת... עוד תקיעה של 10mSec!
ומתחתיה פונקציה אחרת לשליחת פקודות מיוחדרת ש...
- קוד: בחר הכל
/*
* Send a special command to the display. Used by other functions.
*
* byte command to send
*/
void SerLCD::specialCommand(byte command)
{
beginTransmission(); // transmit to device
transmit(SPECIAL_COMMAND); //Send special command character
transmit(command); //Send the command code
endTransmission(); //Stop transmission
delay(50); //Wait a bit longer for special display commands
}
תוקעת את הכל ל-50mSec!
וכן, בדקתי את הפונקציות שקוראים להן מתוך שתי הפונקציות האלה, במקרה של תקשורת I2C הן עושות את מה שמצפים מהן, אבל במקרה שמתקשרים עם הרכיב דרך SPI יש עוד
delay(10)
... ה-SPI קצת יותר בעייתי מה-I2C, אז נניח לו. אבל אם אני רוצה לעשות משהו עם המסך דרך חיבור ה-I2C או QWIIC, אז כמעט עם כל פניה הבקר תקוע בין 20 ל-50 מילישניות.וזה לא רק עם פקודות מיוחדות, זה גם עבור כל כתיבה למסך:
- קוד: בחר הכל
/*
* Write a byte to the display.
* Required for Print.
*/
size_t SerLCD::write(uint8_t b)
{
beginTransmission(); // transmit to device
transmit(b);
endTransmission(); //Stop transmission
delay(10); // wait a bit
return 1;
} // write
ככה זה גם עבור כתיבת מחרוזת של תווים, תוספת של 10 מילי-שניות עבור כל קריאה לפונקציית גישה למסך.
אתם יכולים להגיד, נו, אפשר לחשוב, זה רק 10 מילי... אז בואו נעשה חישוב כמה הבקר שלכם תקוע בפעולה הסטנדרטית של ניקוי מסך, כתיבת שורה ראשונה, מעבר לשורה שניה וכתובה נוספת:
ניקוי - 10+10
כתיבה - 10
מעבר שורה - 50
כתיבה - 10
סה"כ: 90mSec תקיעת הבקר
פלוס זמן שלוקחת התקשורת דרך ה-I2C, אבל היא תהיה די דומה לכל המסכים.
בהצלחה לכם אם אתם רוצים לתזמן משהו בצורה מדוייקת... שלא לדבר על פרוייקטים שצריכים לעבוד על סוללות ולחסוך כמה שיותר זרם...
היית סקרן לראות מה אחרים עשו עם זה, אז לקחתי את המסך של SeeedStudio שגם מתקשר דרך I2C כדי לראות מה הם עושו בתוך הקוד שלהם:
המסך הזה מבוסס על בקר LCD מדגם AIP31068 שיודע לתקשר דרך I2C ומממש (נראה לי) את כל הפונקציונליות שמסך של SparkFun מציע. וגם הקוד נראה קצת דומה, וגם ערכי הדגלים וה-API הפנימי מאוד זהה... כך שאני מניח שמישהו השתמש בקוד של מישהו אחר כבסיס והמשיך משם.
אגב, מסכים דומים של WaveShare מבוססים על אותו הבקר, כך שכנראה שזה משהו די סטנדרטי, כמו שהבקר HD44780 הוא דה-פאקטו הסטנדרט למסכי 16x2 עם חיבור מקבילי.
אז איך הקוד של SeeedStudio נראה?
פונקציית ניקוי מסך מחכה רק 2 מילי-שניות:
- קוד: בחר הכל
void rgb_lcd::clear() {
command(LCD_CLEARDISPLAY); // clear display, set cursor position to zero
delayMicroseconds(2000); // this command takes a long time!
}
לא להיבהל, ה-2000 זה במיקרו-שניות.
לפקודת מעבר שורה אין בכלל השהיה:
- קוד: בחר הכל
void rgb_lcd::setCursor(uint8_t col, uint8_t row) {
col = (row == 0 ? col | 0x80 : col | 0xc0);
unsigned char dta[2] = {0x80, col};
i2c_send_byteS(dta, 2);
}
גם לא לכתיבת טקסט:
- קוד: בחר הכל
inline size_t rgb_lcd::write(uint8_t value) {
unsigned char dta[2] = {0x40, value};
i2c_send_byteS(dta, 2);
return 1; // assume sucess
}
פונקציית
i2c_send_byteS
רק שולחת את הנתונים בלי לתקוע את הבקר.אז המצב הרבה יותר טוב ממה שקורה בקוד של SparkFun.
ההשהיה מממשת בצורה עיוורת את מה שרשום במפרט, שיש כמה פקודות שלוקחות זמן:

לפקודות שלוקחות באיזור ה-40uSec כנראה שלא צריך השהיות כי התקשורת לוקחת בערך את אותו הזמן, פלוס זמן ביצוע של הלוגיקה שלכם במיקרובקר וכבר אין טעם להשהות את הביצוע.
ועדיין... היה אפשר לשפר את זה עוד יותר...
לבקר מסך AIP31068 יש דגל Busy שאפשר לקרוא כדי להבין אם המסך מוכן לקבל פקודה (את זה מצאתי במסמך מפרט אחר, ולא זה ש-SeeedStudio מפרסם):

אז לפני שתי הפקודות הראשונות בטבלה, שלוקחות 1.53mSec, אפשר לקרוא את הרגיסטר עם דגל ה-Busy כדי להבין אם המסך מוכן לפקודה הבאה ואז לשלוח אותה. ואם לא מוכן, לחכות מילי-שניה ולנסות שוב.
קריאת רגיסטר של 8bit דרך ערוץ ה-I2C לוקח פחות מ-100uSec וברוב המקרים כנראה שהמסך בכל מקרה יהיה מוכן לקבל פקודה, כך שחסכנו בערך 1.9mSec מזמן הריצה של הבקר.
מה אתם אומרים על זה? האם אתם כותבים דרייברים משלכם? או מסתמכים על מה שאתם מוצאים באינטרנט?
אשמח לתגובות לפוסט בפייסבוק.