خب، بیایید یک پروژه جدید ایجاد کنیم. این پروژه نسخهای نسبتاً طولانی از برنامهی “Hello World” است. دلیل طولانی بودن آن این است که قرار است تمام کارهایی که سیستم عامل آن را برای ما پنهان میکند را انجام دهد. در ابتدا فایلهای هدر مورد نیاز که اطلاعات مربوط به UART (و بقیهی قسمتهای میکرو) را تعریف میکنند، وارد میکنیم:
1 2 3 | #include "stm32f0xx.h" #include "stm32f0xx_nucleo.h" |
سپس با تابع اصلی (main) شروع میکنیم:
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 | int main(void) { //1 HAL_Init(); // سختافزار را راهاندازی میکند. led2_Init(); uart2_Init(); // ارسال دائمی پیام در حلقهی بینهایت //2 for (;;) { // کاراکتر به کاراکتر ارسال میکند. for(current = 0; hello[current] != '\0'; ++current) { //3 myPutchar(hello[current]); } //4 HAL_Delay(500); } } |
تابع اصلی کاملاً شبیه به تابعی است که در کدهای ۹-۱ و ۹-۲ دیدیم. البته یک مورد اضافه شده است و آن راهاندازی تمام سختافزارهایی که قرار است از آنها استفاده کنیم (۱)، از جمله کتابخانه سختافزار (HAL_Init)، LED قرمز (led2_Init) و پورت سریال UART (uart2_Init). برنامه امبدد نمیتواند متوقف شود، بنابراین یک حلقه بینهایت (۲) داریم که رشته را ارسال میکند (۳) و سپس به مدت نیم ثانیه میخوابد (۴).
یکی از اولین کارهایی که باید در مرحلهی بعد انجام دهیم، ایجاد یک تابع ErrorHandler است که کتابخانه HAL در صورت بروز مشکل آن را فراخوانی میکند. در صورت وجود خطا ما نمیتوانیم پیام خطا چاپ کنیم، زیرا کد چاپ ما بهدرستی کار نمیکند، بنابراین به چشمکزدن LED قرمز متوسل میشویم. این که خطای رخ داده را با استفاده از یک LED نمایش دهیم خیلی محدود کننده به نظر میرسد اما کاربرد این LED درست مانند چراغ چک موتور در خودرو میباشد. ما در اینجا به تابع Error_Handler نمیپردازیم؛ این تابع همانند blink که در فصل 3 توضیح داده شد، با یک نام جدید است.
یک دستگاه سریال باید بهسادگی برنامهریزی شود. بااینحال، مهندسان شرکت STMicroelectronics تصمیم گرفتهاند با ارائهی ویژگیهای اضافی، UART ساده را بهبود بخشند. در نتیجه، دستگاه سریال سادهی ما حالا برای توصیف نجوهی راهاندازی یک دفترچهی راهنمای ۴۵ صفحهای دارد. ما فقط میخواهیم از این ابزار برای ارسال کاراکتر استفاده کنیم و حتی نیازی به دریافت اطلاعات روی پورت سریال نداریم.
خوشبختانه، کتابخانه HAL تابعی به نام HAL_UART_Init فراهم کرده که بسیاری از جزئیات پیچیده را از دید ما پنهان میکند. اما متأسفانه، خود فراخوانی HAL_UART_Init همچنان نیاز به تنظیم برخی جزئیات دارد — نمیتوان همه چیز را سادهسازی کرد. در تابع uart2_Init، باید یک ساختار مقداردهی اولیه تنظیم کنیم و سپس HAL_UART_Init را صدا بزنیم.
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 | void uart2_Init(void) { راهاندازی //UART UART2 -- یکی از UARTها به ST-LINK USB متصل است// //1 uartHandle.Instance = USART2; uartHandle.Init.BaudRate = 9600; // سرعت ۹۶۰۰ بود //3 uartHandle.Init.WordLength = UART_WORDLENGTH_8B; // ۸ بیت بر کاراکتر //4 uartHandle.Init.StopBits = UART_STOPBITS_1; // ۱ بیت توقف //5 uartHandle.Init.Parity = UART_PARITY_NONE; // بدون پریتی //6 uartHandle.Init.Mode = UART_MODE_TX_RX; // ارسال و دریافت //7 uartHandle.Init.HwFlowCtl = UART_HWCONTROL_NONE; // بدون کنترل سختافزاری // بیشنمونهبرداری از دادههای ورودی // uartHandle.Init.OverSampling = UART_OVERSAMPLING_16; // از نمونهبرداری یک بیتی استفاده نکنید //8 uartHandle.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE; // بدون ویژگی پیشرفته //9 uartHandle.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT; /* * برای کسانی که یک شبیهساز ترمینال را وصل میکنند، * ترجمه میشوند 9600,8,N,1پارامترهای بالا به */ if (HAL_UART_Init(&uartHandle) != HAL_OK) { Error_Handler(); } } |
در مرحلهی بعد برای راهاندازی UART تابع HAL_UART_Init را فراخوانی میکنیم (۹) که برای اینکه این تابع بتواند وظیفهی خود را انجام دهد نیاز به کمک دارد. پینهای ورودی/خروجی چندمنظوره (GPIO) روی پردازندهی ما میتوانند کارهای مختلفی انجام دهند، از جمله اینکه بهعنوان پینهای GPIO عمل کنند. اکثر آنها دارای «توابع جایگزین» هستند، به این معنی که میتوانید آنها را برای انجام کاربردهای مختلف برنامهریزی کنید (پین GPIO، دستگاه USART، باس SPI، باس I2C، پین PWM و غیره). توجه داشته باشید که همهی پینهای میکروکنتلر از همهی امکانات و دستگاهها پشتیبانی نمیکنند و هر کدام ممکن است امکانات مختلفی داشته باشند. در نهایت، HAL_UART_Init تابع HAL_UART_MspInit را فراخوانی میکند که پینها را برای UART راهاندازی میکند (تابع HAL_UART_MspInit همان کمکی است که تابع HAL_UART_Init برای انجام کار خود نیاز دارد):
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 | HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart) { /* بررسی تخصیص درگاه سریال */ if(huart == NULL) { return HAL_ERROR; } // ... if(huart->gState == HAL_UART_STATE_RESET) { /* اختصاص منابع قفل و راهاندازی آن */ huart->Lock = HAL_UNLOCKED; راهاندازی سختافزار سطح پایین: GPIO،//CLOCK HAL_UART_MspInit(huart); } |
برای اینکه تابع HAL_UART_MspInit کار خود را به درستی انجام دهد ما باید آن را به نسبت نیاز خود تنظیم کنیم .به طور پیشفرض، دو پینی که دستگاه سریال ما را راهاندازی میکنند، به نام PA2 و PA3 بوده که درواقع همان پینهای GPIO هستند. ما باید به سیستم بگوییم که از تابع جایگزین پینها استفاده کند و آنها را به پینهای دستگاه سریال تبدیل کند.
تابع HAL_UART_MspInit ما شبیه به کد راهاندازی پین GPIO است که برای «blink» استفاده کردیم، اما با برخی تفاوتهای جزئی:
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 | void HAL_UART_MspInit(UART_HandleTypeDef* uart) { GPIO_InitTypeDef GPIO_InitStruct; 1 if(uart->Instance == USART2) { /* فعالسازی کلاک مربوط به سریال */ 2 __HAL_RCC_USART2_CLK_ENABLE(); /* پیکربندی GPIO برای USART2 * PA2 ------> USART2_TX * PA3 ------> USART2_RX */ GPIO_InitStruct.Pin = GPIO_PIN_2 | GPIO_PIN_3; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; تابع جایگزین - مربوط به //UART 3 GPIO_InitStruct.Alternate = GPIO_AF1_USART2; HAL_GPIO_Init(GPIOA، &GPIO_InitStruct); } } |
این تابع با بررسی اینکه از کدام USART استفاده میکنیم شروع میشود. ما در این کد فقط USART2 را راهاندازی میکنیم (۱). سپس کلاک قسمت مربوط به USART2 را فعال میکنیم (۲). در مرحله بعد، پینهای GPIO را پیکربندی میکنیم (۳) (ما قبلاً این کار را در برنامهی blink انجام دادهایم) که به تراشه میگوید PA2/PA3 پینهای GPIO نیستند؛ بلکه باید به USART2 متصل شوند.
برای ارسال کاراکترها با پورت سریال، از تابع myPutchar استفاده میکنیم. USARTUSART یک وسیلهی I/O با نگاشت در حافظه (Memory-Mapped I/O) است. برای فرستادن یک کاراکتر، باید آن را به یک مکان حافظه خاص (یک رجیستر) اختصاص دهیم (بنویسیم) و سپس آن کاراکتر از طریق سیم خارج میشود.
در کد زیر، رجیستر TDR (Transmitter Data Register) را برای ارسال کاراکتر ch به USART تنظیم میکنیم:
1 | uartHandle.Instance->TDR = ch;ارسال کاراکتر به// USART |
بااینحال، برای ارسال صحیح کاراکتر، باید زمانبندی دقیقی را رعایت کنیم. برای این کار، از کد زیر استفاده میکنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | void myPutchar(const char ch) { مقدار پرچم UART_FLAG_TXE را در لحظه فراخوانی تابع دریافت و ذخیره میکنیم// این مقدار بهسرعت تغییر میکند، بنابراین اگر برنامه را روی خط// "if" پایین متوقف کنید، مقدار آن به صفر تنظیم میشود زیرا سریعتر از// زمانی که شما میتوانید آن را ببینید، از بین میرود // int result __attribute__((unused)) = (uartHandle.Instance->ISR & UART_FLAG_TXE); // تا زمانی که پرچم `TXE` (Transmit Empty) تنظیم نشده باشد، منتظر میمانیم. while ((uartHandle.Instance->ISR & UART_FLAG_TXE) == 0) continue; uartHandle.Instance->TDR = ch; // ارسال کاراکتر به USART { |
رجیستری که کاراکتر مورد نظر برای ارسال را در آن مینویسیم، رجیستر انتقالدهنده داده (TDR: transmit data register) نامیده میشود. اگر درحالیکه یک کاراکتر منتقل میشود، رجیستر TDR را بنویسیم، کاراکتر جدید کل کاراکتر قبلی را پاک میکند و باعث ایجاد خطا و سردرگمی در دریافت اطلاعات میشود. برای ارسال حروف a، b، c، کد زیر استفاده میشود:
1 2 3 4 5 6 7 8 9 10 11 | uartHandle.Instance->TDR = 'a'; sleep_1_960_second(); uartHandle.Instance->TDR = 'b'; sleep_1_960_second(); uartHandle.Instance->TDR = 'c'; sleep_1_960_second(); |
در این کد، مشکل اینجاست که زمان نوشتن کاراکتر b رجیستر TDR، کاراکتر a هنوز به طور کامل ارسال نشده است. در نتیجه، بخشی از اطلاعات a پاک شده و با b جایگزین میشود. به همین ترتیب، در زمان نوشتن c رجیستر TDR، بخشی از اطلاعات b پاک شده و با c جایگزین میشود.
بنابراین، برای ارسال صحیح کاراکترها بهصورت متوالی، باید از نوشتن رجیستر TDR در حین ارسال کاراکتر قبلی خودداری کنیم. بهعبارتدیگر، باید منتظر بمانیم تا هر کاراکتر به طور کامل ارسال شود و سپس کاراکتر بعدی را ارسال کنیم.
این نوع زمانبندی بهخصوص زمانی که میخواهیم بین کاراکترها کد اجرا کنیم، کار سختی است. تراشه STM32 برای هر چیزی بیتی دارد، از جمله «TDR خالی است، اکنون میتوانید کاراکتر دیگری بنویسید». این بیت در رجیستری به نام رجیستر وقفه و وضعیت (ISR) قرار دارد که دارای تعدادی بیت برای نشاندادن وضعیت دستگاه است.
شکل ۹-۲ نموداری این رجیستر را از راهنمای مرجع STM32F030R8
محتویات رجیستر وقفه و وضعیت
در تصویر بالا بیت TXE (بیت 0) برای ما جالب است . HAL بیت TXE را با استفاده از نام UART_FLAG_TXE تعریف میکند. قبل از اینکه بتوانیم بدون تداخل با کاراکتر در حال ارسال، دادهای را به TDR ارسال کنیم، باید منتظر بمانیم تا بیت TXE پاک شود (صفر شود). توجه کنید که ISR توسط خود سختافزار کنترل میشود و ما نمیتوانیم مقداری در آن بنویسیم و فقط امکان خواندن از آن را داریم که در اینجا هم TXE را میخوانیم.
بااینحال، uartHandle.Instance->ISR یک آدرس حافظهی خاص است که به یک سختافزار الکتریکی (USART) متصل است. وضعیت دستگاه سریال زمانی تغییر میکند که چیزی مانند تکمیل انتقال یک کاراکتر اتفاق بیفتد، و زمانی که این اتفاق میافتد، محتویات uartHandle.Instance->ISR نیز توسط خود سختافزار تغییر میکند.
توجه: فیلد ISR بهصورت volatile اعلام میشود تا به کامپایلر C بگوید که این مقدار میتواند به طور جادویی در هر زمانی به روشی که خارج از کنترل کامپایلر است، تغییر کند.
حالا اگر سعی کنید uartHandle.Instance->ISR را با استفاده از دیباگر بررسی کنید، به نظر میرسد که فلگ UART_FLAG_TXE همیشه مقدار یک را دارد. دلیل اینکه ما این بیت را همیشه یک میبینیم این است که درواقع این بیت با سرعت 1/960 ثانیه توسط سختافزار پاک میشود که این زمان از دید کامپیوتر زمان زیادی است اما از دید انسان آنقدر زمان کمی است که حتی دیده هم نمیشود.
برای اینکه بفهمیم در پشت صحنه چه اتفاقی میافتد، یک دستور بیاستفاده به کد اضافه کردهایم:
1 | int result __attribute__((unused)) = (uartHandle.Instance->ISR & UART_FLAG_TXE); |
این دستورالعمل مقدار بیت UART_FLAG_TXE را از ISR میخواند و آن را در result ذخیره میکند. حالا ممکن است مقدار (uartHandle.Instance->ISR & UART_FLAG_TXE) در طول اجرای برنامه به صورت سختافزاری تغییر کند، اما مقدار result که در همین لحظه محاسبه شده است (در زمان ارسال کاراکتر) تا پایان ثابت میماند.
شما میتوانید در دیباگر به result نگاه کنید و ببینید که مقدار بیت در ابتدای حلقه چه بوده است. عبارت عجیبی در کد به چشم شما خواهد خورد:
1 | __attribute__((unused)) |
در GCC (کامپایلر GNU)، وقتی متغیری تعریف میکنید ولی در ادامه استفادهاش نمیکنید (معمولاً جهت استفاده در حالت دیباگ این را تعریف میکنیم)، کامپایلر اخطار (warning) میدهد.
رشتهای که برای “Hello World” ارسال میکنیم با \r\n (بازگشت به ابتدای خط، پرش به خط بعدی) تمام میشود. در برنامهی اصلی “Hello World” ما از فصل ۱، سیستمعامل خروجی (stream) را ویرایش کرد و \n را برای ما به \r\n تغییر داد. ما سیستمعاملی نداریم، بنابراین باید همه کار را خودمان انجام دهیم.
کد 9-3 نسخهی سریال کامل برنامهی “Hello World” ما را شامل میشود.
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 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 | /** @brief پیام "Hello World!" را روی پورت سریال ارسال میکند. */ #include <stdbool.h> #include "stm32f0xx_nucleo.h" #include "stm32f0xx.h" const char hello[] = "Hello World!\r\n"; // پیامی که قرار است ارسال شود int current; // شماره کاراکتر در پیامی که در حال ارسال آن هستیم UART_HandleTypeDef uartHandle; مقداردهی اولیه //UART /** @brief این تابع در صورت بروز خطا اجرا میشود. عملیات این تابع فقط چشمکزدن LED است. */ void Error_Handler(void) { /* روشنکردن LED2 */ HAL_GPIO_WritePin(LED2_GPIO_PORT, LED2_PIN, GPIO_PIN_SET); while (true) { تعویض وضعیت //LED2 HAL_GPIO_TogglePin(LED2_GPIO_PORT, LED2_PIN); } } // انتظار یک ثانیه HAL_Delay(1000); /** ارسال یک کاراکتر به UART @param ch کاراکتری که ارسال میشود */ void myPutchar(const char ch) { این خط مقدار بیت UART_FLAG_TXE را در لحظهی فراخوانی دریافت کرده و ذخیره میکند.// این مقدار (TXE) ممکن است تغییر کند،// بنابراین اگر اجرای برنامه را در خط if (یا while) بعدی متوقف کنید (مثلاً با breakpoint)،// مقدار آن صفر میشود، چون این بیت خیلی سریع تغییر میکند — سریعتر از آنکه بتوانید آن را ببینید!// int result __attribute__((unused)) = (uartHandle.Instance->ISR & UART_FLAG_TXE); بلاک کردن تا زمانی که پرچم خالی انتقال (TXE) تنظیم شود// while ((uartHandle.Instance->ISR & UART_FLAG_TXE) == 0) continue; uartHandle.Instance->TDR = ch; ارسال کاراکتر به// UART { /** @brief مقداردهی اولیه LED2 (چشمکزن قرمز برای خطا). */ void led2_Init(void) { مقداردهی اولیه کلاک// LED LED2_GPIO_CLK_ENABLE(); GPIO_InitTypeDef GPIO_LedInit; برای LED مقداردهی اولیه// مقداردهی اولیه// 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); } /** @brief مقداردهی اولیه UART2 برای خروجی. */ void uart2_Init(void) { مقداردهی اولیه //UART UART2 -- یکی از پورتهای UART متصل به //USB ST-LINK uartHandle.Instance = USART2; uartHandle.Init.BaudRate = 9600; سرعت 9600// uartHandle.Init.WordLength = UART_WORDLENGTH_8B; 8 بیت/کاراکتر// uartHandle.Init.StopBits = UART_STOPBITS_1; 1 بیت توقف// uartHandle.Init.Parity = UART_PARITY_NONE; // بدون پریتی uartHandle.Init.Mode = UART_MODE_TX_RX; // ارسال و دریافت uartHandle.Init.HwFlowCtl = UART_HWCONTROL_NONE; // بدون کنترل سختافزاری // زیادی نمونهبرداری ورودی uartHandle.Init.OverSampling = UART_OVERSAMPLING_16; // از نمونهبرداری یک بیتی استفاده نکنید. uartHandle.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE; // بدون هیچ ویژگی پیشرفتهای uartHandle.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT; /* برای کسانی که یک شبیهسازی ترمینال متصل میکنند، پارامترهای بالا به 9600,8,N,1 ترجمه میشوند. */ if (HAL_UART_Init(&uartHandle) != HAL_OK) { Error_Handler(); } { int main(void) { HAL_Init(); // مقداردهی اولیه سختافزار led2_Init(); uart2_Init(); // ارسال پیام به مدتزمان طولانی for (;;) { // ارسال کاراکتر به کاراکتر for (current = 0; hello[current] != '\0'; ++current) { myPutchar(hello[current]); } HAL_Delay(500); } } /** @brief تابع جادویی که توسط لایه HAL برای واقعیسازی مقداردهی اولیه UART فراخوانی میشود. در این حالت، نیاز داریم که پینهای UART را در حالت جایگزین قرار دهیم تا بهعنوان پینهای UART عمل کنند و نه مانند پینهای GPIO. * @note: فقط برای UART2 کار میکند که به مبدل سریال USB متصل است. @param uart اطلاعات UART */ void HAL_UART_MspInit(UART_HandleTypeDef* uart) { GPIO_InitTypeDef GPIO_InitStruct; if(uart->Instance == USART2) { /* فعالکردن کلاک دستگاه */ __HAL_RCC_USART2_CLK_ENABLE(); /* پینهای USART2 GPIO * PA2 ------> USART2_TX * PA3 ------> USART2_RX */ GPIO_InitStruct.Pin = GPIO_PIN_2|GPIO_PIN_3; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; عملکرد جایگزین -- عملکرد// UART GPIO_InitStruct.Alternate = GPIO_AF1_USART2; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); } } /** @brief تابع جادویی که توسط لایه HAL برای ازکارانداختن سختافزار UART فراخوانی میشود. این چیزی است که هرگز انجام نمیدهیم، اما برای کاملبودن، آن را در اینجا قرار میدهیم. @note: فقط برای UART2 کار میکند که به مبدل سریال به USB متصل است. @param uart اطلاعات UART */ void HAL_UART_MspDeInit(UART_HandleTypeDef* uart) { if(uart->Instance == USART2) { /* غیرفعالکردن کلاک دستگاه */ __HAL_RCC_USART2_CLK_DISABLE(); /* پینهای USART2 GPIO * PA2 ------> USART2_TX * PA3 ------> USART2_RX */ HAL_GPIO_DeInit(GPIOA, GPIO_PIN_2|GPIO_PIN_3); } { |
کد 9-3: برنامه 08.serial
نویسنده شو !
سیسوگ با افتخار فضایی برای اشتراک گذاری دانش شماست. برای ما مقاله بنویسید.