یادگیری برنامه نویسی  STM32 رو از کجا شروع کنم ؟

برای مشاهده جلسه اول به برد بلوپیل و پروگرمر st-link نیاز دارید.

در جلسه اول آموزش stm32  با سه روش پروژه led چشمک زن رو انجام میدیم: 

– دسترسی به رجیستر ها با استفاده از آدرس

– دسترسی به رجیستر ها با استفاده از فایل های cmsis

– تغییر رجیستر ها با استفاده از توابع کتابخونه hal

لینک دانلود جلسه اول

http://dl.elcen.ir/s01/stm32_s01.rar

بخش اول : میکروکنترلر چیه و چطوری کار میکنه؟

میکروکنترلر چیه؟

STM32  ها یک خانواده میکروکنترلر ساخت شرکت ST هستند. قبل از اینکه بریم سراغ STM32 باید بدونیم میکروکنترلر چیه و چطوری کار میکنه. بخش های اصلی هر میکروکنترلر شامل:

هسته، پریفرال ها، حافظه ها و باس ها

فرض کنید میکروکنترلر یک کارخونه مکانیزه است که تعدادی ماشین و فقط یک اپراتور داره. ماشین ها همون پریفرال ها هستند و cpu اپراتور کارخونه است.

دلیل اینکه stm32 ها محبوب هستند این هست که هم پریفرال های (ماشین ها) قدرتمند و هم CPU (اپراتور) قدرتمندی دارند.

حافظه ها و نقشه حافظه

خونه های حافظه 8 بیتی یا یک بایت هستند و هر خونه ی حافظه یک آدرس 32 بیتی داره. شرکت آرم مشخص کرده برای میکروکنترلری که از هر کدوم از cpu هاش استفاده میکنه نقشه ی حافظه چطوری باشه . یعنی آدرس هر بخش از کجا شروع بشه و چقدر حجم بهش اختصاص داده بشه. خونه های حافظه در میکرو برای ذخیره سازی کد، متغیر ها، تنظیمات پریفرال ها و داده های مربوط به پریفرال ها استفاده میشن.

کد رو در حافظه فلش ذخیره میکنیم ، که در F103C8 مقدارش 128kbyte. فلش بخشی از قسمت کد در نقشه ی حافظه است.

SRAM برای ذخیره سازی متغیر ها و داده های مربوط به توابع استفاده میشه. و در میکرو ما 20kbyte و از آدرس 0x20000000 شروع میشه.

بخش دیگه در حافظه وجود داره مربوط به پریفرال ها که تنظیمات پریفرال ها و داده هایی که دریافت میکنند یا خروجی میدن، در این خونه های حافظه نوشته میشه. آدرس خونه های حافظه ی مربوط به پریفرال ها از 0x4000000 شروع میشه. 

هسته چه کاری انجام میده؟

ما یک کد مینویسیم با زبان c  و کامپایلر کد رو به زبان cpu ترجمه میکنه،  یک فایل executable درست میکنه و این فایل در فلش میکروکنترلر نوشته میشه.  وظیفه ی cpu اجرای این کده ، cpu  از اول فلش شروع میکنه و کد رو اجرا میکنه. 

مثال کارخونه رو درنظر بگیرید که CPU اپراتور این کارخونه است. در کد دقیقا نوشته میشه که اپراتور برای تنظیم دستگاهها باید چه کاری انجام بده. cpu با کلاک کار میکنه و با هر کلاک یه عملیات جدید انجام میده ، در خونه های حافظه ی فلش جلو میره و دستورالعمل ها رو اجرا میکنه ، به  مجموعه دستوالعمل هایی که یک cpu میتونه اجرا کنه میگیم instruction set و برای هر cpu متفاوته.

Instruction هایی که cpu های cortexm میتونن اجرا کنن یسری عدد مشخص 16 بیتی هستند.. Cpu  میرسه به یک خونه ی حافظه فلش ، یک عدد 16 بیتی رو از اون خونه میخونه ، داخل cpu یک decoder وجود داره که instruction رو شناسایی میکنه و با توجه به instruction عملیات مورد نیاز رو انجام میده.   هر instruction یک کار کوچیک انجام میده ، مثلا دو تا عدد رو با هم جمع میکنه.

مهمترین instruction ها load و store هستند. با این دو دستورالعمل cpu میتونه عددی رو از یک آدرس بخونه یا عددی رو در جایی از حافظه ذخیره کنه.

پریفرال ها

در کارخونه ی میکروکنترلر تعدادی ماشین وجود داره که هر کدوم برای یک کار خاص طراحی شدن. 

مثلا در یک کارخونه چرخدنده سازی بزرگ، در کارگاهشون ماشین تراش ، بورینگ ، هاب زنی و فرز رو مشاهده میکنید که هر کدومشون یه کنترل پنل دارند که اپراتور با توجه به کاری که میخواد انجام بده تنظیمات رو با استفاده از کنترل پنل هر دستگاه اعمال میکنه تا ماشین، کاری که میخواهیم رو برامون انجام بده. 

در میکروکنترلر هم تعدادی ماشین داریم، مثلا پریفرال gpio , وظیفش تنظیم پین های ورودی و خروجی دیجیتال. پریفرال adc میتونه ولتاژ آنالوگ رو به عدد دیجیتال تبدیل کنه. پریفرال i2c میتونه عددی رو با استفاده از پروتکل i2c ارسال و دریافت کنه.

پریفرال ها چجوری تنظیم میشن؟

دستگاه ها یه سری ورودی تنظیمات دارن و در مورد عملکردشون فیدبک هم در اختیارمون میزارن. فقط اینجا مثل تنظیمات ماشین فرز اهرم نداریم. باید اعداد مشخصی رو در خونه های حافظه ی از پیش تعیین شده ی مربوط به پریفرال بنویسیم تا تنظیمات دلخواهمون رو بدست بیاریم.

