حال دوباره به برنامه “Hello World” برمیگردیم، اما این بار از برد Nucleo استفاده میکنیم که چند چالش را به همراه دارد. اولین چالش مکان نوشتن پیام است. روی این برد هیچ نمایشگری وجود ندارد. خوشبختانه، این تراشه یک پورت سریال دارد که به یک پورت سریال/USB در نیمهی بالایی برد متصل شده است.
چالش بعدی، خود نوشتن است. باید ابتدا دستگاه را راهاندازی کرده و سپس روشی برای ارسال واقعی یک کاراکتر به آن پیادهسازی کنیم. این دستگاه برای دریافت/ارسال کاراکترها به صورت تکی طراحی شده است و ما باید این محدودیت را هنگام نوشتن برنامهی خود در نظر داشته باشیم.
قبل از کار با دستگاه، این فرآیند را شبیهسازی خواهیم کرد. زبان C دارای مجموعهی زیادی از توابع استاندارد مانند puts است که خروجی گرفتن دادهها را آسان میکند (در سیستم عامل این خروجی به ترمینال یا کنسول ارسال میشود اما در سیستمهای امبدد توابع استاندارد ورودی خروجی C یا همان stdout یا در دسترس نیستند و یا اگر باشند رفتارشان متفاوت است) . برد Nucleo چنین ویژگیهای جالبی ندارد، بنابراین باید توابع خروجی خود را بنویسیم. برای اینکه به برنامهنویسی سطح پایین مورد نیاز برای برد Nucleo برسیم، برنامه “Hello World” را کاراکتر به کاراکتر خواهیم نوشت.
زمانی که یک برنامه C تابع استاندارد puts را فراخوانی میکند، فرایند برنامهنویسی طولانیای را استارت میزند که شامل فراخوانیهای کرنل، بافر کردن داخلی، زمانبندی وقفه (interrupt scheduling) و درایورهای دستگاه میشود (در فصل بعد بیشتر در مورد آنها صحبت خواهیم کرد). در نهایت، به نقطهای میرسد که دادهها را کاراکتر به کاراکتر بسوی دستگاه ارسال میکند. برای شبیهسازی این فرآیند، ما هر بار یک کاراکتر را به سیستمعامل ارسال میکنیم. به عبارت دیگر، خودمان را به استفاده از تابع استاندارد putchar برای نوشتن خروجی محدود میکنیم.
کد ۹-۱ برنامهای را نشان میدهد که عبارت “Hello World\n” را به روش سختی چاپ میکند. تکرار میکنم، ما این کار را به روش سختی انجام میدهیم چون بعداً، برای کار با برد Nucleo، مجبور خواهیم شد آن را به روشی پیچیده تری انجام دهیم.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | putchar.c /* چاپ یک رشته کاراکتر به کاراکتر */ #include <stdio.h> char hello[] = "Hello World\n"; کاراکترهایی که باید چاپ شوند int curChar; // // شماره کاراکتری که در حال چاپ هستیم int main() { for (curChar = 0; hello[curChar] != '\0'; ++curChar) putchar(hello[curChar]); return (0); } |
کد مربوط به نوشتن رشته کاراکتر به کاراکتر
تنها بخش جالب در این برنامه حلقه for است که بعد از تعداد مشخصی از کاراکترها متوقف نمیشود. در عوض، زمانی متوقف میشود که برنامه به کاراکتر پایان رشته (‘\0‘) برسد. به این ترتیب، برنامه میتواند هر طول رشتهای را خروجی دهد.
برای بهبود این برنامه، ابتدا curChar را به یک متغیر محلی تبدیل میکنیم. سپس تابعی به نام myPutchar تعریف میکنیم که کاراکتر را به خروجی استاندارد ارسال میکند (به کد ۹-۲ مراجعه کنید).
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 | putchar.c /* یک رشته را کاراکتر به کاراکتر چاپ کند. با استفاده از تابع اختصاصی خودمان */ #include <stdio.h> char hello[] = "Hello World\n"; // کاراکترهایی که باید چاپ شوند 1 /** putcharپیاده سازی مجدد @param ch کاراکتری که باید ارسال شود @note به نظر بیفایده نمیرسد */ 2 void myPutchar(const char ch) { // 3 putchar(ch); } int main() { int curChar; // شاخص کاراکتر جاری که در حال چاپ هستیم for (curChar = 0; hello[curChar] != '\0'; ++curChar) 4 myPutchar(hello[curChar]); return (0); } |
کد مربوط به تک تک کاراکترها با استفاده از تابع خروجی اختصاصی خودمان
در ابتدای تابع myPutchar، عناصر اضافی ۱ را به بلاک کامنت اضافه کردهایم. کلیدواژه @param نشانگر یک پارامتر است و کلیدواژه @note یک یادداشت را تعریف میکند. میتوانید از کلیدواژههای بسیار دیگری در کامنتهای به سبک Doxygen استفاده کنید، اما فعلاً برای سازگاری با کد موجود STM از اصول اولیه استفاده خواهیم کرد.
عملکرد واقعی با اعلام void myPutchar(const char ch) 2 شروع میشود که نشان میدهد procedure یعنی تابع myPutchar هیچ چیزی برنمیگرداند و یک پارامتر از نوع char میگیرد. مقداردهنده const نشان میدهد که ما آن را درون رویه تغییر نمیدهیم. (در واقع، اگر تلاش کنیم مقدار این پارامتر را درون تابع تغییر دهیم، کامپایلر با تولید خطا جلوی آن را خواهد گرفت.)
هنگامی که رویه اجرا میشود ۴، برنامه مراحل زیر را انجام میدهد:
مجموعه مشابهی از مراحل هنگام فراخوانی putchar 3 اجرا میشود. تنها تفاوت این است که ما مجبور بودیم myPutchar را بنویسیم، و افرادی که کتابخانه استاندارد C را نوشتند، putchar را ارائه کردند.
ایجاد یک تابع (myPutchar) که فقط یک تابع دیگر (putchar) را فراخوانی میکند، چندان مفید نیست. برد Nucleo تابع putchar ندارد، بنابراین بعداً در این فصل تابع اختصاصی خود را مینویسیم. اما قبل از انجام این کار، اجازه دهید به جزئیات دستگاه سریال نگاه کنیم.
خروجی سریال یکی از سادهترین راهها برای خارج کردن داده از یک سیستم امبدد است. رابط الکتریکی آن از یک خط ارسال (TX)، یک خط دریافت (RX) و زمین (GND) تشکیل شده است. در بسیاری از سیستمهای امبدد، این خطوط در دسترس کاربر معمولی نیستند و فقط برای توسعهدهندگانی در دسترس هستند که تمایل دارند قاب را باز کنند و به پورت سریال وصل شوند.
میکروکنترلر ما یک دستگاه سریال دارد که میتوانیم در آن بنویسیم. تنها کاری که باید انجام دهیم، اتصال خطوط TX، RX و GND بین میکروکنترلر (نیمه پایین برد توسعه) و دستگاه USB/سریال در نیمه بالایی برد است.
جدول زیر اتصالات مورد نیاز ما را نشان میدهد:
دستگاه USB/سریال و سایر دستگاههای پشتیبانی | میکروکنترلر | ||
CN3-1 | TX | CN9-1 | RX |
CN3-2 | RX | CN9-2 | TX |
CN4-3 | GND | CN6-5 | GND |
دقت کنید که در یک ارتباط سریال همیشه دو دستگاه وجود دارد که میتوانند برای هم دیتا ارسال و از یکدیگر دیتا دریافت کنند و در اتصال این دو دستگاه به یکدیگر باید خطوط ارسال و دریافت به صورت ضربدری به همدیگر وصل شوند. یعنی در یک سمت RX به TX سمت دیگر وصل میشود و برعکس.
اگر یک Raspberry Pi یا سیستم امبدد دیگری (به غیر از nucleo) بدون کنترلر سریال داخلی داشته باشیم، باید این اتصالات را برقرار کنیم. شکل ۹-۱ چیدمان این قطعات و سیمکشی داخلی را، همانگونه که توسط STM طراحی شده، نشان میدهد.
STM از قبل اتصالات را برای ما برقرار کرده است. نیازی به جامپر (اتصالدهنده) نیست.
ارتباطات سریال روی برد Nucleo
ارتباطات سریال به زمانهای بسیار دور، به سالهای قبل از میلاد (یعنی قبل از رایانهها) باز میگردد. تلگراف، اینترنت زمان خودش بود و امکان انتقال پیامهای راه دور را از طریق سیم فراهم میکرد. فرستنده از یک کلید تلگراف تشکیل شده بود که با فشار دادن آن، گیرنده «کلیک» میکرد. کلیکها با استفاده از سیستمی به نام کد مورس (که هنوز هم امروزه استفاده میشود) رمزگذاری میشدند. این اختراع، انقلابی در ارتباطات ایجاد کرد. میتوانستید پیامی را به شهر دوری ارسال کنید و همان روز پاسخ دریافت کنید. (با این کار، پستهای اسبسوار را کنار گذاشتند!)
ولی، یک مشکل وجود داشت؛ درانتهای تلگرافها به اپراتورهای ماهری نیاز داشتید که کد مورس را بلد باشند. افراد بدون مهارت نمیتوانستند پیام ارسال یا دریافت کنند و آموزش اپراتورها پرهزینه بود.
یک راه حل استفاده از دو ساعت بود: یکی برای فرستنده و دیگری برای گیرنده. روی صفحه ساعت حروف الفبا از A تا Z نوشته شده بود. به عنوان مثال، برای ارسال حرف S، فرستنده صبر میکرد تا عقربهی تکی ساعت به حرف S اشاره کند و سپس کلید تلگراف را فشار میداد. گیرنده میدید که عقربه به S اشاره کرده و حرف را ثبت میکرد.
با این حال، همگام نگه داشتن ساعتها تقریباً غیرممکن بود، بنابراین یک مخترع بسیار باهوش تصمیم گرفت که هر ساعت عقربههای خود را در موقعیت بالا متوقف کند. هنگامی که فرستنده میخواست حرفی ارسال کند، کلید تلگراف را به عنوان سیگنال شروع فشار میداد. ساعتها زمان را به اندازهای دقیق نگه میداشتند که یک دور کامل را به درستی روی صفحه بچرخند. سپس فرستنده سیگنال حرف را فشار میداد. هنگامی که عقربه به بالا میرسید، مکث کوتاهی به نام زمان توقف، به ساعت کندتر فرصت میداد تا به آن برسد. توالی وقایع به این صورت بود: سیگنال شروع، سیگنال حرف، زمان توقف.
حالا بیایید به اختراع ماشین Teletype که میتوانست متن را از طریق خطوطی مشابه خطوط تلگراف ارسال کند، جهشی بزنیم. به جای یک پالس تکحرفی، Teletype کاراکترها را به مجموعهای از هشت پالس (هفت تا برای داده و یک پالس برای بررسی خطای ابتدایی) کدگذاری میکرد. این ماشین از یک رمزگذار صفحهکلید ساخته شده از اهرمهایی استفاده میکرد تا فشار یک کلید را به یک کد ۸ بیتی تبدیل کند. این کد با یک ثبت شیفت مکانیکی که شبیه درپوش توزیعکننده (distributor cap) بود، تغذیه میشد. دستگاه پالسها را از طریق سیم ارسال میکرد، جایی که یک تلکس دیگر آنها را به یک حرف چاپی واحد تبدیل میکرد.
روند کار Teletype به این شکل بود: فرستنده یک کلید را فشار میداد و فرستنده مکانیکی یک سیگنال ۱۰ بیتی (۱ بیت شروع، ۸ بیت داده و ۱ بیت توقف) ارسال میکرد. هنگامی که گیرنده بیت شروع را دریافت میکرد، ثبت شیفت خود (موتور دیگری با درپوش توزیعکننده) را روشن میکرد و از پالسهای ورودی برای چرخاندن سر چاپ استفاده میکرد تا حرف صحیح چاپ شود. پس از ارسال ۸ بیت داده، هر دو دستگاه برای حفظ همگامسازی، حداقل به مدت زمان ۱ بیت (بیت توقف) مکث میکردند.
اکثر Teletypeها میتوانستند کاراکترها را با سرعت ۱۱۰ باود (بیت بر ثانیه) یا ۱۰ کاراکتر در ثانیه ارسال کنند. این سرعت در عصر اتصالات اینترنتی مگابیتی زیاد به نظر نمیرسد، اما یک پیشرفت انقلابی در ارتباطات بود. رایانههای امروزی همچنان از ارتباط سریالی که Teletype استفاده میکرد، استفاده میکنند. سرعتها بهبود یافته است، اما پروتکل پایهای یکسان باقی مانده.
یکی دیگر از میراثهای ماشین تایپ Teletype، نحوه پایان دادن به خطوط است. پس از تایپ ۸۰ کاراکتر، میتوانستید کاراکتری به نام carriage return یا به اختصار CR را به دستگاه ارسال کنید تا سر چاپ را به ابتدای خط (موقعیت ۱) برگرداند. مشکل این بود که حرکت سر چاپ به ابتدای خط، دو دهم ثانیه طول میکشید. اگر بلافاصله بعد از CR کاراکتری ارسال میکردید، در اثر تلاش سر چاپ برای چاپ همزمان با حرکت، لکهای تار روی خط چاپ میشد.
مهندسان Teletype برای حل این مشکل، پایان خط را به دو کاراکتر تبدیل کردند. کاراکتر اول، CR، سر چاپ را به موقعیت ۱ منتقل میکرد. کاراکتر دوم، line feed یا به اختصار LF، کاغذ را یک خط به بالا میبرد. از آنجایی که LF چیزی روی کاغذ چاپ نمیکرد، انجام همزمان آن با حرکت سر چاپ به سمت چپ مشکلی ایجاد نمیکرد.
با ظهور کامپیوترها، هزینه ذخیرهسازی بسیار بالا بود (صدها دلار برای هر بایت). بنابراین، ذخیره دو کاراکتر برای پایان خط پرهزینه به نظر میرسید. توسعهدهندگان یونیکس (که الهامبخش لینوکس است) تصمیم گرفتند فقط از کاراکتر LF (و یا همان /n) استفاده کنند. اپل تنها از CR یا همان (\r) استفاده کرد و مایکروسافت برای پایان خط، هر دو کاراکتر CRLF و یا همان \r\n را با هم به کار گرفت.
زبان برنامهنویسی C بهطور خودکار انواع مختلف خطوط جدید را در کتابخانه سیستم مدیریت میکند، اما این فقط زمانی است که از کتابخانه سیستم استفاده کنید.اگر مستقیماً خودتان داده را ارسال میکنید (همانطور که ما در این پروژه انجام میدهیم)، باید کل توالی پایان خط (\r\n) را بنویسید.
امروزه، تقریباً هر پردازندهی امبددی یک رابط سریال روی خود دارد. ساخت دستگاههای سریال ساده و ارزان است. تنها تفاوت بین رابط امروز و رابط دهه ۱۸۰۰ این است که سرعت بالاتر رفته است (از ۱۱۰ بیت بر ثانیه به ۱۱۵۲۰۰ بیت بر ثانیه) و ولتاژها تغییر کردهاند. در دهه ۱۸۰۰ ,ولتاژ بین ۱۵- تا ۳- برای بیت صفر و بین ۳ تا ۱۵+ برای بیت یک استفاده میکردند. این هنوز هم “استاندارد” است، اما اکثر رایانهها از ولتاژهای ۰ (برای صفر) و ۳ (برای یک) استفاده میکنند.
دستگاهی که I/O سریال را مدیریت میکند، UART (فرستنده-گیرنده جهانی غیرهمزمان) نامیده میشود. دو نوع اصلی ارتباط سریال وجود دارد: غیرهمزمان (asynchronous ) و همزمان (synchronous ). در ارتباطات همزمان، کلاکهای فرستنده و گیرنده باید با ارسال مداوم کاراکترها توسط فرستنده، همگام شوند. سپس گیرنده به کاراکترهای ورودی نگاه میکند و زمانبندی کلاک را از آنها استنتاج میکند. فرستنده همیشه باید کاراکترها را ارسال کند، حتی اگر فقط یک کاراکتر «بیکار» (بدون داده) باشد.
در ارتباطات غیرهمزمان، هیچ کلاک مشترکی وجود ندارد. بیت شروع باعث میشود گیرنده کلاک خود را راهاندازی کند و به دنبال کاراکتر بگردد. ارتباط غیرهمزمان فرض میکند که فرستنده و گیرنده میتوانند کلاکهای خود را به اندازهی کافی برای زمان یک کاراکتر به هم نزدیک نگه دارند. از آنجایی که برای همگامسازی کلاکها نیازی به انتقال مداوم نیست، هیچ کاراکتر بیکاری وجود ندارد. در حالت بیکار، فرستنده فقط چیزی ارسال نمیکند.
تراشهی STM دارای یک پورت است که هم به ارتباطات همزمان و هم غیرهمزمان اجازه میدهد، بنابراین در مستندات STM، آن را USART (فرستنده-گیرنده جهانی همزمان/غیرهمزمان) خواهید دید. این برنامه از اصطلاح UART برای سازگاری با کتابخانه STM HAL استفاده میکند.
نویسنده شو !
سیسوگ با افتخار فضایی برای اشتراک گذاری دانش شماست. برای ما مقاله بنویسید.