هر برنامه طراحی شده برای میکروکنترلرها نیاز به تبادل داده با دنیای خارج یا درایو تجهیزات جانبی خارجی دارد. به عنوان مثال، یک میکروکنترلر ممکن است با استفاده از UART با ماژولهای دیگر روی برد پیامهایی را مبادله کند، یا ممکن است دادهها را با استفاده از رابط SPI در حافظه فلش خارجی موجود ، ذخیره کند. این موضوع شامل انتقال مقدار معینی از داده بین SRAM داخلی یا حافظه فلش و رجیسترهای جانبی است و برای انجام انتقال به تعداد معینی چرخه CPU نیاز داردکه منجر به از دست دادن قدرت محاسباتی پردازنده (CPU در فرآیند انتقال اشغال می شود)، کاهش عملکرد کلی و در نهایت از دست دادن رویدادهای مهم ناهمزمان می شود. کنترل کننده Direct Memory Acces s (DMA) یک واحد سخت افزاری اختصاصی و قابل برنامه ریزی است که به واحدهای جانبی MCU اجازه می دهد تا بدون دخالت هسته Cortex-M به حافظه های داخلی دسترسی داشته باشند. CPU به طور کامل از سربار تولید شده توسط انتقال داده آزاد می شود (به جز سربار مربوط به پیکربندی DMA)، و می تواند فعالیت های دیگری را به صورت موازی انجام دهد. واحد DMA طوری طراحی شده است که می تواند به هر دو صورت کار کند (یعنی امکان انتقال داده از حافظه به واحدهای جانبی و بالعکس) و همه میکروکنترلرهای STM32 حداقل یک کنترلر DMA را ارائه می دهند، اما اکثر آنها دو DMA مستقل را پیاده سازی میکنند.
واحد DMA یک ویژگی “پیشرفته” MCU های مدرن است که کاربران تازه کار استفاده از آن را بسیار پیچیده در نظر میگیرند. در عوض، مفاهیم زیربنایی DMA واقعا ساده هستند و زمانی که آنها را درک کردید، استفاده از آن بسیار آسان خواهد بود. علاوه بر این، خبر خوب این است که CubeHAL تمام مراحل پیکربندی DMA را برای یک واحد جانبی انجام داده و تنها تنظیمات اولیه را به عهده کاربر میگذارد. در ادامه شما را با مفاهیم اساسی مربوط به استفاده از DMA آشنا میکنیم و یک نمای کلی از ویژگیهای DMA در تمام خانوادههای STM32 را ارائه خواهیم داد. قبل از اینکه بتوانیم ویژگیهای ارائه شده توسط ماژول HAL_DMA را تجزیه و تحلیل کنیم، درک برخی مفاهیم اساسی در کنترل کننده DMA مهم است.
نیاز به DMA و نقش آن در باس های داخلی
هر واحد جانبی در میکروکنترلر STM32 نیاز به تبادل داده با هسته داخلی Cortex-M دارد. برخی از آنها این دادهها را به عنوان سیگنالهای ورودی/خروجی ترجمه کرده و آنها را بر اساس یک پروتکل ارتباطی معین به دنیای خارج مبادله میکنند (مثل رابطهای UART یا SPI). برخی دیگر به گونهای طراحی شدهاند که دسترسی به رجیسترهای آنها در داخل منطقه نقشه حافظه جانبی (از 0x4000 0000 تا 0x5FFF FFFF) باعث تغییر وضعیت آنها میشود (به عنوان مثال، رجیستر GPIOx->ODR وضعیت همه ورودی/خروجیهای متصل به پورت را کنترل می کند). با این حال، به خاطر داشته باشید که از نظر CPU، همچنین تبادل داده ای نیز به معنای انتقال داده بین هسته و واحد جانبی میباشد.
هسته MCU، در تئوری، میتواند به گونهای طراحی شود که هر واحد جانبی فضای ذخیرهسازی مخصوص به خود را داشته باشد و طوری با هسته CPU همراه شود که هزینه های مربوط به انتقال حافظه را به حداقل برساند. اما این روش، معماری MCU را پیچیده میکند و به مقدار زیادی سیلیکون و «مولفههای فعال» نیاز دارد که انرژی مصرف میکنند. بنابراین، رویکردی که در همه میکروکنترلرها استفاده میشود، استفاده از بخشهایی از حافظه داخلی (SRAM و همچنین فلش) به عنوان فضای ذخیرهسازی موقت برای واحدهای جانبی مختلف است. این ، به خود کاربر بستگی دارد که تصمیم بگیرد چقدر فضا را به این مناطق اختصاص دهد. به عنوان مثال، اجازه دهید این قطعه کد را در نظر بگیریم:
uint8_t buf[20];
...
HAL_UART_Receive(&huart2, buf, 20, HAL_MAX_DELAY);
در اینجا میخواهیم بیست بایت از رابط UART2 را بخوانیم، از این رو یک آرایه (ذخیرهسازی موقت) با همان اندازه را در داخل SRAM اختصاص میدهیم. تابع HAL_UART_Receive () بیست بار به رجیستر داده huart2.Instance->DR دسترسی خواهد داشت تا بایتها را از واحد جانبی به حافظه داخلی منتقل کند، به علاوه فلگ UART RXNE را بررسی میکند تا تشخیص دهد که چه زمانی داده جدید آماده انتقال است. CPU در طول این عملیات درگیر خواهد شد (شکل 1 را ببینید)، حتی اگر نقش آن محدود به انتقال دادهها از واحد جانبی به SRAM باشد:
در حالی که این رویکرد از یک سو طراحی سخت افزار را ساده میکند، از سوی دیگر مشکلات عملکردی دیگری را معرفی میکند. هسته Cortex-M “مسئول” بارگذاری دادهها از حافظه جانبی به SRAM بوده که نه تنها CPU را از انجام سایر فعالیتها باز میدارد، بلکه باید CPU منتظر تکمیل واحدهای “آهسته تر” نیز باشد. به همین دلیل است که در میکروکنترلرهای پیشرفته، واحدهای سخت افزاری اختصاص داده شده تا برای انتقال داده بین واحدهای جانبی و SRAM استفاده شوند.
قبل از اینکه به عمق جزئیات DMA بپردازیم، بهتر است مروری بر تمام اجزای درگیر در فرآیند انتقال داده از یک واحد جانبی به حافظه SRAM و بالعکس داشته باشیم. معماری باس STM32F030، یکی از ساده ترین میکروکنترلرهای STM32، در شکل زیر نشان داده شده است که تفاوت زیادی با سایر خانواده های پیشرفته تر STM32 دارد. شکل زیر به ما چند نکته مهم را میگوید:
هر دو هسته Cortex-M و کنترلکننده DMA1 از طریق یک سری گذرگاه با سایر واحدهای جانبی MCU تعامل دارند. اگر هنوز مشخص نیست، ذکر این نکته مهم است که حافظههای فلش و SRAM نیز اجزای خارج از هسته MCU هستند و بنابراین باید از طریق اتصال باس با یکدیگر تعامل داشته باشند.
هسته Cortex-M و کنترلکننده DMA1 هر دو master هستند. یعنی آنها تنها واحدهایی هستند که میتوانند عملیات را در گذرگاه شروع کنند. هر چند تنها یکی از آنها میتواند در یک زمان به یک واحد جانبی مشخص دسترسی داشته باشد.
BusMatrix دسترسی بین هسته Cortex-M و کنترلر DMA1 را مدیریت میکند. دسترسی به گذرگاه از یک الگوریتم Round Robin استفاده میکند. BusMatrix از دو Master یعنی CPU، DMA و چهار Slave رابط فلش، SRAM، AHB1 ( با پل AHB به گذرگاه محیطی پیشرفته APB) و AHB2 تشکیل شده است. BusMatrix همچنین اجازه می دهد تا به طور خودکار چندین واحد جانبی را به هم متصل کنید.
گذرگاه System هسته Cortex-M را به BusMatrix متصل میکند.
گذرگاه DMA رابط اصلی گذرگاه پیشرفته (AHB) DMA را به BusMatrix متصل میکند.
پل AHB به APB اتصالات سنکرون کامل بین AHB و گذرگاه APB را فراهم میکند، جایی که اکثر تجهیزات جانبی متصل هستند.
در تصویر بالا میبینیم که پیکان درخواست DMA از بلوک واحدهای جانبی (مستطیل سفید) به کنترل کننده DMA1 می رود. خب این پیکان چه کاری انجام میدهد؟ در گذشته دیدیم که کنترلکننده NVIC هسته Cortex-M را در مورد درخواستهای وقفه ناهمزمان (IRQ) که از واحدهای جانبی میآیند مطلع میکند. هنگامی که یک واحد جانبی آماده انجام کاری است (به عنوان مثال، UART آماده دریافت داده است یا یک تایمر سرریز میشود)، یک مسیر IRQ اختصاصی ارائه میشود. هسته در تعداد معینی از چرخهها، ISR مربوطه را اجرا میکند که حاوی کد لازم برای مدیریت IRQ است. فراموش نکنید که واحدهای جانبی ، SLAVE هستند: آنها نمیتوانند به طور مستقل به گذرگاه دسترسی داشته باشند یعنی برای شروع یک عملیات همیشه به یک MASTER نیاز دارند. حال اگر از DMA برای انتقال دادهها از واحدهای جانبی به حافظه استفاده کنیم، راهی داریم که به آن اطلاع دهیم که واحدهای جانبی آماده تبادل داده هستند. به همین دلیل است که تعدادی خط درخواست اختصاصی از واحدهای جانبی به کنترلر DMA در دسترس است.
کنترل کننده DMA
در هر میکروکنترلر STM32، کنترلر DMA یک واحد سخت افزاری است که:
دو پورت master متصل به گذرگاه پیشرفته با کارایی بالا (AHB) به نامهای پورت peripheral و پورت memory دارد ، یکی با واحد جانبی و دیگری با کنترلکننده حافظه (SRAM، فلش، FSMC و غیره) ارتباط برقرار می کند. در برخی از کنترلکنندههای DMA، پورت peripheral میتواند با یک کنترلکننده حافظه ارتباط برقرار کند و امکان انتقال حافظه به حافظه را فراهم کند.
یک پورت slave، متصل به گذرگاه AHB دارد که برای برنامه ریزی کنترلر DMA توسط master دیگر یعنی CPU استفاده میشود.
دارای تعدادی کانال مستقل و قابل برنامه ریزی (منابع درخواست) بوده، که هر کدام به یک مسیر درخواست واحد جانبی خاص (UART_TX، TIM_UP و غیره) متصل هستند – تعداد و نوع درخواست ها برای یک کانال در زمان طراحی MCU تعیین میشود.
اجازه می دهد تا به هر یک از کانال ها ، اولویتهای مختلف را اختصاص دهید( اولویت دسترسی حافظه به واحدهای جانبی سریعتر و مهم )
اجازه میدهد تا دادهها در هر دو جهت جریان داشته باشند، یعنی از حافظه به واحدهای جانبی و از واحدهای جانبی به حافظه.
هر میکروکنترلر STM32 تعداد متفاوتی از DMA ها با کانال های مختلف را با توجه به خانواده و نوع آن، ارائه میدهد. جدول زیر تعداد دقیق آنها را برای میکروکنترلر های STM32 نشان میدهد. با این حال، خانوادههای STM32F2/F4/F7 یک کنترلر DMA پیشرفتهتر را همراه با یک BusMatrix چند لایه ارائه میکنند که امکان تقویت و موازی کردن انتقالهای DMA را فراهم میکند.
پیاده سازی DMA در میکروکنترلر های F0/F1/F3/L1
شکل زیر DMA را در میکروکنترلرهای F0/F1/F3/L1 نشان میدهد. در اینجا، برای سادگی، تنها یک مسیر درخواست نشان داده شده است، اما هر DMA یک مسیر درخواست را برای هر کانال پیادهسازی میکند. هر مسیر درخواست دارای تعداد متغیری از منابع درخواست واحدهای جانبی متصل به آن است. یک کانال در طول طراحی تراشه به مجموعه ثابتی از واحدهای جانبی متصل میشود. با این حال، تنها یک واحدجانبی به طور همزمان میتواند در همان کانال فعال باشد. به عنوان مثال، جدول زیر نحوه اتصال کانالها به واحدهای جانبی در یک میکروکنترلر STM32F030 را نشان میدهد. هر مسیر درخواستی میتواند توسط “نرم افزار” راهاندازی شود.
هر کانال دارای یک اولویت قابل تنظیم است که اجازه میدهد تا دسترسی به گذرگاه AHB را کنترل کنید. یک واحد ناظر داخلی درخواستهایی را که از کانالها میآیند بر اساس اولویت تنظیم شده توسط کاربر سازماندهی می کند. اگر دو مسیر درخواست هم زمان فعال شوند و کانالهای آنها دارای اولویت یکسان باشند، کانالی که شماره کمتری دارد در اولویت است.
بسته به نوع میکروکنترلر مورد استفاده، یک یا دو کنترلر DMA برای مجموع 12 کانال مستقل (5 برای DMA1 و 7 برای DMA2) موجود است. به عنوان مثال، همانطور که در جدول زیر نشان داده شده است، STM32F030 تنها یک DMA1 با 5 کانال را ارائه میکند.
در بخش قبلی معماری گذرگاه STM32F030 را دیدیم. شکل زیر معماری گذرگاه میکروکنترلر های کارآمدتر را نشان میدهد (به عنوان مثال، STM32F1). همانطور که میبینید، این دو خانواده یک بخش گذرگاه داخلی کاملاً متفاوت دارند. میتوانید دو گذرگاه اضافی به نامهای ICode و DCode را مشاهده کنید.
اکثر میکروکنترلرهای STM32 از معماری کامپیوتری مشابهی استفاده میکنند به جز STM32F0 و STM32L0 که بر اساس هسته های Cortex-M0/0+ هستند. آنها در واقع تنها هسته های Cortex-M بر اساس معماری von Neumann میباشند ، در مقایسه با دیگر هستههای Cortex-M که بر اساس معماری Harvard ساخته شده اند. تمایز اساسی بین این دو معماری این است که هسته های Cortex-M0/0+ به حافظه فلش، SRAM و واحدهای جانبی با استفاده از یک گذرگاه مشترک دسترسی دارند، در حالی که هسته های دیگر Cortex-M دارای دو مسیر گذرگاه مجزا برای دسترسی به فلش (یکی برای اجرای دستورالعمل ها به نام instruction bus ، به عبارت ساده I-Bus یا حتی I-Code، و یکی برای دسترسی به دادههای ثابت به نام گذرگاه داده، به عبارت ساده D-Bus یا حتی D-Code) و یک گذرگاه اختصاصی برای دسترسی به SRAM و واحدهای جانبی (که باس سیستم یا به سادگی S-Bus نیز نامیده میشود) هستند.
در هستههای Cortex-M0/0+، DMA و هسته Cortex با استفاده از BusMatrix دسترسی به حافظه و واحدهای جانبی را به چالش می کشند. فرض کنید که CPU در حال انجام عملیات ریاضی بر روی دادههای موجود در registers های داخلی خود (R0-R14) است. اگر DMA دادهها را به SRAM منتقل کند، BusMatrix دسترسی را از هسته Cortex به حافظه فلش، برای بارگذاری و اجرای دستورالعمل بعدی، منتقل میکند. بنابراین هسته در انتظار نوبت خود متوقف میشود. در دیگر هستههای Cortex-M، CPU میتواند به طور مستقل به حافظه فلش دسترسی داشته باشد و عملکرد کلی را افزایش دهد. این یک تفاوت اساسی است که قیمت میکروکنترلرهای STM32F0 را توجیه میکند: آنها نه تنها SRAM و فلش کمتری دارند و در فرکانسهای پایین تر اجرا میشوند، بلکه با معماری ساده تر و ذاتاً عملکرد کمتری روبرو هستند.
DMA_HandleTypeDef in F0/F1/F3/L0/L1/L4 HALs
برنامه نویسی DMA نسبتاً ساده است، به خصوص اگر روشن باشد که DMA چگونه کار میکند. علاوه بر این، CubeHAL برای تسهیل درک جزئیات سخت افزاری طراحی شده است. تمام توابع HAL مربوط به تنظیمات DMA طوری طراحی شده اند که به عنوان پارامتر اول نمونه ای از ساختار C DMA_HandleTypeDef را بپذیرند.
ساختار DMA_HandleTypeDef به شکل زیر در CubeF0/F1/F3/L1 HAL تعریف شده است:
typedef struct {
DMA_Channel_TypeDef *Instance; /* Register base address */
DMA_InitTypeDef
Init; /* DMA communication parameters */
HAL_LockTypeDef
Lock; /* DMA locking object */
__IO HAL_DMA_StateTypeDef
State; /* DMA transfer state */
void *Parent; /* Parent object state */
void (* XferCpltCallback)( struct __DMA_HandleTypeDef * hdma);
void (* XferHalfCpltCallback)( struct __DMA_HandleTypeDef * hdma);
void (* XferErrorCallback)( struct __DMA_HandleTypeDef * hdma);
__IO uint32_t
ErrorCode; /* DMA Error code */
} DMA_HandleTypeDef;
در ادامه قصد داریم مهمترین بخشهای این ساختار را عمیق تر بررسی کنیم:
Instance
یک اشاره گر به توصیفگر DMA/Channel است که قرار است از آن استفاده کنیم. به عنوان مثال، DMA1_Channel5 کانال پنجم DMA1 را نشان میدهد. به یاد داشته باشید که کانالها در طول طراحی MCU به واحدهای جانبی متصل میشوند، بنابراین برای مشاهده کانال متصل به واحد جانبی که میخواهید در حالت DMA استفاده کنید، به دیتاشیت مربوط به MCU خود مراجعه کنید.
Init
نمونهای از ساختار DMA_InitTypeDef است که برای پیکربندی DMA/Channel استفاده میشود.
Parent
این اشاره گر توسط HAL برای پیگیری کنترل کننده های واحد جانبی مرتبط با DMA/Channel فعلی استفاده می شود. به عنوان مثال، اگر از یک UART در حالت DMA استفاده میکنیم، این فیلد به نمونهای از UART_HandleTypeDef اشاره میکند. به زودی خواهیم دید که چگونه کنترل کنندههای واحد جانبی به این بخش “متصل” میشوند.
XferCpltCallback، XferHalfCpltCallback، XferErrorCallback
اینها اشاره گر به توابعی هستند که به عنوان callbacks استفاده میشوند تا به کد کاربر نشان دهند که انتقال DMA تکمیل شده است، نیمه تمام شده یا خطایی رخ داده است. همانطور که در ادامه خواهیم دید، این توابع به طور خودکار، هنگامی که یک وقفه DMA با استفاده از تابع HAL_DMA_IRQHandler توسط HAL رخ میدهد، فراخوانی میشوند.
تمام تنظیمات DMA/Channel با استفاده از نمونهای از ساختار C DMA_InitTypeDef انجام میشود که به صورت زیر تعریف شده است:
typedef struct { uint32_t Direction;
uint32_t PeriphInc;
uint32_t MemInc;
uint32_t PeriphDataAlignment;
uint32_t MemDataAlignment;
uint32_t Mode; uint32_t Priority;
} DMA_InitTypeDef;
Direction
جهت انتقال DMA را تعریف میکند و میتواند یکی از مقادیر جدول زیر باشد.
PeriphInc
همانطور که گفته شد، کنترلکننده DMA دارای یک پورت peripheral است که برای تعیین آدرس رجیستر peripheral درگیر در انتقال حافظه استفاده میشود (به عنوان مثال، برای یک رابط UART آدرس Data Register آن (DR)). از آنجایی که انتقال حافظه DMA معمولاً شامل چندین بایت است، DMA را می توان طوری برنامه ریزی کرد که به طور خودکار رجیستر peripheral را بابت هر بایت ارسالی افزایش دهد. این موضوع هم زمانی که یک انتقال حافظه به حافظه انجام میشود و هم زمانی که واحد جانبی به صورت byte, half-word و word قابل آدرسدهی میباشد (مانند یک حافظه SRAM خارجی) ، صادق است. در این حالت برای PeriphInc میتوان از مقدار DMA_PINC_ENABLE و در غیر این صورت از DMA_PINC_DISABLEاستفاده کرد.
MemInc
این فیلد همان معنای فیلد PeriphInc را دارد، اما درمورد پورت memory است. که میتواند مقدار DMA_MINC_ENABLE را، زمانی که نشانی حافظه مشخص شده باید پس از ارسال هر بایت افزایش یابد، داشته باشد. مقدار DMA_MINC_DISABLE زمانی که نشانی حافظه مشخص شده باید بدون تغییر باشد، اختصاص مییابد.
PeriphDataAlignment
اندازه انتقال دادههای peripheral و memory از طریق این فیلد و قسمت بعدی کاملاً قابل انتخاب هستند و میتوانند یکی از مقادیر جدول زیر را داشته باشند. کنترل کننده DMA طوری طراحی شده است تا به طور خودکار دادهها (packing/unpacking) را زمانی که اندازه دادههای مبدا و مقصد متفاوت است، تنظیم کند. برای اطلاعات بیشتر لطفاً به Reference Manual میکروکنترلر خود مراجعه کنید.
MemDataAlignment
اندازه داده انتقالی حافظه را مشخص میکند و می تواند یکی از مقادیر جدول زیر را داشته باشد.
Mode
کنترل کننده DMA در میکروکنترلرهای STM32 دارای دو حالت کاری است: DMA_NORMAL و DMA_CIRCULAR. در DMA_NORMAL حالت عادی ، DMA مقدار مشخص شده ای از داده را از منبع به پورت مقصد ارسال میکند و فعالیت را متوقف می کند و برای انجام یک انتقال دیگر باید دوباره به اصطلاح مسلح شود. در DMA_CIRCULAR حالت دایره ای، در پایان ارسال، DMA به طور خودکار شمارنده انتقال را ریست کرده و دوباره از اولین بایت بافر در منبع شروع به ارسال میکند (یعنی بافر منبع را به عنوان ring buffer در نظر میگیرد). این حالت، حالت پیوسته continuous mode نیز نامیده میشود و تنها راه برای دستیابی به سرعت انتقال واقعاً بالا در برخی از واحدهای جانبی (مانند دستگاههای SPI با سرعت بالا) است.
Priority
یکی از ویژگی های مهم کنترلکننده DMA، امکان اختصاص اولویتها به هر کانال، جهت کنترل درخواستهای همزمان است. این فیلد میتواند یکی از مقادیر جدول زیر را داشته باشد.
در صورت درخواست همزمان از واحدهای جانبی متصل به کانال های با اولویت یکسان، ابتدا کانال با شماره کمتر فعال می شود.
نحوه اجرای انتقال داده در حالت Polling Mode
هنگامی که channel/stream DMA مورد نیاز خود را پیکربندی کردیم، باید چند کار دیگر را نیز انجام دهیم:
تنظیم آدرسها در پورت memory و peripheral
تعیین مقدار داده ای که قرار است انتقال دهیم
مقداردهی DMA
فعال کردن حالت DMA در واحد جانبی مربوطه
کتابخانه HAL سه بخش اول را با استفاده از تابع زیر اجرا می کند :
HAL_StatusTypeDef HAL_DMA_Start(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddr\ ess, uint32_t DataLength);
در حالی که مرحله چهارم وابسته به واحد جانبی مورد نظر است، و باید به دیتاشیت میکروکنترلر خود مراجعه کنیم. با این حال، همانطور که خواهیم دید، کتابخانه HAL نیز این بخش را انجام میدهد (به عنوان مثال، هنگام پیکربندی UART در حالت DMA از تابع مربوطه HAL_UART_Transmit_DMA() استفاده میکنیم (حال در مثال بعدی میخواهیم یک رشته داده را با استفاده از حالت DMA روی واحد جانبی UART2 ارسال کنیم که مراحل زیر را شامل میشود:
پیکربندی UART2 با استفاده از ماژول HAL_UART
تنظیم کانال DMA1 برای انجام انتقال حافظه به واحد جانبی memory-to-peripheral
آمادهسازی (مسلح کردن) کانال مربوطه DMA برای اجرای انتقال و فعال سازی UART در حالت DMA
مثال زیر برای برد Nucleo-F030 طراحی شده است:
MX_DMA_Init();
MX_USART2_UART_Init();
hdma_usart2_tx.Instance = DMA1_Channel4;
hdma_usart2_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_usart2_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart2_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart2_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart2_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart2_tx.Init.Mode = DMA_NORMAL;
hdma_usart2_tx.Init.Priority = DMA_PRIORITY_LOW;
HAL_DMA_Init(&hdma_usart2_tx);
HAL_DMA_Start(&hdma_usart2_tx, (uint32_t)msg, (uint32_t)&huart2.Instance- >TDR, strlen(msg)\
);
//Enable UART in DMA mode
huart2.Instance->CR3 |= USART_CR3_DMAT;
HAL_DMA_PollForTransfer(&hdma_usart2_tx, HAL_DMA_FULL_TRANSFER, HAL_MAX_DELAY);
//Disable UART DMA mode
huart2.Instance->CR3 &= ~USART_CR3_DMAT;
//Turn LD2 ON
HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_SET);
متغیر hdma_usart2_tx نمونه ای از ساختار DMA_HandleTypeDef است که قبلا دیدیم. در اینجا ما DMA1_Channel4 را برای انتقال memory-to-peripheral پیکربندی میکنیم. از آنجایی که واحد جانبی USART دارای یک رجیستر انتقال داده (TDR) با سایز یک بایت است، DMA را طوری پیکربندی میکنیم که آدرس peripheral به طور خودکار افزایش پیدا نکند (DMA_PINC_DISABLE)، در حالی که میخواهیم آدرس حافظه منبع به طور خودکار بعد از هر بایت ارسال شده افزایش یابد (DMA_MINC_ENABLE). پس از تکمیل پیکربندی، HAL_DMA_Init() را فراخوانی میکنیم که پیکربندی رابط DMA را مطابق اطلاعات ارائه شده در ساختار hdma_usart2_tx.Init انجام میدهد. سپس، تابع HAL_DMA_Start() را فراخوانی میکنیم، که آدرس حافظه منبع (که آدرس آرایه msg است)، آدرس peripheral مقصد (یعنی آدرس USART2->TDR رجیستر) و مقدار دادهای را که میخواهیم انتقال دهیم، پیکربندی میکند.
DMA اکنون آماده برای شروع کار است، و همانطور که نشان داده شده است، با فعال کردن بیت مربوط به واحد جانبی USART2 انتقال داده را شروع میکنیم. در نهایت، توجه داشته باشید که تابع MX_DMA_Init() برای فعال کردن کنترل کننده DMA1 از ماکرو __HAL_RCC_DMA1_CLK_ENABLE() استفاده می کند. (تقریباً هر ماژول داخلی STM32 با استفاده از ماکرو __HAL_RCC_<PERIPHERAL>_CLK_ENABLE() فعال میشود).
از آنجایی که نمیدانیم چه مدت طول می کشد تا فرآیند انتقال کامل شود، از تابع زیر استفاده میکنیم:
HAL_StatusTypeDef HAL_DMA_PollForTransfer(DMA_HandleTypeDef *hdma, uint32_t CompleteLevel, uin\ t32_t Timeout);
که به طور خودکار منتظر تکمیل کامل انتقال است. این روش برای ارسال داده در حالت Polling می باشد. پس از اتمام انتقال، حالت کاری UART2 DMA را غیرفعال کرده و LED LD2 را روشن میکنیم.
نحوه اجرای انتقال داده در حالت Interrupt Mode
از نظر عملکرد، انتقال DMA در حالت Polling بی معنی است، مگر اینکه کد ما نیازی به انتظار برای تکمیل انتقال نداشته باشد. اگر هدف ما بهبود عملکرد کلی است، هیچ دلیلی برای استفاده از کنترلر DMA و سپس صرف سیکلهای متعدد CPU برای انتظار تکمیل انتقال وجود ندارد. بنابراین بهترین گزینه این است که DMA را فعال کنید و اجازه دهید وقتی انتقال کامل شد به ما اطلاع دهد. DMA قادر به ایجاد وقفههای مربوط به فعالیت هر یک از کانالهای خود است (به عنوان مثال، DMA1 در یک MCU STM32F030 یک IRQ برای کانال 1، یکی برای کانالهای 2 و 3، یکی برای کانالهای 4 و 5 دارد). علاوه بر این، سه بیت فعال مستقل برای فعال کردن IRQ در انتقال نیمه کامل، انتقال کامل و خطای انتقال در دارد.
DMA را میتوان در حالت وقفه طی این مراحل فعال کرد:
سه تابع را به عنوان تابع callback تعریف کنید و آنها را به نشانگرهای تابع XferCpltCallback، XferHalfCpltCallback و XferErrorCallback در یک کنترلر DMA_HandleTypeDef ارجاع دهید. (اشکالی ندارد اگر فقط توابعی را که به آنها نیاز داریم تعریف کنیم، اما اشاره گری که نیاز ندارید را NULL قرار دهید، در غیر این صورت ممکن است خطای عجیبی رخ دهد. )
ISR را برای IRQ مرتبط با کانالی که استفاده میکنید وارد کنید و با فراخوانی HAL_DMA_IRQHandler() به کنترل کننده DMA_HandleTypeDef ارجاع دهید.
IRQ مربوطه را در کنترلر NVIC فعال کنید.
از تابع HAL_DMA_Start_IT() استفاده کنید، که به طور خودکار تمام مراحل راهاندازی لازم را برای شما انجام میدهد و میتوانید همان آرگومانهای تابع HAL_DMA_Start را به آن منتقل کنید.
توجه : به طور پیشفرض کتابخانه HAL تمام IRQهای موجود برای یک کانال را فعال میکند، حتی اگر به برخی از آنها احتیاج نداشته باشیم (برای مثال، ممکن است احتیاجی به وقفه انتقال نیمه نداشته باشیم). اگر عملکرد بهینه برای شما مهم بوده، پس نگاهی به کد تابع HAL_DMA_Start_IT() بیندازید و آن را طبق نیاز خود ویرایش کنید. متأسفانه ST تصمیم گرفته است که HAL را به گونه ای طراحی کند که جزئیات زیادی را به کاربر ارائه دهد و این امر باعث کاهش سرعت شده است.
توجه : یک نکته در مورد توابع callback مربوط به XferCpltCallback، XferHalfCpltCallback و XferErrorCallback مهم است: زمانی که از DMA بدون کتابخانه CubeHAL استفاده میکنیم باید آنها را تنظیم کرد. اجازه دهید این مفهوم را روشن کنیم.
فرض کنید که از UART2 در حالت DMA استفاده میکنیم. اگر مدیریت DMA را خودمان انجام میدهیم، بهتر است که توابع callback را تعریف کرده و هر بار که انتقال انجام میشود، تنظیمات مربوط به وقفه UART را مدیریت کنیم. با این حال، اگر از روتینهای HAL_UART_Trasmit_DMA()/HAL_UART_Receive_DMA() استفاده میکنیم، HAL قبلاً آن توابع callback را به درستی تعریف میکند و ما مجبور نیستیم آنها را تغییر دهیم.
47 hdma_usart2_tx.Instance = DMA1_Channel4;
48 hdma_usart2_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
49 hdma_usart2_tx.Init.PeriphInc = DMA_PINC_DISABLE;
50 hdma_usart2_tx.Init.MemInc = DMA_MINC_ENABLE;
51 hdma_usart2_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
52 hdma_usart2_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
53 hdma_usart2_tx.Init.Mode = DMA_NORMAL;
54 hdma_usart2_tx.Init.Priority = DMA_PRIORITY_LOW;
55 hdma_usart2_tx.XferCpltCallback = &DMATransferComplete;
56 HAL_DMA_Init(&hdma_usart2_tx);
57
58 /* DMA interrupt init */
59 HAL_NVIC_SetPriority(DMA1_Channel4_5_IRQn, 0, 0);
60 HAL_NVIC_EnableIRQ(DMA1_Channel4_5_IRQn);
61
62 HAL_DMA_Start_IT(&hdma_usart2_tx, (uint32_t)msg, \
63 (uint32_t)&huart2.Instance->TDR, strlen(msg));
64 //Enable UART in DMA mode
65 huart2.Instance->CR3 |= USART_CR3_DMAT;
66
67 /* Infinite loop */
68 while (1);
69 }
70
71 void DMATransferComplete(DMA_HandleTypeDef *hdma) {
72 if(hdma->Instance == DMA1_Channel4) {
73 //Disable UART DMA mode
74 huart2.Instance->CR3 &= ~USART_CR3_DMAT;
75 //Turn LD2 ON
76 HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_SET);
77 }
استفاده از ماژول HAL_UART با انتقال در حالت DMA
برای فعال سازی USART در حالت DMA باید برخی از رجیسترهای مربوطه را تغییر دهیم تا این حالت فعال شود. ماژول HAL_UART برای راحتی کانفیگ جزئیات سخت افزاری طراحی شده است. مراحل مورد نیاز برای فعال سازی USART در حالت DMA به شرح زیر است:
- پیکربندی channel/stream مورد نظر DMA متصل به UART که می خواهید از آن استفاده کنید.
- پیوند UART_HandleTypeDef به DMA_HandleTypeDef با استفاده از __HAL_LINKDMA();
- وقفه DMA مربوط به channel/stream مورد استفاده را فعال کنید و تابع HAL_DMA_IRQHandler() را از ISR آن فراخوانی کنید.
- وقفه مربوط به UART را فعال کنید و تابع HAL_UART_IRQHandler() را از ISR آن فراخوانی کنید (بسیار مهم، این مرحله را نادیده نگیرید).
- از تابع HAL_UART_Transmit_DMA() و HAL_UART_Receive_DMA() برای تبادل داده ها UART استفاده کنید و آماده باشید که از تکمیل انتقال با اجرای تابع HAL_UART_RxCpltCallback() مطلع شوید.
کد زیر نحوه دریافت سه بایت از UART2 در حالت DMA را در میکروکنترلر STM32F030 نشان میدهد:
uint8_t dataArrived = 0;
int main(void) {
HAL_Init();
Nucleo_BSP_Init(); //Configure the UART2
//Configure the DMA1 Channel 5, which is wired to the UART2_RX request line
hdma_usart2_rx.Instance = DMA1_Channel5;
hdma_usart2_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_usart2_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart2_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart2_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart2_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart2_rx.Init.Mode = DMA_NORMAL;
hdma_usart2_rx.Init.Priority = DMA_PRIORITY_LOW;
HAL_DMA_Init(&hdma_usart2_rx);
//Link the DMA descriptor to the UART2 one
__HAL_LINKDMA(&huart, hdmarx, hdma_usart2_rx);
/* DMA interrupt init */
HAL_NVIC_SetPriority(DMA1_Channel4_5_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel4_5_IRQn);
/* Peripheral interrupt init */
HAL_NVIC_SetPriority(USART2_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USART2_IRQn);
//Receive three bytes from UART2 in DMA mode
uint8_t data[3];
HAL_UART_Receive_DMA(&huart2, &data, 3);
while(!dataArrived); //Wait for the arrival of data from UART
/* Infinite loop */
while (1);
}
//This callback is automatically called by the HAL when the DMA transfer is completed
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { dataArrived = 1; }
void DMA1_Channel4_5_IRQHandler(void) {
HAL_DMA_IRQHandler(&hdma_usart2_rx); //This will automatically call the HAL_UART_RxCpltCallb\ ack()
}
()HAL_UART_RxCpltCallback دقیقاً از کجا فراخوانی میشود؟ در پاراگرافهای قبلی دیدیم که DMA_HandleTypeDef حاوی یک اشاره گر (به نام XferCpltCallback) به تابعی است که توسط روتین HAL_DMA_IRQHandler() پس از تکمیل انتقال DMA فراخوانی میشود. با این حال، هنگامی که ما از ماژول HAL برای یک واحد جانبی خاص استفاده میکنیم (در این مورد HAL_UART)، نیازی به ارائه callback های خود نداریم: آنها به صورت داخلی توسط HAL تعریف میشوند و از آنها برای انجام فعالیتهای خود استفاده میکند. کتابخانه HAL به ما این امکان را میدهد که توابع callback خود را تعریف کنیم (HAL_UART_RxCpltCallback() برای انتقال UART_RX در حالت DMA)، که به طور خودکار توسط HAL فراخوانی میشود، همانطور که در شکل زیر نشان داده شده است. این قانون برای همه ماژول های HAL اعمال میشود.
تنظیمات DMA برای ADC با استفاده از STMCUBEMX
همانطور که در شکل زیر نشان داده شده است، از بخش system core و زیربخش RCC، crystal resonator خارجی را فعال کنید. از قسمت Analog و زیربخش ADC1، IN0 و IN1 را فعال کنید. از تب تنظیمات پارامتر، پارامترهای مورد نیاز را تنظیم کرده و پین PB0 (GPIO_Output) را همانطور که در شکل زیر نشان داده شده است، فعال کنید. از ADC1 بخش Analog و تب تنظیمات DMA، روی دکمه “ADD” کلیک کنید و پارامترها را همانطور که در شکل زیر نشان داده شده است اعمال کنید. از تب پیکربندی Clock ، فرکانس های Clock مناسب را مطابق شکل زیر تنظیم کنید.
روی تب Generate Code کلیک کرده و پروژه Keil uVision را تولید کنید. کد هدر فایل main.c در شکل زیر نشان داده شده است. در نهایت میتوانیم پروژه را کامپایل کرده و یک کد هگز برای میکروکنترلر تولید کنیم.
تنظیمات پروژه Timer، ADC و DMA با استفاده از STMCUBEMX
هدف از این مثال راه اندازی یک تایمر دورهای است که یک ADC را با استفاده از DMA راه اندازی میکند.از آنجایی که فرکانس کلاکTIM3 8 مگاهرتز است، برای تولید رویدادها در 10 هرتز، از Prescaler 800-1 با counter period 1000-1 استفاده می شود. بعد از Prescaler فرکانس 10 کیلوهرتز است. شمارنده پس از 0.1 ثانیه به 1000 می رسد و پس از آن دوباره بارگذاری می شود. یکی از تنظیمات مهم در اینجا «انتخاب رویداد آغازگر: رویداد بهروزرسانی» (Trigger Event Selection: Update Event) می باشد که برای راهاندازی ADC استفاده میشود. از بخش تایمرها و زیربخش TIM3، منبع کلاک را انتخاب کنید و تنظیمات پارامتر های مورد نیاز را انجام دهید.
از قسمت Analogue و زیربخش ADC1، IN1 را فعال کنید. از تب تنظیمات پارامتر، پارامترهای مورد نیاز را همانطور که در شکل زیر نشان داده شده است، تنظیم کنید. از ADC1 بخش Analogue و تب تنظیمات DMA، روی دکمه افزودن کلیک کرده و پارامترها را همانطور که در شکل زیر نشان داده شده است اعمال کنید.
بعد از انجام مراحل بالا بر روی Generate Code کلیک کرده و پروژه مورد نظر را بر اساس تنظیمات انجام شده اعمال میکنیم:
تنظیمات پروژه DMA و USART
در مثال آخر این فصل، از DMA در پروتکل USART استفاده خواهیم کرد. مطمئن شوید که وقفه USART1 را فعال نمیکنید .برنامه Cubemx بخش DMA را راه اندازی خواهد کرد. پس از انجام تنظیمات DMA، اکنون USART را پیکربندی میکنیم.
همانطور که در شکل زیر نشان داده شده است، از تب تنظیمات DMA، DMA را برای پینهای USART3_RX و USART3_TX اضافه کنید.
از تنظیمات NVIC در بخش USART3، وقفههای DMA1 را برای پینهای USART3_RX و USART3_TX فعال کنید:
بعد از انجام مراحل بالا بر روی Generate Code کلیک کرده و پروژه مورد نظر را بر اساس تنظیمات انجام شده اعمال میکنیم:
کد داخل حلقه while در شکل زیر نشان داده شده است. میتوانید در پایین فایل main.c، تابع وقفه را برای یک انتقال کامل شده USART اضافه کنید.