اطلاعاتی و تنظیماتی که برای پریفرال انجام میدیم و یا اطلاعاتی که پریفرال بهمون برمیگردونه همشون به شکل اعدادی هستند که در جای مشخصی از حافظه ثبت میشن که به این خونه های حافظه میگیم رجیستر های پریفرال. 

مثلا هر کدوم از پریفرال های gpio هفت تا رجیستر 32 بیتی دارند(هر رجیستر 4 خونه ی حافظه). که cpu برای تنظیم این پریفرال اعداد مشخصی رو در بیت های رجیستر هاش مینویسه. یا برای اطلاع از وضعیت یک پین محتویات رجیستر مربوطه رو میخونه. 

وقتی میکرو روشن میشه و شروع به کار میکنه هسته مثل اپراتور یک کارخونه باید تنظیمات همه ی ماشین ها (پریفرال ها) رو انجام بده.

CPU با استفاده از دستورات load و store اعداد رو در رجیستر های پریفرال ها مینویسه و یا میخونه. اینطوری تنظیمات پریفرال ها رو انجام میدیم و میتونیم خروجیشون رو هم دریافت کنیم.

آدرس رجیستر های پریفرال ها

آدرس شروع رجیستر های پریفرال ها از 40 میلیون هگز شروع شده ، به هر پریفرال هم 1kb حافظه اختصاص داده شده. آدرس هایی که اینجا نوشته شده آدرس اولین رجیستر هر کدوم از این پریفرال هاست، بهش میگیم base address اون پریفرال. مثلا base address  تایمر 2   0x4000 0000 . این آدرس همشون پر نشدن برای مثال تا آدرس 0x4000 0400 به tim2 اختصاص داده شده. این فضا 1kbyte ، البته تایمر2 ، با 22 تا رجیستر فقط 88byte  رو اشغال میکنه.

برای پیدا کردن آدرس هر رجیستر باید base addredss پریفرال رو بعلاوه ی address offset اون رجیستر کرد. address offset برای هر رجیستر در register map پریفرال نوشته شده. مثلا جدولی که میبینید register map پریفرال gpio که همه ی رجیستر های gpio و offset هر کدوم در این جدول مشخص شده.

باس ها

در میکرو چندین باس داریم که داده ها بوسیله اونها جابجا میشن. باس ها سیم هایی هستند که برای انتقال داده بین حافظه ها و CPU استفاده میشن. مثلا سه باس AHB ، APB1 و APB2 کار انتقال داده بین رجیستر های پریفرال ها و CPU رو انجام میدن. 

پریفرال GPIOC روی باس APB2 قرار داره ، یعنی داده هایی که بین CPU و رجیستر های این پریفرال جابجا میشن از روی باس APB2 عبور میکنند.

میکرو ما 32 بیتی این به این معنا است که این میکرو میتونه داده های 32 بیتی رو در یک لحظه ، یعنی با یک عملیات جابجا کنه ، به عبارت دیگه دیگه عرض باس انتقال داده 32 بیته.

بخش دوم : پریفرال GPIO

بلوک دیاگرام GPIO

پین هایی که به عنوان ورودی و خروجی میتونن استفاده بشن، 37 عدد هستند که متعلق به چهار تا پورت اند. این ساختاری که میبینید پشت این 37 تا پین وجود داره، دیود های پروتکشن برای جلوگیری از آسیب به میکرو در مقابل ولتاژ کم یا زیاده، دیود بالایی اجازه نمیده که ولتاژ روی پین از vdd+0,7 بیشتر بشه و دیود پایینی اجازه نمیده ولتاژ روی پین از vss-0.7 کمتر بشه. Input درایور شامل مقاومت های pull up  و pull down که میتونیم انتخاب کنیم که ازشون استفاده کنیم یا نه، اگر پین به عنوان ورودی آنالوگ تنظیم شده باشه، اینجا مسیرش جدا میشه، در غیر اینصورت از یه مدار اشمیت تریگر عبور میکنه، پین ممکنه به عنوان gpio تنظیم نشه و کنترلش در اختیار سایر پریفرال ها باشه که alternate function  همونه.   و در صورتی که پین ورودی دیجیتال باشه صفر یا یک بسته به اینکه وضعیت پین low با high باشه در یک بیت مشخص دخیره میشه در input data register .

خروجی هم به این صورته که هر کدوم از پین های پورت یه بیت مشخص در رجیستر odr دارن که اگر یک باشه وضعیت روی پین هم high یا V3.3 و اگر 0 باشه وضعیت پین LOW یا ولتاژش 0. Output driver ورودیش رو از این رجیستر یا سایر پریفرال ها میگیره و میتونه خروجی push pull  یا opern drain  تولید کنه.

Gpioa و B,C,D سه تا پریفرال مشابه اند که هر کدوم یک ست از این رجیستر ها دارند، برای کنترل پین هاشون.

خروجی Open drain 

در حالت open drain از یک nmos استفاده میکنیم و بوسیله ی اون فقط میتونیم پین که در حالت خروجی تنظیم شده به زمین متصل کنیم ، پین یا به زمین متصل شده یا به جایی وصل نیست که در این حالت به اون high impedance  یا  float میگیم و وضعیت روی پین مشخص نیست.

با استفاده از یک مقاومت pull-up میشه حالت float رو حذف کرد و حالا با قطع و وصل کلید، وضعیت روی پین بین زمین و pull-up جابجا میشه. از کلید اینجا برای مدل کردن nmos استفاده شده.

خروجی push/pull

