در قسمت قبل با جزییات سختافزاری و نحوه کارکرد واحد USART تا حدودی آشنا شدیم. بررسی کردیم که نحوه ارسال داده توسط فرستنده USART چگونه صورت میگیرد. در این بخش میبینیم که نحوه دریافت اطلاعات توسط گیرنده USART نیز به فرآیند ارسال بیشباهت نیست. واحد USART میتواند بسته به مقدار بیت M در رجیستر USART_CR1، یک کلمه 8 یا 9 بیتی را دریافت کند. در دریافتکننده اطلاعات، تنها زمانی بیت start تشخیص داده میشود که یک الگوی خاص از نمونهها یا بیتها دریافت شود. این الگو باید به ترتیب (از راست به چپ) 1 1 1 0 X 0 X 0 X 0 0 0 0 باشد و اگر این رشته بهطور کامل دریافت نشود، عمل تشخیص بیت start لغو میشود و دریافتکننده به حالت idle (یا بیکار) برمیگردد. در این حالت هیچ پرچمی فعال نیست و گیرنده منتظر اولین لبه پایینرونده میماند.
کلاک بخش گیرنده نیز مانند فرستنده از همان واحد baud rate generator مشترک تأمین میشود. بااینحال، کلاکی که گیرنده دریافت میکند 16 مرتبه از کلاک فرستنده سریعتر است. بدین طریق تعداد نمونهگیری و دقت بیشتر (تا 16 نمونه) برای bit-time تضمین خواهد شد. در دیاگرام زیر نحوه تشخیص بیت start و شرایط دقیق موردنیاز برای تأیید دریافت بیت start، نشان دادهشده است:
در زمان دریافت اطلاعات توسط USART، دادهها روی پین RX و (مشابه ارسال داده) از بیت کمارزش شیفت پیدا میکنند. همچنین در این زمان، رجیستر RDR مانند یک بافر بین بأس داخلی و شیفت رجیستر دریافت اطلاعات، عمل میکند. این موضوع با نگاه کردن به دیاگرام نشان دادهشده در بخش قبل بهتر مشخص میشود:
برای دریافت داده توسط واحد USART مراحل زیر باید طی شوند:
- واحد USART با 1 کردن بیت UE در رجیستر USART_CR1، فعال میشود.
- طول کلمه، با مقدار دادن به بیت M در رجیستر USART_CR1، تنظیم میشود.
- تعداد بیت stop باید در رجیستر USART_CR2 تنظیم شود.
- (در صورت استفاده از ارتباط Multi buffer) DMA بهوسیله تنظیم رجیستر USART_CR3 تنظیم میشود (DMAR).
- baud rate موردنظر برای ارتباط، باید در رجیستر USART_BRR تنظیم شود.
- با یک کردن بیت RE در رجیستر USART_CR1، گیرنده فعال میشود و منتظر تشخیص بیت start میماند
هنگام دریافت یک کاراکتر، پرچم RXNE فعال میشود که نشاندهندهی انتقال اطلاعات از شیفت رجیستر به رجیستر RDR است. بهبیاندیگر فعال شدن پرچم RXNE به این معنی است که اطلاعات دریافت شده و آماده خواندن است. همچنین در این زمان، در صورت فعال بودن بیت RXNEIE، یک وقفه تولید خواهد شد.
در حالت multibuffer، پرچم RXNE بعد از دریافت هر بایت فعال، و با خواندن Data Register توسط DMA غیرفعال میشود. در حالت single buffer نیز، غیرفعال سازی بیت RXNE، بهوسیله خواندن نرمافزاری رجیستر USART_DR صورت میپذیرد. مانند بیت TXE، بیت پرچم RXNE را نیز میتوان با نوشتن صفر در آن پاک کرد. ذکر این نکته ضروری است که قبل از آنکه داده بعدی بهطور کامل دریافت شود، پرچم RXNE باید پاکشده باشد. در غیر این صورت، با خطای تداخل (overrun) مواجه میشویم.
در زمان دریافت اطلاعات، بیت RE نباید صفر شود، در غیر این صورت، عملیات دریافت بایت فعلی، لغو خواهد شد.
اکنونکه با مفاهیم مربوط به دریافت اطلاعات توسط واحد USART آشنا شدیم، میخواهیم به سراغ توسعه یک پروژه برویم و بهوسیله واحد USART موجود در میکروکنترلر STM32، از پورت سریال داده دریافت کنیم.
ایجاد پروژه
برای ایجاد پروژه این قسمت، مانند قسمت پیشین از تب Pinout & Configuration و بخش Connectivity، USART1 را انتخاب میکنیم. سپس باید از قسمت Mode گزینه Asynchronous را انتخاب کنیم. این بار از قسمت Parameter Settings، پارامتر Data Direction را روی حالت Receive and Transmit تنظیم میکنیم. بقیهی پارامترها ازجمله Buad Rate، طول کلمه و Parity را میتوانیم بدون تغییر رها کنیم.
همچنین یک پایه خروجی را برای یک LED چشمکزن تنظیم میکنیم. برای این منظور میتوانیم از همان LED متصل به پایه PC13 استفاده کنیم. اکنون میتوانیم پروژه را ایجاد کنیم و به سراغ فایل کد برویم.
نوشتن کد پروژه
در کتابخانه HAL، تابع مورد استفاده برای دریافت داده توسط واحد USART، به صورت زیر است:
همانطور که توضیحات تابع مشخص است، این تابع چهار پارامتر دارد که پارامتر اول اشارهگر به ساختار حاوی اطلاعات مربوط به تنظیمات ماژول UART، پارامتر دوم اشارهگر به بافر داده، پارامتر سوم طول داده و پارامتر چهارم مدتزمان Timeout است. این تابع مانند تابع HAL_UART_Transmit، در حالت blocking عملیات خود را انجام میدهد. به این معنی که پردازنده در زمان انجام این عملیات، تمامی عملیات دیگر را متوقف میکند و تا زمانی که مقدار داده مورد انتظار دریافت نشده، یا مدتزمان Timeout سپری نشده باشد، عملیات دیگری انجام نخواهد شد. این روند، به دلیل اینکه همه عملیات مهم دیگر را متوقف میکند، تنها در برنامهای قابلاستفاده است که فقط بخواهیم از ورودی داده دریافت کنیم. استفاده از این نوع دریافت داده، یعنی روش polling، علاوه بر ایرادی که به آن اشاره شد، مشکلات دیگری نیز دارد که در ادامه باهم بررسی میکنیم.
اکنون یک بافر برای نگهداری پیام ارسالی تعریف میکنیم:
1 | uint8_t message[10]; // Declaring a buffer of 10 bytes |
حالا میتوانیم با استفاده تابع معرفی شده برای دریافت داده با USART، اطلاعات را از پورت سریال دریافت کنیم. برای این منظور کد زیر را در حلقه while(1) مینویسیم:
1 2 3 4 5 6 7 8 | /* USER CODE BEGIN 3 */ HAL_UART_Receive(&huart1, message, 4, 100); // Receive 4 bytes of data via UART HAL_GPIO_TogglePin (GPIOC, GPIO_PIN_13); // Toggle LED HAL_Delay (250); // 250 ms Delay HAL_UART_Transmit(&huart1, message, 4, 4); // Attempt to send the received data HAL_UART_Transmit(&huart1, "\r\n", 2, 1); // New Line |
در این کد برای دریافت داده، 100 میلیثانیه زمان Timeout تعیین کردهایم. وضعیت LED متصل به پایه PC13 را نیز در هر بار اجرا معکوس میکنیم. به این طریق، میتوانیم تأثیر زمان Timeout در دستور دریافت داده را بر روی چشمک زدن LED ببینیم. بهبیاندیگر، به این طریق تأثیر یک دستور دریافت در حالت blocking، در کنار دستورات دیگر، دیده خواهد شد. بعد از دستورات مربوط چشمک زدن LED، اطلاعات بافر را توسط UART ارسال میکنیم تا در پورت سریال، داده دریافت شده را مشاهده کنیم. اکنون از پروژه build میگیریم و کد را روی میکرو دانلود میکنیم. در اینجا نیز مانند قسمت قبلی، برای نمایش اطلاعات و همچنین ارسال داده به پورت سریال، از نرمافزار RealTerm استفاده میکنیم.
در نرمافزار RealTerm از تب Port پورت COM موردنظر، که مبدل TTL to USB به آن متصل شده است را انتخاب میکنیم و Baud Rate را روی عدد 115200 تنظیم میکنیم. سپس باید روی کلید Change کلیک کنیم.
تنظیم ترمینال سریال RealTerm.
اکنون در تب Send، چهار کاراکتر یا بایت 1234 را در کادر مربوط به ارسال مینویسیم و سپس کلید Send ASCII را در مقابل کادر میزنیم.
ارسال داده به پورت سریال.
مشاهده میکنیم که تنها 1 بایت از 4 بایت ارسالی دریافت شده است. برای اینکه بتوانیم هر 4 بایت را دریافت کنیم، زمان Timeout را از 100 میلیثانیه به 1 ثانیه افزایش میدهیم. بدین منظور آرگومان مربوط به زمان Timeout را در دستور دریافت USART تغییر میدهیم:
1 | HAL_UART_Receive(&huart1, message, 4, 1000); // Receive 4 bytes of data via UART |
دوباره از کد، Build میگیریم و روی میکرو دانلود میکنیم. مجدداً از ترمینال سریال همان داده 1234 را به پورت ارسال میکنیم.
دریافت و نمایش 4 بایت در ترمینال سریال.
مشاهده میکنیم که این بار، هر 4 بایت داده دریافت شده است. اما با نگاه به LED چشمک زدن متوجه میشویم که سرعت چشمک زدن آن، به مقدار قابلتوجهای کاهشیافته است. دلیل این امر نیز Blocking بودن دستور دریافت و افزایش زمان Timeout آن به یک ثانیه است. برای اینکه از این مشکلات جلوگیری کنیم. باید از حالت Non-blocking برای دریافت اطلاعات استفاده کنیم. در ادامه این حالت را بررسی میکنیم.
تغییر تنظیمات پروژه
در صورت استفاده از وقفه برای انتقال اطلاعات، فرآیندهای دیگر تا اتمام کامل انتقال به تعویق نخواهند افتاد. پس بهعبارتدیگر با استفاده از وقفه، از حالت Non-Blocking بهره خواهیم گرفت.
اکنون فایل ioc. پروژه را برای تغییر پیکربندی باز میکنیم، بهمنظور فعال کردن وقفه USART، واحد USART1 را انتخاب میکنیم و از تب NVIC Settings، وقفه USART1 global را فعال میکنیم؛
اکنون مجددا به سراغ فایل کد میرویم.
تغییرات کد
خط کد مربوط به دریافت اطلاعات در حالت polling را کامنت میکنیم؛
1 2 | // // Receive data via UART in polling mode // HAL_UART_Receive(&huart1, message, 4, 1000); // Receive 4 bytes of data via UART |
همچنین خط مربوط به اعلان بافر را کامنت میکنیم تا بافر را بهصورت گلوبال و خارج از تابع Int main تعریف کنیم؛
1 | // uint8_t message[10]; // Declaring a buffer of 10 bytes |
1 | uint8_t message[10]; // Declaring a global buffer of 10 bytes |
قبل از حلقه while(1) دستور دریافت داده بهصورت وقفه را مینویسیم؛
1 2 3 | // Receive data via UART using interrupt HAL_UART_Receive_IT (&huart1, message, 4); /* USER CODE END 2 */ |
اکنون باید به سراغ تابع Rx Complete Callback برویم و دستور دریافت مجدد داده را در آن بنویسیم. زیرا در تکمیل دریافت داده این تابع صدا زده میشود و اگر بخواهیم دریافت اطلاعات بیش از یک بار انجام شود باید در بدنه این تابع مجدداً دستور دریافت اطلاعات نوشته شود. پس تابع Rx Complete Callback را بهصورت زیر و درون فایل main.c تعریف میکنیم:
1 2 3 4 | void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { HAL_UART_Receive_IT(&huart1, message, 4); } |
دوباره از کد Build میگیریم و روی میکرو دانلود میکنیم. مجدداً از ترمینال سریال همان داده 1234 را به پورت ارسال میکنیم؛
این بار مشاهده میکنیم که علاوه بر اینکه داده بهصورت کامل دریافت شده است، سرعت چشم زدن LED نیز تغییری نمیکند.
در این قسمت از سری آموزش STM32 با توابع HAL، با نحوه دریافت اطلاعات بهوسیله USART، هم به شکل Polling (Blocking mode) و هم به شکل وقفه (Non-blocking mode) آشنا شدیم و در قسمتهای بعد، با روش سوم یعنی استفاده از DMA نیز آشنا خواهیم شد. در قسمت بعدی نحوه Redirect کردن توابع کتابخانه stdio را یاد میگیریم. با ما همراه باشید.
سلام اسم برنامه برای کار با ماژول ttlچیه؟
برای خواندن داده ها از پورت سریال نرم افزارهای زیادی هست
از جمله
putty
realterm
teraterm
GTKTerm
و …
رشته هایی که با فرمت uint_8 هستن رو میشه به رشته با فرمت char تبدیل کرد اگه کسی راهنماییم کنه ممنون میشم
سلام دوست عزیز
بله میشه به سادگی با کست کردن میتونید این کار رو انجام بدید
uint8_t buf[] = "sisoog.com";
char *cbuf = (char*)buf;
چی بگم . مگه چیزی دارم که بگم از خوبیتون فقط اینکه بینظیرید .
خیلی چیزا یاد گرفتم ازتون و میدونم که چقدر ارزشمنده ایها
اگر کاری از دستم بر میاد در این راه لطفا بگین با کمال میل انجام میدم (:
سلام دوست عزیز
ما هدفمون تو سیسوگ بالابردن سطح علمی الکترونیک کشورمون هست. شما و دوستان دیگر با به اشتراک گذاری دانشتون با سایر فعالان در این حوزه در پلتفرم سیسوگ میتونید به ما تو این مسیر کمک کنید.
HAL_UART_Receive_IT(&huart1, message, 4);
باید اینجوری اصلاح بشود
ممنون از شما
اصلاح شد
سلام،
خیلی ممنون و سپاسگزار از آموزش های خوبتان.
من کلا stm32 را با سایت شما آموزش گرفتم و یک پرینتر جوهر افشان دارم با stm32f407vgt6 می سازم.
البته عنوز تمام نشده.
فقط یک نکته که در دستور آخری که برای تابع کال بک اینجا نوشتین یک اشتباه کوچک دارد که لطفا اصلاح فرمایید
HAL_UART_Receive_IT(&huart2, Rx_data, 4);
اشاره گر اشتباه است و باید اینجوری اصلاح بشود
HAL_UART_Receive_IT(&huart1, Rx_data, 4);
باز هم تشکر می کنم.
سلام دوست عزیز
خیلی خوشحالم که در این مسیر تونستیم کمکی کنیم
و ممنونم برای توجه و لطف شما – مساله مربوط اصلاح شد
سلام وقتتون بخیر
چرا در تابع کال بک اتمام دریافت که برای استفاده مجدد در فایل main نوشتیم بعضی دستورات کار نمیکند؟من یک متغیر را مقدار دهی میکنم و یا السیدی را پاک میکنم دستورات انجام نمیشود.
سلام دوست عزیز، وقت شما هم بخیر.
یک نکته اینکه در مورد توابع کال بک، به طور معمول سعی میشه که بدنه این توابع تا جای ممکن کوتاه و خلاصه نوشته بشه و از قرار دادن دستورات غیرضروری خودداری میکنیم.
اما در مورد اینکه چرا اعمال مورد نظر شما انجام نمیشن، علت این اتفاق احتمالا اینه که متغیرهای مورد نظر شما از نوع گلوبال تعریف نشدن. در مورد LCDهم متوجه منظور سوالتون نشدم. دقیقا از چه کدی استفاده میکنید؟
متغیر هام گلوبال هستند و خارج از تابع main و در قسمت مربوطه مشخص شده مربوط به متغیر های گلوبال تعریف کرده ام.
منظورم از پاک کردن السیدی هم به این صورت است که من از کتابخونه السیدی استفاده می کنم و تابع مربوط به clear کردن السیدی در بقیه جاهای برنامه استفاده میکنم کاملا کار میکنه اما توی تابع کال بک خیر.
نحوه تعریف تابع کال بک هم به این صورته که من خارج از تابع main و در پایین برنامه اضافه میکنم و دستورات لازم را داخلش مینویسم و در قسمت فراخوانی توابع این تابع را مینویسم.
پس احتمالا باید فلگهارو چک بکنید که ببینید آیا به درستی فعال میشن