Попередні статті:
Передмова
В цій статті ми познайомимось з деякими моментами роботи інтерфейсу CooCox IDE, як підключити бібліотеку, та які функції вони мають. Як вмикати і налаштовувати периферію GPIO. Та звернемо увагу на деякі моменти мови C++. Скомпілюємо нашу першу програму і прошиємо її до нашого мікроконтролера. Розберемось як все працює.Стаття розрахована на початківця який ще не мав справу з мовою C++ і мікроконтролерами STM32. Але мета статті не навчити мови C++. Існує безліч хороших навчальних книжок. То ж, як я вже зауважив в попередній статті "STM32: Стартуємо! Навчаємось! Реалізуємо!", треба озброїтись навчальною книгою по C++. Наприклад Брайан Оверленд "С++ БЕЗ СТРАХУ". Також не буду передруковувати документацію по STM32, щоб розібратись як працює мікроконтролер. Для цього є вся документація на сайті виробника мікроконтролерів. Початкові знання про синтаксис мови C++, цикли, умови, змінні, функції, тощо - ви вже маєте знати, чи мати про них уявлення. В статті буду зосереджуватись на тих моментах пов'язаних з мовою програмування і мікроконтролерами, які мені були не так явно зрозумілими і потребували додаткових пошуків у мережі. Мій пошук в мережі показав, що ці питання є типовими для початківців.
Що потрібно
Для того щоб почати чимось керувати за допомоги нашого мікроконтролера, а також реагувати на якісь події розглянемо простеньку програму, яка перемикає світлодіоди по натисканню кнопки. Два світлодіода і одна кнопка користувача вже є на платі STM32VLDISCOVERY. Для нашої першої програми потрібна сама плата розробника та під'єднаний кабель USB до комп'ютера. Що потрібно для старту: в статті "STM32: Стартуємо! Навчаємось! Реалізуємо!". Додаткових деталей і запчастин не потрібно.
Розташування елементів
Розташування елементів на сторінці 5, малюнок 3 з документації на плату.Розташування елементів на платі STM32VLDISCOVERY (Малюнок 3, сторінка 5) |
Нас цікавлять LD3 (зелений світлодіод), який під'єднаний до 9 виводу порту "C" нашого мікроконтролера, LD4 (синій світлодіод), який під'єднаний до 8 виводу порту "C", та кнопки користувача B1 USER, яка під'єднана до 0 виводу порту "A" (окреслено червоним). Це видно з електричної схеми на малюнку 12 сторінка 20 з документації на плату.
Схема підключення
Електрична схема STM32VLDISCOVERY (малюнок 12, сторінка 20) |
На ділянці 2 - схема підключення світлодіодів. Простіше не буває. Послідовно з світлодіодом резистор який буде обмежувати струм. Замість світлодіода можна підключити транзисторний ключ який буде вмикати чи вимикати більш потужні пристрої. Але це згодом.
Програма
Щоб пересвідчитись що все працює як слід запустимо програму на мікроконтролері, а вже потім розберемось як то все працює. Запускаємо CooCox IDE і створюємо новий проект. Назвемо його "first program". Обираємо свій "Chip" (ST - > ST32F10X -> ST32F100RB). Підключаємо потрібні бібліотеки: обираємо в "repository" бібліотеку GPIO. Також обереться автоматично пов'язана з нею бібліотека RCC. Подвійний "клік" на файлі "main.c" в структурі проекту.
Як встановити CooCox IDE і створити новий проект в статті "CooCox IDE встановлення і запуск".
В вікні "main.c" видаляємо весь текст (шаблон для програми) і вставляємо програму, яка представленна нижче, в вже пусте поле "main.c" звичайним копіюванням тексту. Далі тиснемо "Build" F7. Нашу програму у вигляді тексту компілятор перетворить на процесорний код і збереже у файл готовий до "заливки" у мікроконтролер. Потім "Flash" - "Program Download" і програма запишеться в пам'ять мікроконтролера. На платі де програматор, буде постійно увімкнений червоний світлодіод, який свідчить що живлення на плату надходить. А під час процесу "прошивання" програматор буде блимати іншим червоним світлодіодом. Після процесу "заливки" має увімкнутись синій вогник - зелений вимкнутий, а натиснувши на кнопку B1 USER все поміняється - синій вимкнеться, а зелений увімкнеться. З кожним натисканням вогники будуть міняти свій стан на протилежний.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 | #include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" int main(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE); GPIO_InitTypeDef Led_Port; Led_Port.GPIO_Mode=GPIO_Mode_Out_PP; Led_Port.GPIO_Pin=(GPIO_Pin_8 | GPIO_Pin_9); Led_Port.GPIO_Speed=GPIO_Speed_2MHz; GPIO_Init(GPIOC, &Led_Port); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); GPIO_InitTypeDef Button_Port; Button_Port.GPIO_Mode=GPIO_Mode_IN_FLOATING; Button_Port.GPIO_Pin=GPIO_Pin_0; Button_Port.GPIO_Speed=GPIO_Speed_2MHz; GPIO_Init(GPIOA, &Button_Port); GPIO_SetBits(GPIOC,GPIO_Pin_8); GPIO_ResetBits(GPIOC,GPIO_Pin_9); while(1) { if (GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)== Bit_SET) { GPIOC->ODR^=(GPIO_Pin_8 | GPIO_Pin_9); } } } |
Як це працює
Деякі моменти в цій статті будуть призначені для зовсім початківців. Їх треба зрозуміти, це дуже важливо. Повертатись до них в наступних статтях вже не будемо. А як наприклад, ви маєте досвід в мові програмування C++, а STM32 тримаєте в руках вперше, то можна продовжити читання з розділу "Вмикаємо периферію мікроконтролера". А як ви розбираєтесь і в C++ , і в STM32, то для вас представлена програма має бути зрозумілою. І цю статтю можете пропустити та дочекатись наступних публікацій, які для вас будуть цікавішими.
Директива #include
1 2 3 | #include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" |
Про файли з розширенням ".c" не турбуйтесь їх вкладати не треба (уявіть що файл з кодом ".c" це продовження заголовного файлу ".h"), це зробить препроцесор, а потім підготовлене передасть компілятору. Заголовний файл "stm32f10x.h" не має файлу серцевого коду з розширенням ".c". Файл "stm32f10x.h" це стандартна бібліотека - CMSIS, у ній визначено/оголошено всі імена регістрів мікроконтролера. Тому немає потреби задавати їх адреси в пам'яті, постійно зазираючи до технічної документації по мікроконтролеру. Не містить програмного коду як такого.
Шаблон програми з обов'язковими елементами
Синтаксис C++: Йдемо далі. Коли ми створили новий проект у вікні "main.c" автоматично сформувався синтаксичний шаблон з стандартними обов'язковими елементами:1 2 3 4 5 6 7 8 9 10 11 12 | //Оголошення функцій, змінних, директив, тощо int main(void) { //Програма яка виконається хочаб один раз while(1) { //Безкінечний цикл } } |
C++: Що і коли брати в фігурні дужки "{}", де ставити в кінці ";", а де не потрібно. Все це і набагато більше дізнайтесь в книзі Брайан Оверленд "С++ БЕЗ СТРАХУ", чи якійсь іншій книзі, яка вам до вподоби. Або скористайтесь пошуком.
Головна програма "main"
Повернемось до нашої програми яку ми скопіювали в пусте поле "main.c" - четвертим рядком ми позначаємо що зараз буде основний блок програми "int main(void)". Де "int" це тип даних integer. Для повернення коду помилки програмою.
C++: Ознайомитись з типами даних в мові C++ можна з навчальної літератури, або скористатись пошуком.
Вмикаємо периферію мікроконтролера
Блок-схема мікроконтролера
Щоб пристрій був автономним і споживав якомога менше енергії в мікроконтролерах початково всі модулі відключено. Які є модулі в нашому розпорядженні можна подивитись на малюнку 6, сторінка 8 документації на наш чип.
Блок схема STM32F100RB (малюнок 6, сторінка 8 STM32VLDISCOVERY) |
Вмикаємо периферію
В рядку шість нашої програми ми подаємо тактування на порт "C":6 | RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE); |
Розберемо рядок починаючи праворуч (читаємо навпаки). "ENABLE" - це просто "увімкнути або встановити біт". Далі, лівіше - "RCC_APB2Perifph_GPIOC". Скажу вам по дуже великому секрету, цей текст для мікроконтролера абсолютно нічого не означає, цей текст для нас - людей. Так, а що цей текст означає для мікроконтролера? А наведіть курсор миші на цей текст "RCC_APB2Perifph_GPIOC" в CooCox IDE і затримайте його там на секунду. З'явиться підказка як на малюнку нижче, щоб сфокусуватись на підказці тиснемо "F2".
Директива #define
Це звичайнісінький макрос, ще одна дуже корисна директива мови C++ "#define". Простими словами робимо підміну. Шістнадцяткове число "0x00000010" типу "uint32_t" (ціле число без знака розміром у 32 розряди) підміняємо на вираз "RCC_APB2Periph_GPIOC". Для чого це? А щоб нам було зручніше і зрозуміліше. Мікроконтролер керується/налаштовується через регістри. А саме, наприклад, щоб подати тактування на GPIOC треба четвертий біт регістру APB2ENR встановити в одиницю (двійкове число буде 0000 0000 0001 0000, четвертий біт рахуємо з нуля). Як всі ці назви регістрів, та які біти в них потрібно встановити/скинути взнати? І як їх запам'ятати? А це зовсім і не потрібно. Для того ми і під'єднали до нашого проекту бібліотеку stm32f10x_gpio, яка за собою ще "потягнула" бібліотеку stm32f10x_rcc. Відкриємо заголовний файл stm32f10x_rcc.h (подвійний "клік" на цьому файлі в структурі проекту). Шукаємо рядок "#define RCC_APB2Periph_GPIOC ((uint32_t)0x00000010)" який буквально означає - хай текстовий вираз RCC_APB2Periph_GPIOC буде числом 0x00000010 яке привели (призначили) до типу uint32_t. Це значить що препроцесор коли зустріне в тексті програми вираз RCC_APB2Periph_GPIOC то замінить його на число 0x00000010. Також можна побачити поруч багато рядків з директивою #define, з переліком всієї периферії нашого мікроконтролера.
C++: З директивами мови C++ можна ознайомитись з навчальної літератури. Або скористатись пошуком.
Функції
Ще залишилось розібратись з "RCC_APB2PeriphClockCmd" - це назва процедури/функції/підпрограми, далі будемо називати їх "функція" або "процедура". А де ж вона взялась, як ми такої функції не писали в своїй програмі? Все там же в бібліотеці, яку ми приєднали. Якщо навести курсор миші на текст "RCC_APB2PeriphClockCmd" в CooCox IDE і затримаємо там курсор, то з'явиться підказка з текстом цієї функції. Сфокусуватись на підказці можна натиснувши "F2". Але краще відкрийте файл з програмним кодом stm32f10x_rcc.c (подвійний "клік" на цьому файлі в структурі проекту) і шукаємо там цю функцію. Ось текст цієї функції:
1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 | void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState) { /* Check the parameters */ assert_param(IS_RCC_APB2_PERIPH(RCC_APB2Periph)); assert_param(IS_FUNCTIONAL_STATE(NewState)); if (NewState != DISABLE) { RCC->APB2ENR |= RCC_APB2Periph; } else { RCC->APB2ENR &= ~RCC_APB2Periph; } } |
З переліком всіх наявних функцій в бібліотеці RCC можна ознайомитись в кінці заголовного файлу stm32f10x_rcc.h - це оголошення всіх прототипів функцій які є в файлі stm32f10x_rcc.c бібліотеки RCC.
C++: Оголошення прототипів функцій потрібні на початку, щоб компілятор знав, що такі функції десь є в наявності і їх треба пошукати по тексту програми. Можна не оголошувати на початку прототип функції, а розмістити саму функцію. Але як функцій забагато, то добратись до тексту основної програми буває дуже не просто. Це не зручно.
Про функції, вхідні аргументи функцій, що функції можуть повертати і набагато більше дізнайтесь з навчальної літератури по C++. Або скористайтесь пошуком.
Якщо ви уважно прослідкували і повторили все на практиці, а в додачу ще й зрозуміли, то ви вже пізнали "дзен" !Радіймо! Та йдемо далі.Про функції, вхідні аргументи функцій, що функції можуть повертати і набагато більше дізнайтесь з навчальної літератури по C++. Або скористайтесь пошуком.
Підсумок по вмиканню тактування периферії
Підіб'ємо підсумки по вмиканню тактування периферії. В шостому рядку нашої програми ми вмикаємо порт C який належить GPIO і тактується з шини APB2. Вмикаємо за допомоги функції, яка належить бібліотеці RCC. Цю бібліотеку ми приєднали до свого проекту.
Повторимо ще раз. Функція, яка вмикає периферію по шині APB2 має назву RCC_APB2PeriphClockCmd, а параметри/аргументи які приймає функція зазначені в дужках через кому. Ця функція приймає від нас два параметри: перший - RCC_APB2Periph_GPIOC, це макрос #define, який підміняється на число, яке, в свою чергу, вказує який біт треба встановити/скинути в регістрі APB2ENR. Регістр APB2ENR є членом класу RCC. А RCC назва модуля - Reset and clock control. І другий параметр - "ENABLE", що значить - біт треба встановити. А "DISABLE" - що біт треба скинути. З кодом функції можна ознайомитись в файлі stm32f10x_rcc.c бібліотеки RCC.
STM32: Яка периферія від якої шини тактується, можемо дізнатись з документації на чип, чи подивитись у бібліотеку RCC в заголовний файл stm32f10x_rcc.h
Налаштування периферії
Крім того щоб подати тактування (живлення) на потрібну периферію, ще треба її налаштувати потрібним чином. В нашому першому проекті порт C, де підключені світлодіоди до виводів 8 і 9, треба налаштувати на вихід. А там де кнопка, порт A - вивід 0, треба налаштувати на вхід. Це потрібно явно вказати мікроконтролеру. Далі фрагмент програми який заповнює певну структуру налаштування порту "C" потрібними даними для цього: який режим, які ніжки, яка частота тактування.
8 9 10 11 12 | GPIO_InitTypeDef Led_Port; Led_Port.GPIO_Mode=GPIO_Mode_Out_PP; Led_Port.GPIO_Pin=(GPIO_Pin_8 | GPIO_Pin_9); Led_Port.GPIO_Speed=GPIO_Speed_2MHz; GPIO_Init(GPIOC, &Led_Port); |
Структури даних
"GPIO_InitTypeDef" це назва самої структури і знаходиться вона в заголовному файлі stm32f10x_gpio.h бібліотеки GPIO, яку ми під'єднали до свого проекту і має вигляд:
91 92 93 94 95 96 97 98 99 100 101 | typedef struct { uint16_t GPIO_Pin; /*!< Specifies the GPIO pins to be configured. This parameter can be any value of @ref GPIO_pins_define */ GPIOSpeed_TypeDef GPIO_Speed; /*!< Specifies the speed for the selected pins. This parameter can be a value of @ref GPIOSpeed_TypeDef */ GPIOMode_TypeDef GPIO_Mode; /*!< Specifies the operating mode for the selected pins. This parameter can be a value of @ref GPIOMode_TypeDef */ }GPIO_InitTypeDef; |
C++: Про структури можна ознайомитись з навчальної літератури по мові програмування C++. Або скористатись пошуком.
Як бачимо нам не знадобилось створювати свою структуру, а скористались вже створеною з бібліотеки GPIO. Такий собі софтовий конструктор. Це дуже зручно.Починаємо заповнювати об'єкт "Led_Port" елементами структури.
Дев'ятий рядок: Led_Port.GPIO_Mode=GPIO_Mode_Out_PP;
Режим "GPIO_Mode" буде "GPIO_Mode_Out_PP".
STM32: Out - вихід. PP це Push-Pull - «двотактний вихід». Подали 0 - вихід підключився до землі "-" (негативного полюсу джерела живлення), 1 - підключиться до позитивного полюсу джерела живлення "+". Іншими словами, як подамо 0, то світлодіод не буде світитись, як 1 - світлодіод засвітиться. Це дуже просто.
Десятий рядок: Led_Port.GPIO_Pin=(GPIO_Pin_8 | GPIO_Pin_9);
GPIO_Pin - призначить які ніжки порту будуть працювати в режимі Out_PP. В нашому випадку це GPIO_Pin_8 і GPIO_Pin_9, до яких підключені наші зелений і синій світлодіоди.
CooCox IDE: Як потрібна один вивід, то можна записати просто: Led_Port.GPIO_Pin=GPIO_Pin_8;
Як декілька, то треба їх перерахувати в дужках розділяючи між собою знаком "|", як в нашому випадку. Що означає в мові C++ порозрядне "АБО" (дивись тут).
Як потрібні всі ніжки порту, то достатньо вказати таким чином: Led_Port.GPIO_Pin=GPIO_Pin_All
Як декілька, то треба їх перерахувати в дужках розділяючи між собою знаком "|", як в нашому випадку. Що означає в мові C++ порозрядне "АБО" (дивись тут).
Як потрібні всі ніжки порту, то достатньо вказати таким чином: Led_Port.GPIO_Pin=GPIO_Pin_All
Одинадцятий рядок: Led_Port.GPIO_Speed=GPIO_Speed_2MHz;
STM32: Можливі частоти для використання залежать від чипу який ви використовуєте і зазначено в документації на чип.
GPIO_Init - це назва функції яка знаходиться у файлі stm32f10x_gpio.c бібліотеки GPIO, яку ми приєднали до свого проекту. Функція "void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)" приймає два аргументи: який порт будемо налаштовувати і перелік налаштувань. В нашому випадку це буде GPIOC і перелік необхідних налаштувань містить в собі об'єкт структури, який ми назвали Led_Port і вже заповнили його. "Void" означає що ця функція назад нічого не повертає. Все просто!
Інтерфейс CooCox IDE
Хоча ми і спрощуємо собі процес програмування, тим що підключаємо бібліотеки. І нам не потрібно запам'ятовувати чи постійно підглядати в документацію по назви регістрів, та що треба з ними зробити для правильної роботи. Але ж і функцій, які виконують за нас багато потрібної роботи, теж безліч. І в голові тримати всі ті назви функцій, змінних, структур, тощо - теж не потрібно. Нам допоможе підказки інтерфейсу CooCox IDE.Для кращого розуміння роботи інтерфейсу CooCox IDE, та звідки беруться ті чи інші назви функцій, структур, змінних, тощо: пропоную видалити з нашої програми рядки з 8-го по 12-ий, де ми заповнюємо структуру "GPIO_InitTypeDef". Та спробуємо ввести текст програми з клавіатури, а не як це ми зробили на початку, просто копіюючи текст програми з буферу обміну.
Починаємо писати восьмий рядок. Напишемо GPIO і на екрані з'явиться підказка (пропозиції шаблонів), як на представленому малюнку:
Чому саме цей рядок обрали? Бо нам потрібна назва структури. Те що то структура, видно не тільки з назви, а щоб орієнтуватись безпомилково навпроти кожного виразу є графічний значок. Зелений кружечок - це функція. Літера T в жовтому кружечку - це структура, решітка - макрос. Повна таблиця відповідностей графічного знаку з виразом під Spoiler'ом:
Spoiler:
CooCox IDE: Ознайомитись з інтерфейсом CooCox IDE можна з документації "Help -> Help Contents", або за ланкою.
Після того як ми ввели з пропозицій назву структури GPIO_InitTypeDef - назвемо об'єкт структури, як вже домовились Led_Port, та закриємо рядок крапкою з комою - ";"Наступним, дев'ятим рядком, почнемо заповнювати об'єкт структури даними. Починаємо писати назву нашого об'єкту Led , і з'явиться підказка-пропозиція з назвою нашого об'єкта:
Оберемо його натиснувши клавішу "Enter", та введемо з клавіатури "оператор-крапку" - "." і побачимо перелік елементів структури. Просто обираємо перший GPIO_Mode.
Ставимо знак дорівнює - "=" і починаємо писати GPIO_Mode, визирне підказка-пропозиція з усіма можливими режимами:
Нас цікавить двотактовий вихідний режим і це - GPIO_Mode_Out_PP. Сама назва говорить за себе. Обираємо потрібний режим і тиснемо клавішу "Enter". Та завершуємо рядок ";".
Так само заповнюємо інші елементи структури.
Десятий рядок: запишемо так само з підказок-пропозицій, які ніжки будемо використовувати: Led_Port.GPIO_Pin=(GPIO_Pin_8 | GPIO_Pin_9);
Одинадцятий рядок: Led_Port.GPIO_Speed=GPIO_Speed_2MHz; З підказок-пропозицій видно що можливі частоти тактування будуть: 2, 10 і 50 MHz.
Десятий рядок: запишемо так само з підказок-пропозицій, які ніжки будемо використовувати: Led_Port.GPIO_Pin=(GPIO_Pin_8 | GPIO_Pin_9);
Одинадцятий рядок: Led_Port.GPIO_Speed=GPIO_Speed_2MHz; З підказок-пропозицій видно що можливі частоти тактування будуть: 2, 10 і 50 MHz.
Так ми заповнили об'єкт структури необхідними даними. Тепер треба це записати до потрібних регістрів мікроконтролера. Цим займеться функція з назвою GPIO_Init бібліотеки GPIO.
Дванадцятим рядком починаємо вводити з клавіатури GPIO_Init - з'явиться підказка-пропозиція:
Де зелений кружечок це функція, а літера "Т" в жовтому кружечку - структура. Нам потрібна функція. Оберемо її.
Бачимо що інтерфейс CooCox IDE сам додав дужки для введення аргументів і передачі їх в функцію. Та ще й дає підказку які саме аргументи від нас очікують. Це який порт GPIO, та данні об'єкту структури. Запишемо туди GPIOC, та через кому &Led_Port. Та закінчимо рядок крапкою з комою ";".
C++: Чому перед Led_Port треба ставити &? Це оператор взяття адреси пам'яті. Мені, як новачку, було дуже складно зрозуміти і усвідомити це. Але треба постаратись зрозуміти це вже зараз, щоб далі було легше. Є певна структура даних, яку ми назвали Led_Port, опис цієї структури (такі собі правила збереження даних) називається GPIO_InitTypeDef. Ці дані мають десь в пам'яті мікроконтролера зберігатись. Про це потурбується компілятор. Він виділить якусь ділянку пам'яті мікропроцесора для збереження, та розмістить їх там. Але як ми знаємо де саме ця ділянка напевно розташована? Не знаємо і не потрібно знати. Для того, щоб отримати адресу початку даних, треба вказати ім'я об'єкта з даними і перед ім'ям поставити оператор взяття адреси пам'яті - &. Коли компілятор буде обробляти текст нашої програми він підставить конкретну адресу початку наших даних, яку сам і виділив для збереження тих даних. Важливо запам'ятати: вираз &Led_Port звертається не до об'єкта структури Led_Port, а отримує адресу початку даних в пам'яті де зберігаються ці данні об'єкту структури Led_Port.
Ну що ж. З інтерфейсом, а саме з підказками-пропозиціями, що з'являються автоматично - розібрались. Можемо зосередитись на програмі.Далі з чотирнадцятого по двадцятий рядок майже так само налаштовуємо периферію порту A на вхід:
14 15 16 17 18 19 20 | RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); GPIO_InitTypeDef Button_Port; Button_Port.GPIO_Mode=GPIO_Mode_IN_FLOATING; Button_Port.GPIO_Pin=GPIO_Pin_0; Button_Port.GPIO_Speed=GPIO_Speed_2MHz; GPIO_Init(GPIOA, &Button_Port); |
Функції GPIO_Init передаємо такі аргументи як GPIOA (данні структури які ввели стосуються порту А) і адресу початку даних для налаштування &Button_Port. От і все з налаштуваннями нашого першого проекту.
Початкове встановлення світлодіодів
Хай на початку, при увімкнені живлення на плату, світиться синій вогник, а зелений буде вимкнений. В бібліотеці GPIO є такі функції як встановлення біту, та скидання біту. Це для того щоб подати на якийсь цифровий вихід логічну одиницю (1), чи логічний нуль (0). І називаються вони відповідно: GPIO_SetBits і GPIO_ResetBits.
Далі в нашій програмі йдуть такі рядки:
22 23 | GPIO_SetBits(GPIOC,GPIO_Pin_8); GPIO_ResetBits(GPIOC,GPIO_Pin_9); |
Двадцять третій рядок, навпаки, скинути біт, який відповідає за 9 вихід порту "C", до якого під'єднаний зелений світлодіод.
Простіше не буває. Чи не так?
Для допитливих раджу глянути на ці функції GPIO_SetBits і GPIO_ResetBits в файлі бібліотеки "stm32f10x_gpio.c". Та самотужки розібратись як вони працюють.
Підказка: порти GPIO мають декілька регістрів, таких як регістри конфігурації, даних, встановлення/скидання, блокування та альтернативних функцій. Зараз нас цікавлять регістри встановлення/скидання, а саме ODR, BSRR, BRR. Як бачимо в функції GPIO_SetBits (дивитись в файлі "stm32f10x_gpio.c") певний біт, який відповідає за 8 ніжку порту C, записується в регістр BSRR, а функція GPIO_ResetBits (дивитись в файлі "stm32f10x_gpio.c") записує певний біт, який відповідає за 9 ніжку порту C, записується в регістр BRR. Думаю не важко здогадатись що до чого. Але як не зрозуміло все одно, то не переживайте, далі все буде.
Підказка: порти GPIO мають декілька регістрів, таких як регістри конфігурації, даних, встановлення/скидання, блокування та альтернативних функцій. Зараз нас цікавлять регістри встановлення/скидання, а саме ODR, BSRR, BRR. Як бачимо в функції GPIO_SetBits (дивитись в файлі "stm32f10x_gpio.c") певний біт, який відповідає за 8 ніжку порту C, записується в регістр BSRR, а функція GPIO_ResetBits (дивитись в файлі "stm32f10x_gpio.c") записує певний біт, який відповідає за 9 ніжку порту C, записується в регістр BRR. Думаю не важко здогадатись що до чого. Але як не зрозуміло все одно, то не переживайте, далі все буде.
Безкінечний цикл
Далі в нашій програмі йде безкінечний цикл While(1). В якому йде опитування нашої кнопки і якщо вона натиснута, то наші вогники поміняють свій стан на протилежний:
25 26 27 28 29 30 | while(1) { if (GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)== Bit_SET) { GPIOC->ODR^=(GPIO_Pin_8 | GPIO_Pin_9); } } |
Двадцять сьомий рядок: оператор умови if , а в дужках сама умова. Як поглянути на схему підключення кнопки, яка представлена на початку цієї статті. Можна пересвідчитись, що коли кнопка у вільному стані, то через резистор 10К ніжка 0 порту A під'єднана до землі/мінусу живлення/нульового потенціалу. А як кнопку натиснути, то до ніжки 0 порту A підключимо позитивний потенціал/плюс/логічну одиницю.
Ми вже налаштували ніжку 0 порту A на вхід без підтягування до якогось потенціалу. Коли будемо опитувати програмою біт, який відповідає за 0 ніжку порту A, то отримаємо там "0" при розімкнутих контактах кнопки, і "1" коли контакти замкнуться. Як цей стан прочитати? Для цього використаємо функцію бібліотеки GPIO з назвою GPIO_ReadInputDataBit (логічно?). А аргументами, які передамо цій функції, будуть яку ніжку, якого порту будемо читати (GPIOA,GPIO_Pin_0). Якщо подивитись цю функцію у файлі stm32f10x_gpio.c, то побачимо, що вона крім того що приймає аргументи функції, ще й повертає результат типу uint8_t. Це як раз стан в якому знаходиться біт, що ми читаємо (0 або 1). Далі йде "==" - подвійний знак "дорівнює", це оператор порівняння (не плутати з оператором присвоювання "=" - одинарним знаком "дорівнює"). Коротко 27-й рядок означає: Читаємо стан біту, який відповідає виводу до якого підключено кнопка. Як цей біт, буде дорівнювати "1" (біт буде встановлено), то треба виконати блок програми між "{" та "}". І це рядок 28. Як буде дорівнювати "0", то нічого не робимо. Рядок 28 пропускається.
Двадцять восьмий рядок: А ось тут ми напряму керуємо регістром порту GPIO без всіляких функцій. "GPIOC->ODR^=(GPIO_Pin_8 | GPIO_Pin_9);" - означає, що ми звертаємось до члену "ODR" класу "GPIOC" за допомоги оператору "->", де ODR один з регістрів порту "C" в дужках (GPIO_Pin_8 | GPIO_Pin_9) це наші виводи до яких підключені синій і зелений світлодіоди і вони об'єднані оператором порозрядного АБО "|", а оператор "^" - порозрядна інверсія, а "=" призначає новий стан. Простими словами: стан бітів зміниться на протилежний. А як наслідок, вогники синій і зелений поміняють свій стан теж.
Щоб краще зрозуміти цю частину треба самостійно ознайомитись з такими речами:
C++: Цикли, умови, оператори логіки, оператори присвоєння та порівняння, директиви, структури.
C++: Цикли, умови, оператори логіки, оператори присвоєння та порівняння, директиви, структури.
Бібліотеки CMSIS і SPL
У розділі "Директива #include" вже згадувались бібліотеки CMSIS і SPL, а також коротко для чого вони і чим відрізняються. Їх ми використовували у своїй першій програмі. А чи можна обійтись без них? Так, можна. Деякі досвідчені програмісти радять спочатку розібратись з роботою мікроконтролера обмежуючись тільки бібліотекою CMSIS і не використовувати SPL аргументуючи це тим, що так краще можна зрозуміти роботу мікроконтролера. А вже потім, щоб спростити собі життя, використовувати бібліотеки SPL. Я хоч і не досвідчений програміст, але мабуть теж погоджусь з цим, хоча в нашій першій програмі ми почали все навпаки і дуже активно використовували бібліотеки SPL: такі як GPIO і RCC. Чому так? Пригадую, як я тримав перший раз плату STM32VLDiscovery в руках, а мої пізнання мови C++, на той час були дуже поверхневими. Спробувати запустити хоч щось і побачити позитивний результат хотілось вже зараз. Але мені прийшлось дуже довго і багато читати різного, розкиданого і не зовсім мені зрозумілого матеріалу, аж допоки в мене не вийшло скомпілювати без помилок першу програму і залити її до мікроконтролера і вона працювала. А це важливо, щоб на перших кроках був позитивний результат. Сподіваюсь що в нас перший позитивний досвід вже є. Враховуючи наші теперішні знання і невеличку практику, можемо спробувати переписати нашу першу програму без бібліотек GPIO і RCC. Створюємо новий проект. І з бібліотек додаємо тільки CMSIS core і CMSIS Boot. Ось так програма буде виглядати в main.c:
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 | #include "stm32f10x.h" int main(void) { RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; GPIOC->CRH &= ~GPIO_CRH_CNF8; GPIOC->CRH |= GPIO_CRH_MODE8_0; GPIOC->CRH &= ~GPIO_CRH_MODE8_1; GPIOC->CRH &= ~GPIO_CRH_CNF9; GPIOC->CRH |= GPIO_CRH_MODE9_0; GPIOC->CRH &= ~GPIO_CRH_MODE9_1; RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; GPIOA->CRL &= ~GPIO_CRL_CNF0_0; GPIOA->CRL |= GPIO_CRL_CNF0_1; GPIOA->CRL &= ~GPIO_CRL_MODE0; GPIOC->BSRR = GPIO_BSRR_BS8; GPIOC->BRR = GPIO_BRR_BR9; while(1) { if (GPIOA->IDR & GPIO_IDR_IDR0) { GPIOC->ODR^=(GPIO_ODR_ODR8 | GPIO_ODR_ODR9); } } } |
Відмінності очевидні, хоч програма виконує все те ж саме. Тут ми не використовуємо функцій з бібліотек, а керуємо мікроконтролером напряму через регістри шляхом встановлення чи скидання бітів регістрів. Як би я почав знайомство з мікроконтролером саме з цього боку. Певен що в мене б пішов дим з вух і руки опустились. Щоб зрозуміти роботу цього варіанту програми треба прочитати про регістри GPIO мікроконтролера. Документація по всім регістрам тут. Та порозрядні логічні операції в мові C++. Підказка: вираз "|=" в програмі означає - встановити біт, а вираз "&= ~" означає - скинути біт. Далі ми будемо іноді застосовувати такий варіант керування мікроконтролером. Як буде потреба пояснити кожний рядок цієї програми саме в цій статті - пишіть в коментарях.
Як буде потреба пояснити кожний рядок цієї програми саме в цій статті - пишіть в коментарях.
Розберемо тільки ті моменти які відрізняються від попереднього варіанту.
П'ятий рядок: додали стандартну бібліотеку мови C++ "stdbool.h". Вона нам знадобиться, щоб оперувати таким типом даних як bool. Цей тип даних передбачає два стани "TRUE", або "FALSE".
Сьомий рядок: передпроцесорна директива #define. Щоб нам було зручно і зрозуміло створюємо макрос, який буде означати: хай ідентифікатор Led_Blue відповідає рядку-токену GPIO_Pin_8. Тепер по всій програмі де зустрінеться Led_Blue буде матись на увазі GPIO_Pin_8. І нам не треба буде запам'ятовувати на який "нозі" мікроконтролера знаходиться той чи інший світлодіод. А будемо оперувати очевидними для нас речами.
Восьмий - одинадцятий рядок створюємо макроси define для зеленого світлодіода, кнопки і портів де знаходяться світлодіоди і кнопка. Далі по тексту програми тепер пишемо не GPIO_Pin_8, а Led_Blue. Не GPIO_Pin_9, а Led_Green. Не GPIO_Pin_0, а Button. Не GPIOC, а Led_Port. Не GPIOA, а Button_Port. Це зрозуміло.
Шістнадцятий рядок: Вмикаємо периферію одночасно порт A і C. В першому варіанті програми ми спочатку вмикали порт C заповнювали даними налаштувань, а потім вмикали периферію порту A. Так можна робити, але це не оптимально з огляду на програмування. Краще в одному рядку перерахувати що ми вмикаємо.
Вісімнадцятий рядок: Оголошуємо назву структури "GPIO_InitStruct" і приводимо її до типу "GPIO_InitTypeDef". В першому варіанті програми ми оголошували дві структури одного типу (Led_Port і Button_Port), для порту де світлодіоди, і порту де кнопка. Але це не оптимально з огляду програми і марнотратно по відношенню до пам'яті мікроконтролера. Достатньо один раз оголосити структуру, а потім заповнити даними, використати їх і можна в ту ж структуру заносити нові дані для подальшого використання.
Двадцять п'ять - двадцять сім рядки: заповнюємо структуру даними для роботи кнопки. В початковій програмі був присутній рядок з даними про частоту. Але коли ніжка порту працює на вхід, то дані про частоту зайві цей параметр можна не вводити.
Тридцять другий рядок: оголошення нової змінної button_flag типу bool і даємо їй початкове значення false. Можна б було назначити зміну типу INT і оперувати значеннями 0 або 1. Все б працювало як слід, але це теж не оптимально. Нам потрібні всього два значення змінної. А використовувати тип INT дуже марнотратно. Плюс, типом змінної bool зручніше оперувати в програмах коли потрібні логічні порівняння чи потрібен перемикач On/Off. Наприклад можна не писати порівняння if (button_flag==0), а записати if (!button_flag), або замість if (button_flag==1) - if (button_flag), тощо.
Тридцять третій рядок: безкінечний цикл while(1), працює таким чином: якщо кнопка натиснута, то перевіримо button_flag, як false (вираз !button_flag означає false), то назначаємо button_flag значення true (кнопку вже натиснули) і міняємо стан світлодіодів. І поки кнопка буде натиснута, то стан світлодіодів не буде мінятись, бо button_flag перевірку не пройде і частина програми, де міняється стан світлодіодів, не буде виконуватись. А вже як відпустимо кнопку, то виконається сорок другий рядок програми - де button_flag прийме значення false (кнопку відпустили) і при черговому натисканні кнопки стан світлодіодів знову поміняється, бо button_flag перевірку вже пройде.
Варіант програми з CoAssistant
За допомоги CoAssistant можна взнати які числа записувати у регістри для налаштування і роботи мікроконтролера. Зайдемо на сайт CoAssistant за ланкою. Оберемо виробника чипу ST, та модель чипу ST32F100RB обираємо потрібні регістри, їх режим і стан. Натомість отримуємо потрібні числа для запису в регістри. Ось так буде виглядати програма в такому варіанті:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #include "stm32f10x.h" int main(void) { RCC->APB2ENR = 0x14; GPIOC->CRH = 0x22; GPIOA->CRL = 0x4; GPIOC->BSRR = 0x100; GPIOC->BRR = 0x200; while(1) { if (GPIOA->IDR & GPIO_IDR_IDR0) { GPIOC->ODR^=(GPIO_ODR_ODR8 | GPIO_ODR_ODR9); } } } |
Раджу для ознайомлення такі статті: "STM32. Работа с базовыми портами ввода/вывода." А також, дуже хороша стаття-огляд, зрозумілою мовою "Огляд STM32 (ARM Cortex-M від STMicroelectronics)" - рекомендую!
Вдосконалення програми
Зробимо невеличке вдосконалення нашої першої програми, щоб позбутись головного недоліку - поки натиснута кнопка, світлодіоди постійно перемикають свій стан. І як результат, стан світлодіодів непередбачуваний. Також проведемо невеличку оптимізацію програми. Та оформимо програму як справжні програмісти, з коментарями. Візьмемо за основу програму з самого початку статті в розділі "Програма", там де ми використовували бібліотеки і CMSIS, і SPL (GPIO, RCC).
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 | //Директиви Include. Те що вкладаємо до проекту #include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" #include "stdbool.h" //Директиви Define. Назначаємо відповідність ідентифікатора до рядка-токена #define Led_Blue GPIO_Pin_8 #define Led_Green GPIO_Pin_9 #define Button GPIO_Pin_0 #define Led_Port GPIOC #define Button_Port GPIOA //Головна програма int main(void) { //Вмикаємо периферію: порт A і C RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOA,ENABLE); //Оголошуємо назву структуру і приведемо її до певного типу GPIO_InitTypeDef GPIO_InitStruct; //Заповнюємо структуру даними для порту C GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP;//Режим вихід push-pull GPIO_InitStruct.GPIO_Pin=(Led_Blue | Led_Green);//Які ніжки мікроконтролера будуть в роботі GPIO_InitStruct.GPIO_Speed=GPIO_Speed_2MHz;//Якою частотою будуть тактуватись ніжки мікроконтролера GPIO_Init(Led_Port, &GPIO_InitStruct);//Заповнюємо структуру даними для порту C функцією GPIO_Init //Заповнюємо структуру даними для порту A GPIO_InitStruct.GPIO_Mode=GPIO_Mode_IN_FLOATING;//Режим вхід floating GPIO_InitStruct.GPIO_Pin=Button;//Які ніжки мікроконтролера будуть в роботі GPIO_Init(Button_Port, &GPIO_InitStruct);//Заповнюємо структуру даними для порту A функцією GPIO_Init //Встановлення початкового стану світлодіодів GPIO_SetBits(Led_Port,Led_Blue);//Блакитний вогник запалимо GPIO_ResetBits(Led_Port,Led_Green);//Зелений вогник погасимо //Оголосимо нову змінну button_flag, 1 - як натиснули кнопку, 0 - як відпустили кнопку bool button_flag = false;//назначимо їй початковий стан 0 while(1) { if (GPIO_ReadInputDataBit(Button_Port,Button)== Bit_SET) {//Читаємо стан кнопки if (!button_flag) {//Як натиснута кнопка, то перевіряємо прапорець кнопки button_flag=true;//Якщо прапорець 0, то встановимо 1 GPIOC->ODR^=(Led_Blue | Led_Green);//Поміняємо стан вогників на протилежний } }else {//Як кнопка не натиснута button_flag=false;//Прапорець кнопки 0 } } } |
П'ятий рядок: додали стандартну бібліотеку мови C++ "stdbool.h". Вона нам знадобиться, щоб оперувати таким типом даних як bool. Цей тип даних передбачає два стани "TRUE", або "FALSE".
Сьомий рядок: передпроцесорна директива #define. Щоб нам було зручно і зрозуміло створюємо макрос, який буде означати: хай ідентифікатор Led_Blue відповідає рядку-токену GPIO_Pin_8. Тепер по всій програмі де зустрінеться Led_Blue буде матись на увазі GPIO_Pin_8. І нам не треба буде запам'ятовувати на який "нозі" мікроконтролера знаходиться той чи інший світлодіод. А будемо оперувати очевидними для нас речами.
Восьмий - одинадцятий рядок створюємо макроси define для зеленого світлодіода, кнопки і портів де знаходяться світлодіоди і кнопка. Далі по тексту програми тепер пишемо не GPIO_Pin_8, а Led_Blue. Не GPIO_Pin_9, а Led_Green. Не GPIO_Pin_0, а Button. Не GPIOC, а Led_Port. Не GPIOA, а Button_Port. Це зрозуміло.
Шістнадцятий рядок: Вмикаємо периферію одночасно порт A і C. В першому варіанті програми ми спочатку вмикали порт C заповнювали даними налаштувань, а потім вмикали периферію порту A. Так можна робити, але це не оптимально з огляду на програмування. Краще в одному рядку перерахувати що ми вмикаємо.
Вісімнадцятий рядок: Оголошуємо назву структури "GPIO_InitStruct" і приводимо її до типу "GPIO_InitTypeDef". В першому варіанті програми ми оголошували дві структури одного типу (Led_Port і Button_Port), для порту де світлодіоди, і порту де кнопка. Але це не оптимально з огляду програми і марнотратно по відношенню до пам'яті мікроконтролера. Достатньо один раз оголосити структуру, а потім заповнити даними, використати їх і можна в ту ж структуру заносити нові дані для подальшого використання.
Двадцять п'ять - двадцять сім рядки: заповнюємо структуру даними для роботи кнопки. В початковій програмі був присутній рядок з даними про частоту. Але коли ніжка порту працює на вхід, то дані про частоту зайві цей параметр можна не вводити.
Тридцять другий рядок: оголошення нової змінної button_flag типу bool і даємо їй початкове значення false. Можна б було назначити зміну типу INT і оперувати значеннями 0 або 1. Все б працювало як слід, але це теж не оптимально. Нам потрібні всього два значення змінної. А використовувати тип INT дуже марнотратно. Плюс, типом змінної bool зручніше оперувати в програмах коли потрібні логічні порівняння чи потрібен перемикач On/Off. Наприклад можна не писати порівняння if (button_flag==0), а записати if (!button_flag), або замість if (button_flag==1) - if (button_flag), тощо.
Тридцять третій рядок: безкінечний цикл while(1), працює таким чином: якщо кнопка натиснута, то перевіримо button_flag, як false (вираз !button_flag означає false), то назначаємо button_flag значення true (кнопку вже натиснули) і міняємо стан світлодіодів. І поки кнопка буде натиснута, то стан світлодіодів не буде мінятись, бо button_flag перевірку не пройде і частина програми, де міняється стан світлодіодів, не буде виконуватись. А вже як відпустимо кнопку, то виконається сорок другий рядок програми - де button_flag прийме значення false (кнопку відпустили) і при черговому натисканні кнопки стан світлодіодів знову поміняється, бо button_flag перевірку вже пройде.
Програма STM32CubeMX
Певен що процедура вмикання, налаштування і ініціалізації периферії мікроконтролера, викликає, а особливо у новачків, деякі труднощі. В мене так було. До того ж це, дещо, нуднувато. Радіймо! Компанія STMicroelectronics потурбувалась про нас і випустила програму STM32CubeMX - генератор проектів. В ній наочно можна налаштувати периферію і створити готовий код для її ініціалізації. Візуально видно які ніжки мікроконтролера зайнято, використовуємо на поточний час. Проект розвивається і поки ще сируватий. Але вже користуватись можна. Завантажити програму можна тут. Встановлення стандартне, без особливостей. І щоб не мучити вас довгими текстами і знятками екрану покажу відео-приклад як цю програму можна застосувати стосовно нашої першої програми. Візьмемо за основу остаточний варіант програми з розділу "Вдосконалення програми" і переробимо її додавши згенерований код програмою STM32CubeMX:
Режим Debug в CooCox IDE
Дуже наглядним і корисним є режим Debug в CooCox IDE. Можна розібратись як працює програма, або можна знайти помилку. Щоб увімкнути цей режим тисніть в меню CooCox IDE - debug - debug, або Ctrl+F5 на клавіатурі. CooCox IDE увійде в режим debug, з'являться нові віконця: внизу і праворуч - віконце з змінними і їх значеннями, а бічне віконце праворуч буде містити вкладки Registers, Peripherals, Disassembly, тощо. Увімкнути чи вимкнути ті чи інші вкладки можна в меню View.
Тепер коли натискати на клавіатурі клавішу F10, то буде по кроку виконуватись наша програма і ми в реальному часі можемо спостерігати які значення приймають наші змінні в програмі і регістри мікроконтролера. Спробуйте натискати F10 і спостерігайте як міняються значення регістрів і змінних. Та як виконується наша перша програма. Не забувайте натискати кнопку на платі STM32VLDiscovery, щоб поміняти стан світлодіодів і побачити до яких змін це призвело у режимі debug.
Підсумок
Ось ми випробували та докладно розібрали кожен рядок нашої першої програми. Навчились створювати новий проект, та деякі моменти, як користуватись інтерфейсом CooCox IDE, щоб полегшати собі процес програмування. Розібрались як підключати сторонні бібліотеки. Де шукати потрібні нам функції. Як вмикати і налаштовувати периферію GPIO декількома способами, один - через бібліотеки SPL, інший - напряму через регістри мікроконтролера. Та ознайомились з деякими елементами мови програмування C++. Покращили програму шляхом оптимізації і вдосконалили її роботу. Спробували режим Debug за допомоги якого можна розібратись з роботою програми і виявити помилки в роботі програми. Ознайомились з допоміжним софтом: CoAssistant і STM32CubeMX. Цих знань вже достатньо щоб взятись за щось більш цікавіше і в наступній статті створимо електрону гру "Хто швидший" на двох гравців. Буде чим розважитись в родинному колі чи компанії друзів.
Немає коментарів:
Дописати коментар