حالت push/pull با دو تا ترانزیستور یکی nmos ویکی pmos کار میکنه . مدلش مثل یک کلید spdt  و میتونیم پین که در حالت خروجی تنظیم شده رو هم به vcc و هم به gnd متصل کنیم .

در حالت pull phase فقط nmos فعاله ، خروجی به زمین متصله و میکروکنترلر جریان رو sink  میکنه، یعنی جهت جریان به سمت داخل میکروکنترلره.

در حالت push phase فقط pmos فعاله ، خروجی به vcc متصله  و میکرو  جریان رو source میکنه. 

خروجی open drain در کجا استفاده میشه؟

در جاهایی که چندین دستگاه روی یک سیم کنترل دارند مثل باس های ارتباط i2c ، اصلا لازم نیست و حتی امن نیست که دستگاهها بتونن باس رو یک کنن ، در این مواقع باس پول آپ میشه و ic ها فقط میتونن باس رو زمین کنن، اگر هیچ کدوم از آیسی ها باس رو زمین نکنه، باس پول آپ میمونه. اگر پین ها push pull  باشن ممکنه یه مشکلی بوجود بیاد. در نظر بگیرید یک آیسی بخواد باس رو زمین کنه و یه آی سی بخواد باس رو یک کنه اینطوری زمین به vdd وصل میشه، در حالتی که پین آ سی ها open drain  باشه از این اتفاق جلوگیری میشه.

تنظیمات GPIO ، رجیستر port configuration

پریفرالهای gpio برای مشخص کردن تنظیمات پین هاشون دو تا رجیستر داره. دو رجیستر gpiox_crl و gpiox_crh اولی برای پین 0 تا 7 و دومی برای تنظیمات پین 8 تا 15 در نظر گرفته شدن.

 برای هر پین 4 بیت تنظیمات وجود داره دو پارامتر mode و cnf که هر کدوم دو بیت هستند. اگر mode  رو  00 تنظیمش کنیم پین در حالت ورودی و اگر 01 و 10 و 11 تنظیمش کنیم پین در حالت خروجی با ماکزیمم speed 10 و 2 و 50mhz .

دو بیت cnf برای مشخص کردن تنظیمات در حالت ورودی و خروجی استفاده میشن. اگر mode 00 باشه ، یعنی در حالت ورودی ، cnf 00 پین رو در حالت ورودی آنالوگ تنظیم میکنه ، Cnf 01 پین رو در حالت floating input و cnf10 پین رو در حالت ورودی با pull up  و pull down تنظیم میکنه.

وقتی mode غیر از 00 باشه. یعنی در حالت output . اگر cnf 00  و 01 باشه به ترتیب پین در حالت خروجی دیجیتال push pull و open drain تنظیم میشه. و اگر 10و 11 باشه در حالت خروجی alternate functon  ، pp و od تنظیم میشه.

تنظیمات GPIO سرعت

سرعت gpio در حالت output که در اسلاید قبلی تنظیماتش رو بررسی کردیم ، در حقیقت تنظیم slew rate یعنی تنظیم اینکه با چه سرعتی سیگنال میتونه از 0 به 3.3 برسه ، یعنی rise time یا از 3.3 به 0 برسه ، یعنی fall time .

با توجه به این تنظیم و اینکه کلاک میکرو چقدر باشه و اینکه کد رو چظور بنویسیم ماکزیمم سرعت تاگل کردن gpio مشخص میشه. که طبق دیتاشیت میتونه تا 50mhz برسه.

Speed رو همیشه کمترین مقدار ، مقدار low تنظیم کنید مگر اینکه یه دلیل مشخصی داشته باشه، اینطوری نویز کمتری خواهید داشت ،

تنظیمات GPIO – رجیستر ODR و BSRR

برای تغییر وضعیت خروجی باید محتویات odr تغییر کنه. رجیستر دیگه ای داریم به نام  gpioc_bsrr که کارش تغییر محتویات odr . صفر نوشتن توی بیت های این رجیستر کاری انجام نمیده. یک کردن بیت 0 تا 15 , پین با همین شماره از gpioc رو یک میکنه و یک کردن بیت 16 تا 31   پین رو صفر میکنه. در بیت های این رجیستر کلمه ی w نوشته شده به این معنا که این فقط میشه در این رجیستر نوشت.

تنظیمات GPIO – ورودی pull up و pull down

تنظیمات رو برای gpio  در حالت ورودی با pull up و pull down بررسی میکنیم.

برای تنظیم پین در حالت ورودی mode باید 00 باشه. و برای pu یا pd cnf باید 10 باشه .

تنظیمات pu یا pd هم در جدول 20 مشخص شده. و در حالت ورودی  با رجیستر odr pu یا pd رو مشخص میکنیم.

بخش دوم : پریفرال RCC

پریفرال RCC

مثلا هر کدوم از پریفرال های gpio هفت تا رجیستر 32 بیتی دارند(هر رجیستر 4 خونه ی حافظه). که cpu برای تنظیم این پریفرال اعداد مشخصی رو در بیت های رجیستر هاش مینویسه. یا برای اطلاع از وضعیت یک پین محتویات رجیستر مربوطه رو میخونه. 

وقتی میکرو روشن میشه و شروع به کار میکنه هسته مثل اپراتور یک کارخونه باید تنظیمات همه ی ماشین ها (پریفرال ها) رو انجام بده.

CPU با استفاده از دستورات load و store اعداد رو در رجیستر های پریفرال ها مینویسه و یا میخونه. اینطوری تنظیمات پریفرال ها رو انجام میدیم و میتونیم خروجیشون رو هم دریافت کنیم.

بخش چهارم : چجوری به محتویات آدرس دسترسی پیدا کنیم و چطور تغییرش بدیم؟

کست کردن 

Type casting  برای تغییر نوع داده و یا مشخص کردن datatype یک عدد در زبان c استفاده میشه، تابع my function رو در نظر بگیرید که یک ورودی از نوع integer داره، میتونیم یک متغیر integer به نام y تعریف کنیم و اون رو به عنوان ورودی به تابع بدیم.

اگر بخواهیم عدد چهار رو مستقیما به تابع بدیم بعضی وقتها لازمه به کامپایلر بفهمونیم که عدد چهار integer. برای این منظور عدد 4 رو cast میکنیم ، یه پرانتز قبلش میزاریم و داخل پرانتز نوعش رو مینویسم.

پوینتر

خونه های حافظه 8 بیتی اند و هر خونه ی حافظه یک آدرس 32 بیتی داره. یه مثال رو بررسی میکنیم تا در طولش با *  و & آشنا بشیم.

ابتدا متغیر Y  رو از نوع uint تعریف و مقدار دهی میکنیم، که 32 بیته و 4 تا خونه ی حافظه رو اشغال میکنه. 

در مرحله ی بعد یک پوینتر تعریف میکنیم ، پوینتر یک متغیره که مقدارش آدرس یه جایی از حافظه است. همه پوینتر ها هم 32 بیتی اند. در تعریف پوینتر مشخص میشه که این پوینتر به آدرس چه نوع متغیری اشاره میکنه، ابتدا نوع متغیر نوشته میشه و بعد از اون علامت استریسک یا ستاره قرار میگیره. تا اینجا یک متغیر از نوع پوینتر تعریف شده و چون 32 بیته چهار تا خونه حافظه هم اشغال کرده ولی چون هنوز مقدار دهی نشده مقدارش valid نیست.

تو خط بعد آدرس متغیر Y رو تو ptr میذاریم و پوینتر رو مقدار دهی میکنیم. وقتی علامت & یا Reference Operator رو پشت یک متغیر بزاریم آدرسش رو برمیگردونه، آدرس Y آدرس اولین خونه ی حافظه است که این متغیر اشغال کرده، آدرس Y میاد توی چهار تا خونه ی حافظه ی ptr .

توی خط بعد عملکرد Dereference Operator رو میبینیم، با استفاده از اون میتونیم بصورت غیر مستقیم Y رو مقدار دهی کنیم، اگر علامت استرسیک رو قبل از ptr بزاریم خود متغیر Y رو برمیگردونه و میتونیم محتویات آدرسی که پوینتر بهش اشاره میکنه رو تغییر بدیم.

نوع داده ی * ptr قبلش نوشته شده unsigned int ، نوع داده ی ptr هم unsigned int * که پوینتره.

کست کردن آدرس 

ما آدرس رجیستر ها رو داریم و میخواهیم مقدارش رو تغییر بدیم، فرض کنید محتویات یک رجیستر 32 بیتی با آدرس 0x10000000 رو بخواهیم تغییر بدیم. میتونیم یه پونتر تعریف کنیم و مقدارش رو مساوی آدرس رجیستر قرار بدیم و در مرحله بعد با استفاده از Dereference Operator آدرس مورد نظر رو مقدار دهی کنیم.

ولی ضروی نیست که اینجا متغیر ptr رو تعریف کنیم و بهتر هم هست که این کار رو نکنیم چون الکی چهار تا خونه حافظه رو میگیره. اینجا بجای ptr میخواهیم خود آدرس رو بزاریم ، برای اینکار باید آدرس رو کست کنیم تا کامپایلر بفهمه عددی که اینجا نوشتیم پوینتره.

بعد از ستاره بجای ptr یک پرانتز میزاریم، داخل پرانتز آدرس رو مینویسیم و برای کست کردنش تو یک پرانتز قبل از آدرس نوعش رو مینویسم ، نوع پوینتر همین بخش آبی رنگه که قبل از ptr تو تعریفش نوشتیم .

Bitwise Operators

تا اینجا میدونیم چطور به محتویات آدرس دسترسی پیدا کنیم . در ادامه میبینیم چطور میشه محتویات آدرس رو تغییر داد. برای تغییر محتویات یک آدرس از bitwise operator ها استفاده میکنیم . عملکرد اینها رو با چند تا مثال بررسی میکنیم.

Bitwise Operators مثال اول

در مثال اول هدف اینه که بیت چهارم رجیستر A رو یک کنیم. برای اینکار به یک عددی نیاز داریم که فقط بیت چهارمش یک باشه برای تولید این عدد ، با استفاده از عملگر left shift عدد یک رو چهار بار به سمت چپ شیفت میدیم که نحوه ی نوشتنش در مستظیل آبی مشخصه ، عددی که تولید کردیم یعنی v رو با A ، bitwise or میکنیم، اینطوری بیت ها یدونه یدونه با هم or میشن، بغیر از بیت چهار A بقیه بیت ها با 0  or میشن، مقدار این بیت ها 0 یا 1 باشه وقتی با 0 or بشه خودش میشه. بیت چهارم وقتی با 1 or میشه، نتیجه یک میشه.

حاصل bitwise or رجیستر A که بیت چهارمش یک شده. این عدد رو دوباره میریزیم تو خود A .

Bitwise Operators  مثال دوم

در این مثال دو تا از بیت های یک رجیستر رو میخواهیم یک کنیم، بیتهای  20 و 21 . میدونیم باید عددی بسازیم که فقط بیت 20 و 21 یک باشه، یک راه حل اینه که 3 رو که میشه 11 بیست بار به سمت چپ شیفت بدیم. راه حل خوانا تر رو اینجا میبینید مثل اینه مثال قبل رو دو بار تکرار کنیم. 

Bitwise Operators  مثال سوم

تو این مثال قراره بیت 22 و 23 یک رجیستر رو 0 کنیم. برای اینکار نیاز به عددی داریم که فقط بیت 22 و 23 ، 0 باشن، اول با استفاده از left shift مثل مثال قبل یه عدد میسازیم که بیت 22 و23 ،1باشن و بعد این عدد رو not میکنیم، 0 ها 1 میشن و 1 ها 0 میشن و اینطوری عددی که میخواستیم رو بدست میاریم. وقتی این عدد با A bitwise and  میشه، بغیر از بیت 22و 23 بقیه بیت ها با 1 and میشن، 0و1 هر کدوم با 1 and بشن مقدارش خودش میشه ولی هر عددی با 0 and بشه نتیجه 0 میشه.

تا اینجای کار فهمیدیم که برای اینکه هر کدوم از این کار ها رو انجام بدیم باید چه کدی بنویسم ، الان میریم سراغ پیاده سازی.

بخش پنجم : پیاده سازی پروژه led چشمک زن با تغییر مستقیم محتوای آدرس رجیستر ها

بخش ششم : استفاده از structure برای دسترسی به رجیستر های پریفرال ها

typedef

typedef برای تعریف یک نوع جدید متغیر استفاده میشه. در این مثال uint32_t نوع جدید متغیر که تعریف شده و unsigned int نوع قبلی متغیره که uint32_t بر مبنای اون تعریف شده.

structure

Structure هم یک نوع داده است که توسط کاربر مشخص میشه. توی مثال اول ابتدا یک structure با تگ mystruct  تعریف شده که شامل دو تا متغیره x و y از نوع uint32_t ، برای تعریف structure  اول کلمه کلیدی struct و بعد تگ structure که اینجا mystruct و بعد هم در یک کروشه محتویات استراکچر مشخص میشن. استراکچرمون اینجا دو تا عضو x,y رو داره. 

قبل از تابع main  هنوز هیچ فضایی تو حافظه اشغال نمیشه چون نوع متغیر رو تعریف کردیم و هنوز متغیری تعریف نشده. تو تابع main دو تا استراکچر ms1 و ms2 تعریف و مقدار دهی شدن. این دو تا local تعریف شدن . یعنی در یک تابع تعریف شدن که اینجا تابع main و در سایر توابع نمیتونیم ازشون استفاده کنیم.

برای تعریف استراکچر های ms1 و ms2   کلمه ی کلیدی استراکچر ، تگ استراکچر و بعد هم اسامی که برای این دو متغیر تعریف کردیم. دو تا متغیر ms1 و ms2 تعریف میشن که هر کدوم دو تا عضو x  و y از نوع uint32 دارن.

برای تعریف استراکچر های ms1 و ms2   کلمه ی کلیدی استراکچر ، تگ استراکچر و بعد هم اسامی که برای این دو متغیر تعریف کردیم. دو تا متغیر ms1 و ms2 تعریف میشن که هر کدوم دو تا عضو x  و y از نوع uint32 دارن.

در مثال بعد در تعریف استراکچر ، ms1 و ms2 هم تعریف شدن. یه روش دیگه برای تعریف استراکچر های ms2 و ms2 اینه که بعد از کروشه دوم در تعریف خود استراکچر اسامی ms1 و ms2 رو بنویسیم. در این مثال این متغیر global تعریف شدن و در همه ی توابع میشه ازشون استفاده کرد.

میشه typedef و structure رو با هم ترکیب کرد .اینطوری میتونیم با استفاده از یک نوع جدید متغیر  ms1 , ms2 رو تعریف کنیم،

اینطوری به تگ استراکچر هم  نیازی نیست.

ابتدا یک نوع جدید متعیر تعریف میکنیم به نام mystruct_typedef  ، typedef نوع قبلی نوع جدید ، نوع قبلی یک استراکپره که تگ استراکچر هم حذف شده. الان برای تعریف ms1 و  ms2 میتونیم از نوع جدید mystruct_typedef   کنیم. همونطور که با unsigned int  متعیر تعریف میکنیم. با mystruct_typedef  هم میتونیم متعیر تعریف کنیم.

فرض یک استراکچر به نام ms1 تعریف کردیم که اعضای x,y,z,t رو داره  که همشون 32 بیتی هستند و هر کدوم 4 بایت رو اشغال میکن . Ms1 یک متغیره که 16 بایت و 16 تا خونه ی حافظه رو پشت سر هم اشغال کرده. آدرس ms1 آدرس اولین خونه ی حافظه ی این متغیره ، آدرسی که  سبز رنگه.

همیشه هم اینطوری نیست که اعضای استراکچر پشت سر هم چیده بشن، ولی در مورد کاری که ما قراره انجام بدیم اینطور هست.

میدونیم که 7 تا رجیستر gpioc 28 تا خونه حافظه رو پشت سر هم اشغال کردن که از آدرس 0x40 011 000 که base address gpioc هست شروع میشه.

 یک نوع متغیر جدید تعریف میکنیم به نام gpio_typedef که شامل 7 تا متغیر 32  بیتی باشه مثل رجیستر های gpio. متغیر تعریف نکردیم بلکه نوع متغیر تعریف کردیم.

قبلا برای مقدار دهی gpioc_crl ، اولین رحیستر gpioc باید آدرسش رو در قالب یک پوینتر به unsigned int  کست میکردیم.  چون محتویات آدرسی که میخواستیم مقدار دهی کنیم 32 بیتی بود ، unsigned int  

میتونیم base address  gpioc رو به عنوان یه پوینتر به gpio_typedef  کست کنیم. الان محتویات آدرسی که میخواهیم بهش دسترسی پیدا کنیم یک استراکپره که 7 تا عضو 32 بیتی داره.

 با defrefrence operator  هم میتونیم به محتویات آدرس که اینجا استراکچره دسترسی پیدا کنیم. مثل قبل برای دسترسی به اعضای استراکچر میتونیم از کروشه یا دات استفاده کنیم. در نظر داشته باشد در صورت استفاده از دات حتما باید یه پرانتز دیگه هم بزاریم. بخش های سبز اینجا پوینتر اند. 

تا الان عدد آدرس رو کست کردیم در قالب یک پوینتر که محتویات آدرسش از نوع gpio_typedef که یک استراکچره. وقتی ستاره بداریم قبل از پوینتر به محتویات آدرس دسترسی پیدا میکنیم . برای دسترسی به اعضای استراکچر ، ستاره پوینتر رو میداریم تو پرانتز و بعد دات میذاریم. برای دسترسی به اعضای structure  به جای  پرانتز دات میشه از عملگر میخ استفاده کرد و پوینتر از پرانتز بیرون میاد.

تا الان میدونیم برای دسترسی به رجیستر های پریفرال ها لازم نیست که آدرس رجیستر ها رو داشته باشیم ، تنها آدرسی که نیاز داریم ، آدرس اولین رجیستر پریفرال ، BASE ADDRESS پریفرال . برای هر پریفرال یک TYPEDEF تعریف میکنیم. که اعضای اون استراکچر رحیستر های اون پریفرال هستند . فقط باید استراکچر رو با توجه به رجیستر های هر پریفرال تنظیم کنیم . 

بخش هفتم : مراحل کامپایل ، پریپراسسور ، فایل های .h

مراحل کامپایل

در سمت راست مراحل کامپایل یک کد در C رو میبینید ، اولین مرحله در کامپایل پریپراسسور  این مرحله قبل از کامپایله و صحت کد بررسی نمیشه، در این مرحله پریپراسسور دیرکتیو ها یا دستورات پریپراسسور اجرا میشن.

این مرحله زبان برما نویسی خودش رو داره و با این دستورات دستورات میتونن عباراتی رو در کد جایگذاری کرد، یا کل یک فایل رو در مکان های دلخواه قرار بدن یا با استفاده از یک شرط مشخص کرد یه بخشی در کد قرار بگیره یا نه. با این دستورات قبل از اینکه کد کامپایل بشه میشه متن کد رو تغییر داد. همشون هم با علامت # شروع میشن.

مرحله بعد اسمش compiler در این مرحله خروجی مرحله ی پریپراسسور به instruction های مخصوص هر device ترجمه میشه.

در مرجله assembler هم خروجی مرحله ی compiler تبدیل به کد 0 و 1  که میتونه در فلش نوشته بشه.

هر فایل source code با پسوند .c این سه مرحله رو طی میکنه. و تبدیل به یک فایل object code با پسوند .o میشه. در مرحله ی linker فایل های .o به هم وصل میشن و همچنین اگر از کتابخنه های استاندارد زبان c استفاده کرده باشیم در این مرحله به کد اضافه میشه. خروجی مرحله ی linker  executable code که در فلش نوشته میشه و cpu اون کد رو اجرا میکنه.

ماکرو

پریپراسسور c یک macro preprocessor یعنی به ما اجازه میده ماکرو تعریف کنیم، و  بوسیله ی ماکرو ها کد رو قبل از کامپایل تغییر بدیم، ماکرو یک بخشی از کد که یک اسم بهش اختصاص داده شده، و در کد بجای اینکه یک عبارت یا یک عدد رو بنویسیم میتونیم اسم ماکرو که کوتاه تره بنویسیم و این اسم در مرحله پریپراسسور با عبارت مورد نظرمون جایگداری میشه.

define  یکی از preprocessor directive  و با استفاده از define میتونیم دو نوع ماکرو تعریف کنیم. Object-like Macros و function like macro . Function like macro  ها مثل تابع هستند و ورودی میگیرن و بعد از اونها یک جفت پرانتز میاد . در این اسلاید Object-like Macros ها رو بررسی میکنیم. این ماکرو ها میتونن ثابت های عددی یا رشته باشند. یا یک عبارت باشند.

مثلا برای اینکه هر بار در محاسباتمون لازم نباشه عدد 3.14 رو بنویسیم میتونیم pi رو دیفاین کنیم 3.14  ، #define pi  3.14. اینطوری هر جا نوشته باشیم pi در مرحله پریپراسسور با 3.14 جایگزین میشه.

دیفاین کردن آدرس کست شده

قبلا برای دسترسی به آدرس رجیستر های gpioc  آدرس رو در قالب پوینتر به gpio_typedef کست میکردیم. با استفاده از define میتونیم کد رو خوانا تر بنویسیم .

میتونیم آدرس کست شده رو دیفاین کردیم. هر جا در کد بنویسیم gpioc با عبارت روبروش جایگزین میشه که آدرس رحیستر های gpioc . Gpioc در کد یک پوینتره به رجیستر های پریفرال gpioc .

با اضافه کردن این چند خط قبل از  تابع main با استفاده از عملگر میخ خیلی راحت به تمام رجیستر های gpioc  دسترسی پیدا میکنیم. به جای عدد کست شده به آدرس رحیستر های gpioc مینویسیم gpioc  .

عبارت gpioc یک پوینتره به رجیستر های gpioc که محتویات آدرسش از نوع gpio_typedef .

Function like macro

Function like macro عملکردشون مشابه تابعه و یه جفت پرانتز هم بعدشون میاد که ورودی هاش در اون نوشته میشه. در قالب یک مثال عملکردشون رو بررسی میکنیم.

کاری که قراره انجام بشه فعال کردن کلاک gpioc .که برای انجام این کار باید بیت 4 رجیستر apb2enr رو یک کنیم. کتابخونه hal  که در آینده باهاش کار میکنیم برای اینکار ماکرو داره .

برای فعال کردن کلاک gpioc از ماکرو   __HAL_RCC_GPIOC_CLK_ENABLE(); استفاده میکنیم. این ماکرو در یکی از فایل های کتابخونه hal define شده و در مرحله پریپراسسور با یک do while جایگزین میشه . در این do while از یک ماکرو دیگه استفاده شده به نام set bit که دو تا ورودی داره به نام های رج و بیت و ورودی اولش or= ورودی دوم میشه. ورودی اول اینجا رجیستر rcc_apb2enr از رحیستر های پریفرال rcc که با این رجیستر کلاک پریفرال های روی باس apb2 رو فعال میکنیم. و ورودی دوم یک عدد 32 بیتی که بیت چهارمش یکه. و وقتی رجیستر apb2enr با این عدد or بشه کلاک gpioc فعال میشه.

Function like  ماکرو ها چند خط کد هستند که یک اسم بهشون اختصاص داده میشه و در مرحله ی پریپراسسور اسم با کد اصلی جایگزین میشه. وقتی از ماکزو  __HAL_RCC_GPIOC_CLK_ENABLE();  استفاده میکنیم بعد از پریپراسسور تبدیل میشه به یک do while که در اون  بیت چهارم rcc_apb2enr یک شده.

فایل های c. و h.

 در پروژه ها با دو نوع فایل سر و کار داریم فایل های با پسوند .c و .h . فایل های .c فایل های   source code هستند و کد اصلی و توابع اونجا نوشته میشن. فایل main.c تنها فایل در پروژه است که تابع  main رو داره. Cpu  بعد از اجرای startup وارد تابع main  میشه و  بقیه توابع مستقیم یا غیر مستقیم از تابع main فراخوانی میشن. فایل های .c شامل توابع عستند و همچنین ماکرو ها ، تعریف متغیر های گلوبال ، typedef و پروتوتایپ توابع هم در این فایل نوشته میشن.

Header file ها فایل های با پسوند .h هستند که typedef  ها و ماکرو ها و پروتو تایپ توابع در این فایل ها نوشته میشن. وقتی به محتویات این فایل ها در یک فایل source code نیاز باشه اول فایل .c include میشن . Include یکی دیگه از دستورات پریپراسسور   و محتویات فایل .h رو بصورت کامل اول فایل .c کپی میکنه.

دو نوع فایل .h داریم که هر دو با include اضافه میشن. فایل های .h که توسط user نوشته میشن با فرمی که در خط اول نوشته شده اصافه میشن و در یک گیومه اسمشون نوشته میشه.

یسری فایل های .h  هم هستند که جز کتابخونه های زبان c هستند و خود کامپایلر اینها رو داره اگر نیاز داشته باشیم از ماکرو ها یا توابع اینها استفاده کنیم با فرم خط دوم این فایل ها رو include می کنیم.

محدودیت های فایل header file

در حقیقت همه چیز رو میشه در فایل .h نوشت ولی وقتی یک فایل .h در ابتدای چند تا فایل .c include  بشه در مواردی محدودیت داریم . مثلا نباید تعریف متغیر ها رو در فایل .hنوشت  . چون معمولا فایل های .h در ابتدای چندین فایل c . قرار میگیرند و یک متغیر چندین بار تعریف میشه و  بهش فضا اختصاص داده میشه و ارور خواهیم داشت.

همچینین تعریف تابع یا بدنه ی تابع رو نباید در فایل .h نوشت چون وقتی به چند تا فایل .c اضافه بشه تابع چندین بار تعریف میشه که منجر به ارور میشه.

 فایل های .h شامل دیتا تایپ، define ها ، ماکرو ها ، پروتایپ توابع هستند که به چند تا فایل .c اضافه میشن. بجای اینکه در هر فایل .c بنویسیمشون در یک فایل .h مینویسیم و این فایل رو اول چند تا فایل .c include میکنیم.

دستورات شرطی پریپراسسور

تا بحال define و include رو از دستورات preprocessor بررسی کردیم. دستور define برای ساخت یه چیزی شبیه flag هم بکار میره. Define sth ، جاهای دیگه در کد میتونیم چک کنیم که عبارته مورد نظرمون تا به حال define  شده یا نه؟

دستورات شرطی ifdef و ifndef همین کار رو انجام میدن. تنها در صورتی که عبارت روبروی Ifdef  تا بحال define شده باشه محتویات بین ifdef تا endif اجرا میشه. .  هر دو دستور به یک endif بعدشون نیاز دارند.در این مثال انتخاب میشه که چه فایلهایی include بشن. اگر به فایل …. نیاز باشه ، قبلا این عبارت define میشه.

header guard

مثال بعد Include guard یا header guard که برای جلوگیری از تعریف مجدد محتویات فایل .h استفاده میشه. پیش میاد که یک فایل .h چندین بار اول یک فایل .c قرار بگیره، اینطوری مثلا define های توی این فایل چندین بار define  میشن.

برای جلوگیری از این داستان یک عبارت رو در فقط در فایل .h مورد نظر define میکنیم. در ابتدای فایل با دستور ifndef چک میکنیم که آیا __STM32F1xx_HAL_H  تا بحال define شده یا نه؟ کل فایل .h بین ifndef  و endif نوشته میشن. منطق ifndef برعکس ifdef و  اگر تا بحال define شده باشه ، هر چیزی که بین ifndef  و endif باشه نادیده گرفته میشه. چون این فایل .h قبلا به فایل .c اضافه شده.

بخش هشتم : پروژه led چشمک زن ، تغییر رجیستر ها با کمک از struct ، typedef و define

بخش نهم : فایل های CMSIS

CMSIS

برای انجام پروژه تعدادی فایل رو به پروژه اضافه کردیم ، در این بخش این فایل ها رو بررسی میکنیم. Cmsis  یک لایه نرم افزاری که نیازمندی های اولیه پروگرم کردن فلش و همچنین دسترسی به رجیستر ها رو برای میکروکنترلر هایی که cpu   arm cortex دارند فراهم میکنه. یه لایه ی نرمافزاری که سایر کتابخونه ها بر اساس اون نوشته میشه . مثلا کتابخونه hal از cmsis استفاده میکنه. 

4 تا فایل مهم داره cmsis که اینها رو بررسی میکنیم.

هر بار که میکرو ریست میشه. تابع reset handler فراخوانی میشه که این تابع در فایل startup نوشته شده. این تابع ابتدا تابع system init  رو فراخوانی میکنه که این تابع تنظیمات مربوط به کلاک رو انجام میده و در فایل system نوشته شده. بعد از اون reset handler تابع main رو فراخوانی میکنه و بقیه کد اجرا میشه.

فایل start up  یک فایل assembly و پسوندش .s ، فایل system هم یک فایل source code که پسوندش .c . تا الان این دو تا فایل هم بخشی از پروژه مون بودن و اجرا میشدن.

تا الان برای تغییر محتویات رجیستر ها ایتدا از آدرس استفاده کردیم و بعد از typedef و define کارمون خیلی راحت تر شد. یک فایل وجود داره به نام device peripheral access layer header file  که typedef ها و define های مورد نیاز برای دسترسی به رجیستر های تمام پریفرال های میکروکنترلر رو داره  رو داره و لازم نیست خودمون تعریفشون کنیم.

یک فایل .h دیگه هم وحود داره که برای cpu ما اسمش core_cm3.h و typedef و ماکرو داره برای دسترسی به رجیستر های پریفرال ها ی  خود cpu .

بخش دهم : تابع و کتابخونه

تابع چیه؟

تابع تعدادی خونه ی حافظه است که مجموعه ای از instruction هاست و میتونه ورودی و خروجی داشته باشه . مثلا تابع function دو تا ورودی int و خروجیش هم اینتیجره ، این تابع ورودی ها رو با هم جمع میکنه و مقدارش رو return میکنه.

تابع یک declaration  داره که مثل تعریف متغیره ، قبل از اینکه تابع فراخوانی بشه باید تابع رو به کامپایلر معرفی کنیم .  با  اعلان تابع ، نوشتن پروتوتایپ تابع به کامپایلر اسم تابع رو میگیم و اینکه ورودی ها و خروجی هاش از چه نوعی هستند.

با استفاده از تعریف تابع هم مشخص میکنیم که تابع چه کاری انجام میده . تابع یک آدرس داره که آدرس اولین خونه یحافظه ی مربوط به اونه . اسم تابع در اینجا function پوینتر به آدرس تابعه ، وقتی یک تابع فراخوانی میشه cpu به آدرس اون تابع میره و instructon  های مربوطه رو اجرا میکنه.

extern و static

در فایل project.c یک متغیر float به نام variable و یک تابع به نام plus تعریف شده ،

 اگر بخواهیم از فایل led.c به متغیر variable دسترسی داشته باشیم از قبل میدونیم باید متغیر variable رو در فایل led.c extern تعریفش کنیم.دلیلش اینه که متغیر ها بصورت پیشفرض static هستند یعنی فقط از همون فایلی که درش تعریف میشن میشه بهشون دسترسی داشت. بعضی وقت ها میبینید که کلمه ی کلیدی static رو قبل ار تعریف متغیر مینویسن . و این به همین معنا که فقط در همون فایل میشه به اون متغیر دسترس داشت.

 اگر بخواهیم تابع plus رو از فایل led.c فراخوانی کنیم باید اعلان تابع رو در فایل led.c بنویسیم تا کامپایلر در مرحله کامپایل تابع و ورودی ها و خروجی هاش رو بشناسه و بتونه در مرحله link تابع رو پیدا کنه. ولی به کلمه کلیدی exter نیازی نیست . به این دلیل که توابع بصورت پیشفرض extern تعریف میشن. Extern یعنی میشه تو فایلهای دیگه تعریف شده باشن و بهشون دسترسی داشت.

اگر بخواهیم دسترسی به یک تابع رو محدود به یک فایل کنیم باید قبل از تعریف  و اعلان تابع عبارت static رو بنویسیم. مثلا تابع multiply رو فقط از فایل led.c میتونیم فراخوانی کنیم.

تابع inline

قبل از تعریف تابع بعصی وفتها کلمه ی  inline رو هم میبینیم. Inline  یک پیشنهاد به کامپایلر که وقتی تابع فراخوانی شد بدنه ی تابع inline در اون قسمت از کد نوشته بشه مثل functionlike macro ها . و معمولا برای توابع کوتاه استفاده میشه . فرق inline با macro اینه که ماکرو یک حایگداری تکست که در مرحله ی پریپراسسور انجام میشه ولی inline یک پیشنهاد به کامپابلر و انجامش هم ضروری نیست.

وقتی از  inline و macro استفاده میکنیم کدمون طولانی تر میشه چون هر بار که میخواهیم از این شبه تابع ها استفاده کنیم در کد نوشته میشن. بر خلاف تابع که فقط یکبار نوشته میشه و هر وقت لازمش داشتیم فراخوانی میشه . ولی کد سریعتر اجرا میشه چون بخش فراخوانی تابع حذف میشه.