пʼятницю, 22 грудня 2017 р.

STM32 Cube Programmer для програмування мікроконтролерів STM32


STM32CUBEPROG

Напередодні свят компанія STMicroelectronics дарує своїм прихильникам нову програму для прошивання мікроконтролерів STM32CubeProgrammer.

STM32CubeProgrammer (STM32CUBEPROG) - це все-в-одному, мультиопераційне програмне забезпечення для програмування мікроконтролерів STM32.

Ця програма надає просте у використанні та ефективне середовище для читання, запису та перевірки пам'яті пристрою через інтерфейс налагодження (JTAG і SWD) та інтерфейс завантажувача (UART та USB).

Програма STM32CubeProgrammer пропонує широкий спектр можливостей для програмування внутрішніх пам'яті мікроконтролерів STM32 (таких як Flash, RAM та OTP), а також зовнішніх пам'яті.

Програма STM32CubeProgrammer також дозволяє програмувати та завантажувати параметри, перевіряти програмування вмісту та автоматизувати програмування мікроконтролерів за допомогою сценаріїв.

Програма STM32CubeProgrammer має графічний інтерфейс GUI (графічний інтерфейс користувача) та CLI (інтерфейс командного рядка).

Головні особливості

  • Стирання, запис, перегляди та перевірка вмісту пристрою флеш-пам'яті
  • Підтримка Motorola S19, Intel HEX, ELF та двійкові формати bin
  • Підтримує інтерфейси налагодження та завантажувача:
  • Плата налагодження ST-LINK (JTAG / SWD)
  • Інтерфейси завантажувача UART та USB DFU
  • Програми, стирає та перевіряє зовнішню пам'ять, приклади для зовнішніх флеш-завантажувачів допомагають користувачам розробляти завантажувачі для певної зовнішньої пам'яті
  • Автоматизує програмування STM32 (стирання, перевірка, програмування, налаштування параметрів байтів)
  • Дозволяє програмувати пам'ять OTP
  • Підтримує програмування та налаштування параметрів байтів
  • Пропонує інтерфейс командного рядка для автоматизації через сценарії
  • Оновлення прошивки ST-LINK
  • Вмикає безпечне створення вбудованого програмного забезпечення за допомогою інструмента Trimted Package Creator STM32
  • Підтримка декількох ОС: Windows ® , Linux ® , macOS ®

Зображення програми




середу, 22 листопада 2017 р.

STM32: Керування світлодіодом по UART через термінальну програму на ПК


update 08.01.2021: оновив статтю + додав відео-посібник.

Передмова

Сам по собі чип STM32 дуже цікавий та самодостатній, але якщо він має зв'язок з зовнішнім світом, то сфери його застосування розширюються, та стають цікавішими. Спробуємо взаємодіяти з зовнішнім світом. В цьому прикладі, це буде плата NUCLEO-F103RB (у вас може бути інша плата, принцип залишається) та персональний комп'ютер. З'єднаємо їх за допомоги послідовного інтерфейсу UART або COM порту. Це буде проста програма, яка приймає команди від комп'ютера по послідовному порту і відповідно до команд реагує на них. В нашому прикладі, це буде увімкнення та вимкнення світлодіоду, що на борту плати STM32. 

Налаштування периферії

Запускаємо програму STM32CubeIDE, обираємо свою плату або чип, та починаємо налаштовувати периферію. Якщо було обрано, як в мене, плату NUCLEO-F103RB, то помічник налаштування периферії сам запропонує ініціалізувати периферію в режимі за замовчанням. Погоджуємось і нам залишається тільки додатково увімкнути глобальні переривання для USART2, та додати канал DMA для RX USART2:
Налаштування периферії


USART2 порт, який під'єднаний до програматора ST-Link V2-1 на платі NUCLEO. А програматор з'єднаний з комп'ютером USB шнурком і бачиться комп'ютером, як диск на 156 кБ (вірно для NUCLEO плат), та віртуальний COM порт, який виділить ваша операційна система:

Диск та COM порт програматора

Це дозволить ознайомитись з роботою UART без додаткового обладнання та дротів. 

Вкладку "Clock Configuration" програми STM32CubeMX залишив без змін. Хай буде як є. Перейдемо до вкладки "Configuration", та в зоні "Connectivity" натиснемо на "USART2" перейдемо до налаштувань USART2. У вкладці "Parametr Setting" залишаємо як є:

Налаштування параметрів USART2

  • Baud Rate - 115200 Bits/s
  • Word Length - 8 Bits
  • Parity - NONE
  • Stop Bits - 1
  • Data Direction - Receive and Transmit
  • Over Sampling - 16 Samples

Перейдемо до вкладки "NVIC Setting" та дозволимо переривання від USART2:

Налаштування глобального переривання USART2

А щоб код був "легшим", та не завантажувати MCU будемо для передачі та прийому даних використовувати технологію DMA. Для цього перейдемо до вкладки "DMA Setting". Тиснемо кнопку "Add", обираємо "Select" -> USART2_RX і повторюємо це саме для USART2_TX. А так як в нашому прикладі RX (прийом) будемо слухати завжди, то режим для прийому оберемо, як "circular" (круговий). Прийняли пакет даних - знову готові приймати.

Налаштування DMA

Все інше залишаємо як є. І можна генерувати проект.

Пишемо програму

Відкриваємо, чи імпортуємо, до свого засобу розробки проект і для перевірки пробуємо скласти (компілювати) проект. Як все без помилок, продовжимо далі.

В принципі, додати пару рядків і програма готова до прийому чи передаванню даних. Але є одне "але". Якщо ми передаємо дані, то ми знаємо наперед кількість байтів які хочемо передати і явно вказуємо це HAL функції. А от з прийманням даних не все так гладенько. Бо як будемо приймати "пакети" даних певної, постійної довжини, то теж все гладенько і просто. Вказав, спеціально навченій HAL функції, довжину прийомного буферу і все. А коли це будуть якісь команди різної довжини, чи дані різної довжини? Що тоді робити? Як знати що партнер закінчив свою передачу, а нам треба обробляти прийняте? 

Один з виходів з цієї ситуації це прийом по одному байту, та складання прийнятих байт в окремий буфер і аналіз прийнятих байт. І коли буде прийнятий байт '\n' (кінець рядку), чи '\r' (ENTER), то це буде означати, що передачу партнер закінчив і можна обробляти весь рядок прийнятих байтів на прийомній стороні.

Рішення цієї проблеми я вгледів тут. І ми скористаємось вже готовим рішенням та додаймо до свого проекту. В засобі розробки відкриваємо файл main.с і додаємо рядок:

/* USER CODE BEGIN PD */
#define MAXCLISTRING          100 // Biggest string the user will type
/* USER CODE END PD */

Це максимальна довжина рядку який можливо прийняти за один раз. Тут все.

Додамо трішки зминних:

/* USER CODE BEGIN PV */
/* Private variables ---------------------------------------------------------*/
uint8_t rxBuffer = '\000'; // where we store that one character that just came in
uint8_t rxString[MAXCLISTRING]; // where we build our string from characters coming in
int rxindex = 0; // index for going though rxString
/* USER CODE END PV */

Далі додаємо прототип функцій на початку, та редірект функції "_write" для друку повідомлень за допомоги функції "printf":

/* USER CODE BEGIN PFP */
void executeSerialCommand(uint8_t* string);

int _write(int file, char *ptr, int len)
{
	HAL_UART_Transmit(&huart2, (uint8_t*) ptr, len, HAL_MAX_DELAY);
	return len;
}
/* USER CODE END PFP */

В другу секцію коду для розробника додаємо передачу одного рядку тексту для перевірки передачі по UART2, та ініціацію прийому по UART2:

/* USER CODE BEGIN 2 */
  printf("\r\nReceive UART DMA started!\r\n");
  HAL_UART_Receive_DMA(&huart2, &rxBuffer, 1);
  /* USER CODE END 2 */

Безкінечний цикл залишається пустим:

/* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {

  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

  }
  /* USER CODE END 3 */

}

І в секцію чотири для розробника додаємо функцію, прототип якої ми додали на початку програми плюс callback функцію складання команди з прийнятих байтів:

/* USER CODE BEGIN 4 */
void executeSerialCommand(uint8_t* string)
{
	printf("\r\nConsole > ");

	if (strcmp ((char*) string, "at") == 0)
	{
		printf("OK\r\n");
	}
	else if (strcmp((char*) string, "on") == 0)
	{
		HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, ENABLE);
		printf("LED is ON\r\n");
	}
	else if (strcmp((char*) string, "off") == 0)
	{
		HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, DISABLE);
		printf("LED is OFF\r\n");
	}
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	int i = 0;

	printf((char*) &rxBuffer); // Echo the character that caused this callback so the user can see what they are typing

	if (rxBuffer == 8 || rxBuffer == 127) // If Backspace or del
	{
		printf(" \b"); // "\b space \b" clears the terminal character. Remember we just echoced a \b so don't need another one here, just space and \b
		rxindex--;
		if (rxindex < 0) rxindex = 0;
	}
	else if (rxBuffer == '\n' || rxBuffer == '\r') // If Enter
	{
		executeSerialCommand(rxString);
		rxString[rxindex] = 0;
		rxindex = 0;
		for (i = 0; i < MAXCLISTRING; i++) rxString[i] = 0; // Clear the string buffer
	}
	else
	{
		rxString[rxindex] = rxBuffer; // Add that character to the string
		rxindex++;
		if (rxindex > MAXCLISTRING) // User typing too much, we can't have commands that big
		{
			rxindex = 0;
			for (i = 0; i < MAXCLISTRING; i++) rxString[i] = 0; // Clear the string buffer
		}
	}
}
/* USER CODE END 4 */
  • Функція printf друкує текстові рядки до терміналу
  • Функція executeSerialCommand обробляє прийняті команди (додайте сюди свої команди по аналогії)
  • Функція HAL_UART_RxCpltCallback аналізує кожен прийнятий байт і якщо не кінець рядку, не натиснута клавіша ENTER, чи не переповнився прийомний буфер, то всі прийняті байти складаються до буферу. Як прийом закінчено то буфер звільняється, а прийнятий рядок передається до функції executeSerialCommand.

Перевіряємо роботу програми

Компілюємо проект, якщо все було зроблено вірно, то проект збереться без помилок. Та заливаємо зкомпільовану програму до MCU. Щоб перевірити роботу програми і UARTу, треба на комп'ютері запустити якусь термінальну програму, наприклад Tera Term. Та вказати з яким COM портом і на якій швидкості програма буде працювати. В моєму випадку це буде COM6 і швидкість 115200. 

Налаштування порту

Щоб перевірити програму натисніть кнопку скидання на своїй платі розробника при запущеній програмі терміналу на комп'ютері. В терміналі з'явиться напис "Transmit UART DMA is OK!". Далі в терміналі тисніть клавішу ENTER - з'явиться запрошення для вводу команди "MyHomeIoT>". Наберіть великими літерами "AT" і натисніть ENTER і ваш MCU відповість "OK". Наберіть команду "on" маленькими літерами та завершіть введення ENTER і ваш MCU увімкне світлодіод на платі розробника. Дайте команду "off" і MCU вимкне світлодіод. Ось результат описаної роботи:


Приклад роботи програми

Відео-посібник

В відео більш детально і розгорнуто розглянуто приклад взаємодії мікроконтролера з терміналом. Оформлено окремим файлом (невеличка бібліотечка), розглянуто приклад як самому зробити callback функцію для своїх потреб. Файли з кодом по мотивам відео - тут.


неділю, 19 лютого 2017 р.

STM32: Адаптуємо MicroMenu-2 для мікроконтролерів з єдиним простором адрес

Передмова

В проектах з дисплеєм, коли вся інформація не вміщається на дисплей, чи потрібні якісь маніпуляції з пристроєм, або встановлення налаштувань на пристрої, керування пристроєм, тощо - вже потрібне меню. Щоб за допомоги простої клавіатури чи декількох кнопок "бігати" по меню та здійснювати налаштування, керування, або огляд тих самих налаштувань, параметрів, показань з датчиків і таке інше. Це зрозуміло і переваги наявності меню, у своєму проекті, не підлягає сумнівам.

Звісно, просте меню можна організувати за допомоги списку умов типу if...else або switch...case, а кнопками змінювати значення змінним які будуть відповідати тому чи іншому пункту меню. Але це все просто і добре поки декілька пунктів меню і проект не передбачає на розвиток. Бо як проект буде розростатись та почне потребувати ієрархічного меню, то станеться таке нагромадження заплутаного коду, що розібратись буде складно, а додавання нових пунктів меню перетвориться на пекло. Реалізація меню має бути універсальною і гнучкою.

В свій час, коли шукав готову реалізацію меню для мікроконтролерів, натрапив на цікаву реалізацію меню "MicroMenu-2". Це мікроменю було створено для мікроконтролерів AVR де ROM і RAM починались з нульової адреси і потрібно в коді явно вказувати в яку саме пам'ять заносити те чи інше. Для мікроконтролерів з єдиним простором пам'яті це зайве. І щоб це мікроменю запрацювало на STM32 потрібно внести деякі зміни до коду. А також розберемось на прикладі як тим мікроменю користуватись. Свого часу для мене, новачка, виникли труднощі у застосуванні цього меню, то приклад розглянемо детально, як для початківців, в комплексі з дисплеєм і клавіатурою.

Адаптація MicroMenu-2

Бібліотека "MicroMenu-2" складається з трьох файлів:
  • MenuConfig.h - налаштування де будуть зберігатись пункти меню у FLASH чи SRAM
  • MicroMenu.h - структури, визначення, прототипи функцій меню
  • MicroMenu.c - функції обробки меню
Можна зробити невеличкі зміни до файлу "MenuConfig.h" і користуватись як є. Для цього достатньо закоментувати чи видалити рядок:
#include <avr/pgmspace.h>
Поміняти рядок:
#define MENU_ITEM_STORAGE              PROGMEM
На рядок:
#define MENU_ITEM_STORAGE              const
І ще один рядок:
#define MENU_ITEM_READ_POINTER(Addr)   (void*)pgm_read_word(Addr)
Поміняти на такий:
#define MENU_ITEM_READ_POINTER(Addr)   *(Addr)
Після цих змін "MicroMenu-2" чудово працює і на STM32 мікроконтролерах. Приклад як користуватись є там же ж у автора бібліотеки - "example.c". Для більш-менш "просунутих" цього прикладу має бути достатньо. Можна ще тут глянути.

Для себе, зробив зміни у бібліотеці таким чином, щоб взагалі позбавитись файлу "MenuConfig.h" і бібліотека тепер складається з двох файлів "MicroMenu.h":
/**
              MICRO-MENU V2

          (C) Dean Camera, 2012
        www.fourwalledcubicle.com
     dean [at] fourwalledcubicle.com

        Royalty-free for all uses.
                                   */
#ifndef _MICRO_MENU_H_
#define _MICRO_MENU_H_

 #include <stddef.h>

 /** Type define for a menu item. Menu items should be initialized via the helper
  *  macro \ref MENU_ITEM(), not created from this type directly in user-code.
  */
 typedef const struct Menu_Item {
  const struct Menu_Item *Next; /**< Pointer to the next menu item of this menu item */
  const struct Menu_Item *Previous; /**< Pointer to the previous menu item of this menu item */
  const struct Menu_Item *Parent; /**< Pointer to the parent menu item of this menu item */
  const struct Menu_Item *Child; /**< Pointer to the child menu item of this menu item */
  void (*SelectCallback)(void); /**< Pointer to the optional menu-specific select callback of this menu item */
  void (*EnterCallback)(void); /**< Pointer to the optional menu-specific enter callback of this menu item */
  const char Text[]; /**< Menu item text to pass to the menu display callback function */
 } Menu_Item_t;

 /** Creates a new menu item entry with the specified links and callbacks.
  *
  *  \param[in] Name      Name of the menu entry, must be unique.
  *  \param[in] Next      Name of the next linked menu item, or \ref NULL_MENU if no menu link.
  *  \param[in] Previous  Name of the previous linked menu item, or \ref NULL_MENU if no menu link.
  *  \param[in] Parent    Name of the parent linked menu item, or \ref NULL_MENU if no menu link.
  *  \param[in] Child     Name of the child linked menu item, or \ref NULL_MENU if no menu link.
  *  \param[in] SelectFunc  Function callback to execute when the menu item is selected, or \c NULL for no callback.
  *  \param[in] EnterFunc   Function callback to execute when the menu item is entered, or \c NULL for no callback.
  */
 #define MENU_ITEM(Name, Next, Previous, Parent, Child, SelectFunc, EnterFunc, Text) \
  extern Menu_Item_t const Next;     \
  extern Menu_Item_t const Previous; \
  extern Menu_Item_t const Parent;   \
  extern Menu_Item_t const Child;  \
  Menu_Item_t const Name = {&Next, &Previous, &Parent, &Child, SelectFunc, EnterFunc, Text}

 /** Relative navigational menu entry for \ref Menu_Navigate(), to move to the menu parent. */
 #define MENU_PARENT         (Menu_GetCurrentMenu()->Parent)

 /** Relative navigational menu entry for \ref Menu_Navigate(), to move to the menu child. */
 #define MENU_CHILD          (Menu_GetCurrentMenu()->Child)

 /** Relative navigational menu entry for \ref Menu_Navigate(), to move to the next linked menu item. */
 #define MENU_NEXT           (Menu_GetCurrentMenu()->Next)

 /** Relative navigational menu entry for \ref Menu_Navigate(), to move to the previous linked menu item. */
 #define MENU_PREVIOUS       (Menu_GetCurrentMenu()->Previous)

 /** Null menu entry, used in \ref MENU_ITEM() definitions where no menu link is to be made. */
 extern Menu_Item_t NULL_MENU;

 /** Retrieves the currently selected meny item.
  *
  *  \return Pointer to the currently selected meny item.
  */
 Menu_Item_t* Menu_GetCurrentMenu(void);

 /** Navigates to an absolute or relative menu entry.
  *
  * \param[in] NewMenu  Pointer to the absolute menu item to select, or one of \ref MENU_PARENT,
  *                     \ref MENU_CHILD, \ref MENU_NEXT or \ref MENU_PREVIOUS for relative navigation.
  */
 void Menu_Navigate(Menu_Item_t* const NewMenu);

 /** Configures the menu text write callback function, fired for all menu items. Within this callback
  *  function the user should implement code to display the current menu text stored in \ref MENU_ITEM_STORAGE
  *  memory space.
  *
  *  \ref WriteFunc  Pointer to a callback function to execute for each selected menu item.
  */
 void Menu_SetGenericWriteCallback(void (*WriteFunc)(const char* Text));

 /** Enters the currently selected menu item, running its configured callback function (if any). */
 void Menu_EnterCurrentItem(void);

#endif
 та "MicroMenu.c":
/**
              MICRO-MENU V2

          (C) Dean Camera, 2012
        www.fourwalledcubicle.com
     dean [at] fourwalledcubicle.com

        Royalty-free for all uses.
                                   */

#include "MicroMenu.h"

/** This is used when an invalid menu handle is required in
 *  a \ref MENU_ITEM() definition, i.e. to indicate that a
 *  menu has no linked parent, child, next or previous entry.
 */
Menu_Item_t NULL_MENU = {0};

/** \internal
 *  Pointer to the generic menu text display function
 *  callback, to display the configured text of a menu item
 *  if no menu-specific display function has been set
 *  in the select menu item.
 */
static void (*MenuWriteFunc)(const char* Text) = NULL;

/** \internal
 *  Pointer to the currently selected menu item.
 */
static Menu_Item_t* CurrentMenuItem = &NULL_MENU;


Menu_Item_t* Menu_GetCurrentMenu(void)
{
 return CurrentMenuItem;
}

void Menu_Navigate(Menu_Item_t* const NewMenu)
{
 if ((NewMenu == &NULL_MENU) || (NewMenu == NULL))
  return;

 CurrentMenuItem = NewMenu;

 if (MenuWriteFunc)
  MenuWriteFunc(CurrentMenuItem->Text);

 void (*SelectCallback)(void) = CurrentMenuItem->SelectCallback;

 if (SelectCallback)
  SelectCallback();
}

void Menu_SetGenericWriteCallback(void (*WriteFunc)(const char* Text))
{
 MenuWriteFunc = WriteFunc;
 Menu_Navigate(CurrentMenuItem);
}

void Menu_EnterCurrentItem(void)
{
 if ((CurrentMenuItem == &NULL_MENU) || (CurrentMenuItem == NULL))
  return;

 void (*EnterCallback)(void) = CurrentMenuItem->EnterCallback;

 if (EnterCallback)
  EnterCallback();
}
Завантажити адаптовану бібліотеку для STM32 можна по цьому посиланню - "MicroMenu-2 for STM32".

Залізо для прикладу

Для прикладу буду використовувати:

Мініплату розробника STM32F103C8T6 (а ви будь яку іншу, це не важливо):
WH1602A
Схема підключення дисплею буде такою:
Схема підключення дисплею
Схема підключення кнопок:

Підготовка проекту для прикладу

Запускаємо CubeMX, обираємо свій мікроконтролер, або свою плату розробника і позначаємо ніжки для дисплею на вихід, а для кнопок на вхід, як на малюнку (ви можете обрати будь які інші ніжки мікроконтролера головне дати назви ніжкам як на малюнку):
Налаштування ніжок для прикладу
Ніжки куди під'єднано дисплей налаштуйте на вихід GPIO_OutPut, та назвіть за принципом:
  • D4 - LCD_D4 
  • D5 - LCD_D5
  • D6 - LCD_D6
  • D7 - LCD_D7
  • RS - LCD_RS
  • Enable - LCD_E
  • Підсвічування дисплею (за потреби) - LCD_BACKLIGHT
Ніжки куди під'єднані кнопки налаштуйте на вхід GPIO_InPut, та назвіть за принципом:
  • ліворуч - BUTTON_LEFT
  • праворуч - BUTTON_RIGHT
  • догори - BUTTON_UP
  • вниз - BUTTON_DOWN
  • вибір - BUTTON_SELECT
Генеруєте проект для свого засобу розробки. З CubeMX все.

Скопіюйте до згенерованого проекту файли бібліотек для роботи з "WH1602" і файли "MicroMenu-2". Заголовні файли до теки "inc" проекту, а сирцеві файли бібліотек до теки "src" згенерованого CubeMX проекту.

Меню без дисплею і кнопок керування (клавіатури) втрачає сенс, то ж приділимо дисплею і кнопкам деяку увагу в прикладі, щоб було максимально зрозуміло і для початківців, як вбудовувати micromenu до своїх проектів.

Підключаємо дисплей

До головного файлу main.c в розділі користувацьких #include додаємо такий рядок:
/* USER CODE BEGIN Includes */

#include "hd44780.h"

/* USER CODE END Includes */
Під'єднали бібліотеку. Тепер викличемо функцію ініціалізації дисплею в головній функції в користувацькому блоці 2:
/* USER CODE BEGIN 2 */
 
lcdInit();
 
/* USER CODE END 2 */
Щодо дисплею, це поки все. Тепер напишемо код обробника кнопок.

Кнопки (клавіатура)

До файлу main.h у розділ USER CODE Private defines додамо такі рядки:
/* USER CODE BEGIN Private defines */

#define BUTTONn                          5 // Кількість кнопок

typedef enum
{
  BUTTON_LEFT   = 0,
  BUTTON_UP   = 1,
  BUTTON_DOWN   = 2,
  BUTTON_RIGHT   = 3,
  BUTTON_SELECT  = 4,
  BUTTON_NOTHING  = 255
} Button_TypeDef;
/* USER CODE END Private defines */
Назначили кількість кнопок для опитування та нумерований тип Button_TypeDef:

В розділ USER CODE PFP у файлі main.c додамо прототип функції отримання коду натиснутої кнопки:
/* USER CODE BEGIN PFP */
/* Private function prototypes -----------------------------------------------*/

static uint8_t getPressKey(void); // Функція отримання коду натиснутої кнопки

/* USER CODE END PFP */
А в розділі USER CODE 4 файлу main.c додамо саму функцію отримання коду натиснутої кнопки:
/* USER CODE BEGIN 4 */

static uint8_t getPressKey(void)
{
  for(uint8_t i = 0; i < BUTTONn; i++)
  {
   if(HAL_GPIO_ReadPin(BUTTON_PORT[i],BUTTON_PIN[i]) && !flagButton[i])
    {
     flagButton[i] = true;
     key_pressed = i;
    }

   if(!HAL_GPIO_ReadPin(BUTTON_PORT[i],BUTTON_PIN[i]) && flagButton[i])
   {
    flagButton[i] = false;
    key_pressed = BUTTON_NOTHING;
   }
  }
  return key_pressed;
}

/* USER CODE END 4 */
Щоб функція опитування кнопок працювала правильно потрібно ще додати деякі змінні на початку головного файлу main.c в користувацький блок приватних змінних. Додамо рядки зі змінними в блок USER CODE PV:
/* USER CODE BEGIN PV */
/* Private variables ---------------------------------------------------------*/

bool flagButton[BUTTONn] = {};

Button_TypeDef key_pressed = BUTTON_NOTHING;

GPIO_TypeDef* BUTTON_PORT[BUTTONn] = {BUTTON_LEFT_GPIO_Port,
     BUTTON_UP_GPIO_Port,
     BUTTON_DOWN_GPIO_Port,
     BUTTON_RIGHT_GPIO_Port,
     BUTTON_SELECT_GPIO_Port};

const uint16_t BUTTON_PIN[BUTTONn] = {BUTTON_LEFT_Pin,
     BUTTON_UP_Pin,
     BUTTON_DOWN_Pin,
     BUTTON_RIGHT_Pin,
     BUTTON_SELECT_Pin};

/* USER CODE END PV */
А щоб можна було оперувати змінними типу bool потрібно додати бібліотеку "stdbool.h". Блок користувацьких вкладень у файлі main.c буде тепер виглядати так:
/* USER CODE BEGIN Includes */

#include "hd44780.h"
#include <stdbool.h>

/* USER CODE END Includes */
Все готово для вбудовування micromenu до проекту. На дисплей вже можна виводити пункти меню і всіляку інформацію, а кнопками "бігати" по меню і міняти якість параметри/значення. Для перевірки можна запустити компілятор і пересвідчитись що проект збереться без помилок, а значить ми на вірному шляху.

MicroMenu-2

Для прикладу, щоб не переобтяжувати статтю, створимо нескладне меню. Але достатнє щоб зрозуміти принцип. Меню буде такої структури:
Структура меню
Перший рівень буде мати три пункти меню: Menu-1, Menu-2 і Menu-3. З Menu-1 можна буде потрапити до другого рівня меню з двох пунктів: Menu-1.1, Menu-1.2. А з Menu-1.1 можна буде потрапити до єдиного пункту - меню третього рівня: Menu-1.1.1. Розпочнімо!

Перш за все треба вкласти до нашого проекту-прикладу бібліотеку "micromenu-2", файли вже заздалегідь було додано, залишилось додати рядок до USER CODE Includes в файлі main.c. Тепер блок з вкладеннями буде виглядати так:
/* USER CODE BEGIN Includes */

#include "hd44780.h"         // Бібліотека для дисплею WH1602
#include <stdbool.h> // Бібліотека визначення змінних типу bool
#include "MicroMenu.h"         // Бібліотека мікроменю

/* USER CODE END Includes */
Тепер потрібно створити функцію яка буде виводити на екран назви пунктів меню за замовчуванням. Прототип функції додамо до блоку USER CODE PFP в файлі main.c .Тепер цей блок виглядатиме так:
/* USER CODE BEGIN PFP */
/* Private function prototypes -----------------------------------------------*/

static uint8_t getPressKey(void);  // Функція отримання коду натиснутої кнопки
static void Generic_Write(const char* Text);    // Функція виводу тексту на дисплей за замовчуванням

/* USER CODE END PFP */
А саму функцію додамо до блоку USER CODE 4 в файлі main.c:
static void Generic_Write(const char* Text)
{
 if (Text)   // Якщо є текст то...
 {
  lcdClrScr(); // Очистимо дисплей
  lcdPuts(Text); // Надрукуємо текст
 }
}
Можна розпочинати створювати безпосередньо самі пункти меню, їх назви, взаємодію з іншими меню і реакцію на виклик пунктів меню. До блоку USER CODE 0 в файлі main.c додамо спочатку меню першого рівня:
/* USER CODE BEGIN 0 */

// Menus  Name | Next | Prev | Parent | Child | SelectFunction | EnterFunction | Text
MENU_ITEM(Menu_1, Menu_2, Menu_3, NULL_MENU, NULL_MENU, NULL, NULL, "\tMenu-1");
MENU_ITEM(Menu_2, Menu_3, Menu_1, NULL_MENU, NULL_MENU, NULL, NULL, "\tMenu-2");
MENU_ITEM(Menu_3, Menu_1, Menu_2, NULL_MENU, NULL_MENU, NULL, NULL, "\tMenu-3");

/* USER CODE END 0 */
Де в дужках по порядку: поточне ім'я меню, яке буде наступне меню, яке буде попереднє меню, меню предок, меню нащадок, виклик функції коли обрано меню, виклик функції коли натиснуто "ENTER" та текст самого меню. Як бачимо наступне і попереднє меню прописано так щоб при натисканні кнопок вниз чи догори обиралось наступне чи попереднє меню та створено коло з меню (з першого можна потрапити до останнього та навпаки). Поки що не прописані меню нащадки і ніякі функції не викликаються.

Додамо ще одну важливу деталь. Виклик функції показу на дисплей тексту меню за замовчуванням, коли викликається те чи інше меню, та яке меню буде початковим при увімкнені. Ось ці рядки додайте до блоку USER CODE 2 де вже знаходиться функція ініціалізації дисплею:
/* Set up the default menu text write callback, and navigate to an absolute menu item entry. */
Menu_SetGenericWriteCallback(Generic_Write);
Menu_Navigate(&Menu_1);
І щоб вже спробувати "побігати" по меню і побачити хоч якийсь результат, потрібно до безкінечного циклу while(1) в головній функції main() файлу main.c написати взаємодію меню з кнопками (клавіатурою). Додамо такі рядки до нашого прикладу в блок USER CODE WHILE:
  /* USER CODE BEGIN WHILE */
  while (1)
  {
   if (getPressKey() != BUTTON_NOTHING && !flagPressed)
   {
    flagPressed = true;
    switch(getPressKey())
    {
     case BUTTON_LEFT:
      Menu_Navigate(MENU_PARENT);
      break;
     case BUTTON_UP:
      Menu_Navigate(MENU_PREVIOUS);
      break;
     case BUTTON_DOWN:
      Menu_Navigate(MENU_NEXT);
      break;
     case BUTTON_RIGHT:
      Menu_Navigate(MENU_CHILD);
      break;
     case BUTTON_SELECT:
      Menu_EnterCurrentItem();
      break;
     default:
      break;
    }
   } else if (getPressKey() == BUTTON_NOTHING && flagPressed) {
    flagPressed = false;
 }
  /* USER CODE END WHILE */
Та оголосимо в розділі USER CODE 1 змінну типу bool - прапорець натиснутої кнопки:
  /* USER CODE BEGIN 1 */
 bool flagPressed = false;
  /* USER CODE END 1 */
При натисканні кнопки "ліворуч" буде викликатись меню-предок. При натисканні кнопки "догори" буде викликатись попереднє меню. При натисканні кнопки "вниз" буде викликатись наступне меню. При натисканні кнопки "праворуч" буде викликатись меню-нащадок. При натисканні кнопки "вибір" буде викликатись функція прописана в розділі пункту меню -"EnterFunction".

Можна вже компілювати проект та заливати прошивку до макету. Натискайте кнопки "догори" чи "вниз" і побачите на дисплеї як міняється текст з назвами пунктів меню. На інші кнопки - ніякої реакції. Так і має бути. Йдемо далі.

Додамо пункти меню другого рівня. Розташуємо ці рядки до рядків, з меню першого рівня, що ми вже створили:
MENU_ITEM(Menu_1_1, Menu_1_2, Menu_1_2, Menu_1, NULL_MENU, NULL, NULL, "\tMenu-1.1");
MENU_ITEM(Menu_1_2, Menu_1_1, Menu_1_1, NULL_MENU, NULL_MENU, NULL, NULL, "\tMenu-1.2");
В першому меню другого рівня - Menu_1_1 є вихід до меню першого рівня Menu_1. Але ж так само треба з меню першого рівня зробити вхід до меню другого рівня. До рядка опису першого пункту меню, першого рівня внесемо такі зміни:
MENU_ITEM(Menu_1, Menu_2, Menu_3, NULL_MENU, Menu_1_1, NULL, NULL, "\tMenu-1");
І загалом тепер опис нашого меню буде виглядати так:
/* USER CODE BEGIN 0 */

// Menus  Name | Next | Prev | Parent | Child | SelectFunction | EnterFunction | Text
MENU_ITEM(Menu_1, Menu_2, Menu_3, NULL_MENU, Menu_1_1, NULL, NULL, "\tMenu-1");
MENU_ITEM(Menu_2, Menu_3, Menu_1, NULL_MENU, NULL_MENU, NULL, NULL, "\tMenu-2");
MENU_ITEM(Menu_3, Menu_1, Menu_2, NULL_MENU, NULL_MENU, NULL, NULL, "\tMenu-3");

MENU_ITEM(Menu_1_1, Menu_1_2, Menu_1_2, Menu_1, NULL_MENU, NULL, NULL, "\tMenu-1.1");
MENU_ITEM(Menu_1_2, Menu_1_1, Menu_1_1, NULL_MENU, NULL_MENU, NULL, NULL, "\tMenu-1.2");

/* USER CODE END 0 */
Компілюйте, заливайте до MCU і пробуйте потрапити з Menu-1 до Menu-1.1 натиснувши кнопку "праворуч". З Menu-1.1 кнопкою "ліворуч" можна повернутись до Menu-1.

По такому ж принципу створимо пункт меню третього рівня Menu-1.1.1. Не забуваємо прописати точку входу і виходу. Тепер опис нашого меню буде виглядати так:
/* USER CODE BEGIN 0 */

// Menus  Name | Next | Prev | Parent | Child | SelectFunction | EnterFunction | Text
MENU_ITEM(Menu_1, Menu_2, Menu_3, NULL_MENU, Menu_1_1, NULL, NULL, "\tMenu-1");
MENU_ITEM(Menu_2, Menu_3, Menu_1, NULL_MENU, NULL_MENU, NULL, NULL,"\tMenu-2");
MENU_ITEM(Menu_3, Menu_1, Menu_2, NULL_MENU, NULL_MENU, NULL, NULL, "\tMenu-3");

MENU_ITEM(Menu_1_1, Menu_1_2, Menu_1_2, Menu_1, Menu_1_1_1, NULL, NULL, "\tMenu-1.1");
MENU_ITEM(Menu_1_2, Menu_1_1, Menu_1_1, NULL_MENU, NULL_MENU, NULL, NULL, "\tMenu-1.2");

MENU_ITEM(Menu_1_1_1, NULL_MENU, NULL_MENU, Menu_1_1, NULL_MENU, NULL, NULL, "\tMenu-1.1.1");

/* USER CODE END 0 */
Компілюйте, заливайте, пробуйте.

Залишилось додати функцій до нашого меню, щоб можна було щось міняти на нашому макеті, бо просто "бігати" по пунктах меню - який в тому сенс?

Якщо підсвічування дисплею під'єднати до живлення через транзисторний ключ, то можна програмно керувати підсвічуванням дисплею. Вмикати чи вимикати через наше меню.
Схема підключення підсвічування дисплею має бути такою.
Схема підключення підсвічування дисплею через транзисторний ключ
В мене підсвічування дисплею під'єднано до ніжки PB3 мікроконтролера, яка налаштована на вихід і названий цей PIN в CubeMX як LCD_BLACKLIGHT (дивись малюнок "Налаштування ніжок для прикладу"). Тепер до меню можна додати функцію, яка буде вмикати чи вимикати підсвічування дисплею. Або можна, замість підсвічування дисплею, "поблимати" набортним світлодіодом на PC13.

Наприклад, за увімкнення чи вимкнення підсвічування дисплею буде відповідати "Menu_3". Тоді робимо таку послідовність дій:
  • В блоці "USER CODE PFP" оголошуємо прототип функції для цього меню;
// Menus function
static void Level1Item3_Enter(void);
  • Пишемо саму функцію для цього меню, де: очистимо дисплей, надрукуємо інформацію, зчитаємо стан підсвічування і надрукуємо його, та увімкнемо чи вимкнемо підсвічування дисплею в залежності від натиснутої кнопки. Після завершення очищаємо дисплей і повертаємось до поточного пункту меню:
static void Level1Item3_Enter(void)
{

 bool flagPressed = false; // Прапорець натиснутої кнопки

 lcdClrScr();   // Очищаємо екран дисплею

 lcdGoto(LCD_1st_LINE,0); // Друкуємо в першому рядку
 lcdPuts("  BackLight is"); // Назву параметру

// Поки не натиснути кнопка "ліворуч" тут виконує функцію як "вихід"
 while(getPressKey() != BUTTON_LEFT)
 {

  // Перевіримо в якому стані ніжка мікроконтролера куди під'єднано підсвічування дисплею
  if(HAL_GPIO_ReadPin(LCD_BACKLIGHT_GPIO_Port,LCD_BACKLIGHT_Pin))
  {
   lcdGoto(LCD_2nd_LINE,0); // В другому рядку друкуємо стан параметру
   lcdPuts("\tON ");   // Підсвічування увімкнено
  } else {
   lcdGoto(LCD_2nd_LINE,0); // В інакшому разі
   lcdPuts("\tOFF");   // Підсвічування вимкнено
  }
// Скануємо тільки кнопки "вгору" і "вниз"
  if(getPressKey() != BUTTON_NOTHING && !flagPressed)
  {
   flagPressed = true; // Коли якусь кнопку натиснули

   switch(getPressKey())  // Перевіряємо що натиснуто
   {
   case BUTTON_UP:  // Якщо кнопку "вгору", то увімкнемо підсвічування дисплею
    HAL_GPIO_WritePin(LCD_BACKLIGHT_GPIO_Port,LCD_BACKLIGHT_Pin,ENABLE);
    break;
   case BUTTON_DOWN: // Якщо кнопку "вниз", то вимкнемо підсвічування дисплею
    HAL_GPIO_WritePin(LCD_BACKLIGHT_GPIO_Port,LCD_BACKLIGHT_Pin,DISABLE);
    break;
   default:  // В будь якому іншому випадку просто вихід з switch
    break;
   }
  } else if(getPressKey() == BUTTON_NOTHING && flagPressed) {
   flagPressed = false; // Коли кнопку відпустили
  }
 }

 lcdClrScr();  // Очищення дисплею
 Menu_Navigate(&Menu_3); // Повертаємося до того ж меню де були
}
  • Та на останок потрібно ще змінити сам пункт меню таким чином, щоб було посилання на функцію і назву меню більш конкретизувати, щоб було зрозуміло що до чого:
MENU_ITEM(Menu_3, Menu_1, Menu_2, NULL_MENU, NULL_MENU, NULL, Level1Item3_Enter, "  Press Enter\nON/OFF BACKLIGHT");
Тепер можна компілювати, заливати і пробувати як все це працює.

Післямова

Це лише маленький приклад реалізації меню. "Micromenu-2" є дуже гнучкою і зручною реалізацією і далі вже залежить від ваших навичок і фантазії. Наприклад, в кожне меню, в розділ "SelectFunction", можна додати виклик функції, яка відповідає за короткий високий звук "біп". Тоді "бігаючи" по меню ваш пристрій буде весело "біпкати". Також реалізація функцій розділу "EnterFunction" може відрізнятись. Це лише приклад. Можна скоротити кількість кнопок. Наприклад, якщо меню і значення параметрів "закільцьовані", то можна позбутись кнопки "вгору" чи "вниз". Або не використовувати кнопку "Select", яка виконувала, в прикладі, функцію кнопки "Enter". Замість цього, створити додаткові меню, при входженні до якого вже викликати функцію зміни якихось параметрів, налаштувань, тощо. Або зробити відстеження одиночного чи подвійного натискання кнопки і відповідно реагувати на це. Також можна опитування клавіатури (кнопок) оформити як окрему бібліотеку. Дерзайте!

Файли

середу, 8 лютого 2017 р.

STM32: Бібліотека для 7-ми сегментного дисплею


Передмова

Семисегментний дисплей, мабуть, найдешевший з дисплеїв. І його застосування в деяких проектах цілком виправдане. З недоліків, це мала інформативність (тільки цифри і деякі знаки), та займає багато ніжок мікроконтролера (7 ніжок на сегменти + 1 ніжка "кома" + по одному виводу мікроконтролера на кожен розряд дисплею).

Бібліотека проста в застосуванні та налаштуванні. Може працювати з 1 - 9 розрядним дисплеєм. Можна при необхідності додати розрядність по аналогії (якщо це комусь може знадобитись). Бібліотека створювалась під свої потреби і має обмежений "сервіс". Але при необхідності можна додати функцій. Наприклад, рядок цифр що біжить (scroll лівий, правий), вивід чисел типу float, тощо. Також майте на увазі, що сегменти дисплею мають бути під'єднані до виводів якогось одного порту мікроконтролера, а розряди дисплею вже по бажанню/можливостям/зручностям, до будь яких портів і виводів мікроконтролеру.

Як приклад, розглянемо схему підключення, налаштування та роботу з чотирьох розрядним 7-сегментним дисплеєм з загальним катодом. Підключати будемо до плати розробника STM32VLDiscovery на мікроконтролері STM32F100RB. Але у вас може бути будь який інший мікроконтролер.

Схема підключення дисплею

На схемі вказано до яких ніжок мікроконтролера під'єднано дисплей. Це для прикладу, який будемо розглядати далі. А ви можете під'єднувати до виводів мікроконтролеру по своїм міркуванням. Головне щоб сегменти дисплею (A,B,C,D,E,F,G і H (DP)) знаходились на одному порту мікроконтролера (GPIOA, GPIOB, GPIOC, GPIOD, тощо). Тисніть на малюнок для збільшення.
Схема підключення 7-сегментного дисплею

Опис функцій бібліотеки LED7Seg

  • void LED7SegInit(void) - функція ініціалізації дисплею. Викликати, якщо проект не створювався за допомоги CUBEMX. Потрібно внести корективи у функцію, щоб відповідало до яких портів і виводів під'єднаний дисплей. Якщо проект створювався за допомоги CUBEMX, де вказані ніжки до яких під'єднаний дисплей, то викликати цю функцію не треба взагалі. CUBEMX сам згенерує ініціалізацію потрібних ніжок і портів.
  •  void LED7SegPrint(uint32_t incomingNumber) - функція друку цілого позитивного числа. Вхідні параметри: ціле позитивне число.
  • void LED7SegPrintSeparate(uint8_t* aDigit, uint8_t number_of_digits) - функція друку чисел (або знаків, таких як "мінус", або взагалі "пусто") в кожен розряд окремо. Вхідні параметри: вказівник на масив з чисел (знаків), та кількість розрядів дисплею.
  • void LED7SegShow(void) - ця функція відповідає саме за світіння потрібних сегментів і розрядів дисплею (динамічна індикація). Цю функцію потрібно викликати як можна частіше. Розмістити її в головному безкінечному циклі, та потурбуватись, щоб виклик цієї функції ніщо не затримувало. Або створити окреме переривання по таймеру для виклику цієї функції. Або, якщо використовувати RTOS, створити окрему задачу для виклику цієї функції з високим пріоритетом.

LED7Seg.h

Заголовний файл LED7Seg.h містить підключення файлу CMSIS периферії вашого мікроконтролеру в прикладі це #include "stm32f1xx.h". Визначення назв ніжок і портів до яких під'єднано дисплей, якщо це не визначено у файлі main.h за допомоги CUBEMX. Визначено з яких сегментів складаються цифри на дисплеї. Деякі знаки, які можна відтворити на дисплеї. Кількість розрядів дисплею. Прототипи функцій.
/*
 * Автор: Андрій Гончаренко
 * Email: taburyak@gmail.com
 * м.Дніпро
 * 2017
 * Бібліотека у вільному розповсюдженні
 */

#ifndef _LED7SEG_H
#define _LED7SEG_H

// підключити файл з вашою серією мікроконтролера
#include "stm32f1xx.h"

// якщо не визначили ніжки і порти до яких під'єднано дисплей у файлі main.h за допомоги CUBEMX,
// то треба ніжки і порти позначити тут відповідно до електричної схеми підключення дисплею
#if !defined SEG_A_Pin
 #define SEG_A_Pin        GPIO_PIN_0
 #define SEG_A_GPIO_Port  GPIOB
#endif

#if !defined SEG_B_Pin
 #define SEG_B_Pin        GPIO_PIN_1
 #define SEG_B_GPIO_Port  GPIOB
#endif

#if !defined SEG_C_Pin
 #define SEG_C_Pin        GPIO_PIN_2
 #define SEG_C_GPIO_Port  GPIOB
#endif

#if !defined SEG_D_Pin
 #define SEG_D_Pin        GPIO_PIN_3
 #define SEG_D_GPIO_Port  GPIOB
#endif

#if !defined SEG_E_Pin
 #define SEG_E_Pin        GPIO_PIN_4
 #define SEG_E_GPIO_Port  GPIOB
#endif

#if !defined SEG_F_Pin
 #define SEG_F_Pin        GPIO_PIN_5
 #define SEG_F_GPIO_Port  GPIOB
#endif

#if !defined SEG_G_Pin
 #define SEG_G_Pin        GPIO_PIN_6
 #define SEG_G_GPIO_Port  GPIOB
#endif

#if !defined SEG_H_Pin
 #define SEG_H_Pin        0u
 #define SEG_H_GPIO_Port  0u
#endif

#if !defined D0_Pin
 #define D0_Pin         GPIO_PIN_8
 #define D0_GPIO_Port   GPIOB
#endif

#if !defined D1_Pin
 #define D1_Pin         GPIO_PIN_9
 #define D1_GPIO_Port   GPIOB
#endif

#if !defined D2_Pin
 #define D2_Pin         GPIO_PIN_6
 #define D2_GPIO_Port   GPIOC
#endif

#if !defined D3_Pin
 #define D3_Pin         GPIO_PIN_6
 #define D3_GPIO_Port   GPIOA
#endif

#if !defined D4_Pin
 #define D4_Pin         0u
 #define D4_GPIO_Port   0u
#endif

#if !defined D5_Pin
 #define D5_Pin         0u
 #define D5_GPIO_Port   0u
#endif

#if !defined D6_Pin
 #define D6_Pin         0u
 #define D6_GPIO_Port   0u
#endif

#if !defined D7_Pin
 #define D7_Pin         0u
 #define D7_GPIO_Port   0u
#endif

#if !defined D8_Pin
 #define D8_Pin         0u
 #define D8_GPIO_Port   0u
#endif

// складаємо з окремих сегментів відповідні цифри
#define DIG0   ( SEG_A_Pin | SEG_B_Pin | SEG_C_Pin | SEG_D_Pin | SEG_E_Pin | SEG_F_Pin )
#define DIG1   ( SEG_B_Pin | SEG_C_Pin )
#define DIG2   ( SEG_A_Pin | SEG_B_Pin | SEG_G_Pin | SEG_E_Pin | SEG_D_Pin )
#define DIG3   ( SEG_A_Pin | SEG_B_Pin | SEG_G_Pin | SEG_C_Pin | SEG_D_Pin )
#define DIG4   ( SEG_F_Pin | SEG_G_Pin | SEG_B_Pin | SEG_C_Pin )
#define DIG5   ( SEG_A_Pin | SEG_F_Pin | SEG_G_Pin | SEG_C_Pin | SEG_D_Pin )
#define DIG6   ( SEG_A_Pin | SEG_C_Pin | SEG_D_Pin | SEG_E_Pin | SEG_F_Pin | SEG_G_Pin )
#define DIG7   ( SEG_A_Pin | SEG_B_Pin | SEG_C_Pin )
#define DIG8   ( SEG_A_Pin | SEG_B_Pin | SEG_C_Pin | SEG_D_Pin | SEG_E_Pin | SEG_F_Pin | SEG_G_Pin )
#define DIG9   ( SEG_A_Pin | SEG_B_Pin | SEG_C_Pin | SEG_D_Pin | SEG_F_Pin | SEG_G_Pin )
// крапка
#define POINT   ( SEG_H_Pin )
// складемо всі сегменти в купу
#define ALL_SEG ( SEG_A_Pin | SEG_B_Pin | SEG_C_Pin | SEG_D_Pin | SEG_E_Pin | SEG_F_Pin | SEG_G_Pin | SEG_H_Pin )
// порядковий номер в масиві digitsp знаку чи символу
#define NOP  10   // знак "пусто" всі сегменти відключено
#define MINUS  11 // знак - "мінус"

// всі сегменти мають бути під'єднано до одного порту і тут визначається загальний для сегментів порт
#define SEG_PORT SEG_A_GPIO_Port

// кількість розрядів дисплею
#define NUM_DIGIT 4

// Ініціалізація периферії дисплею
void LED7SegInit(void);
// Надрукувати число на дисплеї
void LED7SegPrint(uint32_t incomingNumber);
// Надрукувати числа в дисплей для кожного розряду окреме число
void LED7SegPrintSeparate(uint8_t* aDigit, uint8_t number_of_digits);
// // Запалювання потрібних сегментів та розрядів дисплею (динамічна індикація)
void LED7SegShow(void);
// виставляє в відповідні розряди індикатора числа-розряди
void digitToPort (uint8_t digit);
// число з лічильника розбиває на окремі розряди. вхідні аргументи: значення лічильника, масив для розрядів, кількість розрядів.
void splitToDigit(uint32_t counter, uint8_t* digit, uint8_t number_of_digits);

#endif
 
/*--------------------------------------------------------------------------------*/
//  ENF OF FILE
/*--------------------------------------------------------------------------------*/

LED7Seg.c

Файл LED7Seg.c містить вкладення заголовного файлу, масиви і змінні та самі функції бібліотеки.
/*
 * Автор: Андрій Гончаренко
 * м.Дніпро
 * 2017
 * Бібліотека у вільному розповсюдженні
 */

#include <led7seg.h>
 
// Масив для числа окремо цифра в кожен розряд індикатора
static uint8_t digit[NUM_DIGIT] = {};
// оголошуємо масив з назвами ніжок куди під'єднані розряди індикатора
static const uint16_t digitPin[] = {D0_Pin, D1_Pin, D2_Pin,
                                    D3_Pin, D4_Pin, D5_Pin,
                                    D6_Pin, D7_Pin, D8_Pin};
// оголошуємо масив з назвами портів куди під'єднані розряди індикатора
static GPIO_TypeDef* digitPort[] = {D0_GPIO_Port, D1_GPIO_Port, D2_GPIO_Port,
                                    D3_GPIO_Port, D4_GPIO_Port, D5_GPIO_Port,
                                    D6_GPIO_Port, D7_GPIO_Port, D8_GPIO_Port};
//оголошуємо масив з можливими варіантами символами на індикатор
static const uint16_t digitsp[]={DIG0,DIG1,DIG2,DIG3,DIG4,DIG5,DIG6,DIG7,DIG8,DIG9,~ALL_SEG,SEG_G_Pin};
//Функції ініціалізації та виводу інформації на дисплей

//Ініціалізація периферії до якої підключено дисплей
void LED7SegInit(void)
{
 GPIO_InitTypeDef GPIO_InitStruct;

   /* GPIO Ports Clock Enable */
   __HAL_RCC_GPIOB_CLK_ENABLE();

   /*Configure GPIO pin Output Level */
   HAL_GPIO_WritePin(GPIOB, SEG_A_Pin|SEG_B_Pin|SEG_C_Pin|SEG_D_Pin
                           |SEG_E_Pin|SEG_F_Pin|SEG_G_Pin|SEG_H_Pin
                           |D0_Pin|D1_Pin|D2_Pin|D3_Pin, GPIO_PIN_RESET);

   /*Configure GPIO pins : SEG_A_Pin SEG_B_Pin SEG_C_Pin SEG_D_Pin
                           SEG_E_Pin SEG_F_Pin SEG_G_Pin SEG_H_Pin
                            D0_Pin D1_Pin D2_Pin D3_Pin*/
   GPIO_InitStruct.Pin = SEG_A_Pin|SEG_B_Pin|SEG_C_Pin|SEG_D_Pin
                        |SEG_E_Pin|SEG_F_Pin|SEG_G_Pin|SEG_H_Pin
                        |D0_Pin|D1_Pin|D2_Pin|D3_Pin;
   GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
   GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
   HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

}

// Надрукувати число на дисплеї
void LED7SegPrint(uint32_t incomingNumber)
{
 splitToDigit(incomingNumber, digit, NUM_DIGIT);
}

// Надрукувати числа в дисплей для кожного розряду окреме число
void LED7SegPrintSeparate(uint8_t* aDigit, uint8_t number_of_digits)
{
 for (uint8_t i = 0; i < number_of_digits; i++)
  {
   digit[i] = *(aDigit+i);
  }
}

// Запалювання потрібних сегментів та розрядів дисплею (динамічна індикація)
void LED7SegShow(void)
{
 for(uint8_t i = 0; i < sizeof(digit)/sizeof(digit[0]); i++)
 {
  for(uint8_t i = 0; i < NUM_DIGIT; i++)
  {
   digitPort[i] -> BRR = digitPin[i]; //Вимикаємо всі розряди
  }
  digitPort[i] -> BSRR = digitPin[i]; //Вмикаємо потрібний розряд індикатора
  digitToPort(*(digit+i));               //Виводимо цифру у потрібний розряд
  HAL_Delay(1);                          //Невеличка затримка. Хай цифра світиться якийсь час
 }
}

// Локальні, внутрішні функції

//Функція виставляє в порт потрібну цифру
void digitToPort (uint8_t digit)
{
 SEG_PORT -> ODR &= ~ALL_SEG; //Вимикаємо всі сегменти
 SEG_PORT -> ODR |= digitsp[digit]; //Запалюємо потрібні
}

// число з лічильника розбиває на окремі розряди. вхідні аргументи: значення лічильника, масив для розрядів, кількість розрядів.
void splitToDigit(uint32_t number, uint8_t* digit, uint8_t number_of_digits)
{
 uint32_t tmp = number;

 for (uint8_t i = 0; i < number_of_digits; i++)
 {
  *(digit+i) = tmp % 10;
  tmp = tmp / 10;
 }

}

/*--------------------------------------------------------------------------------*/
//  ENF OF FILE
/*--------------------------------------------------------------------------------*/

Як користуватись

За допомоги CubeMX

Створюєте новий проект в CubeMX, обираєте свій мікроконтролер, наприклад STM32F100RBT6, та позначаєте потрібні ніжки мікроконтролеру на вихід (GPIO_Output). Та не забуваймо дати назву ніжкам (Enter User Label) за принципом:

Сегменти:
  • Сегмент А дисплею - SEG_A,
  • Сегмент B дисплею - SEG_B,
  • Сегмент C дисплею - SEG_C,
  • Сегмент D дисплею - SEG_D,
  • Сегмент E дисплею - SEG_E,
  • Сегмент F дисплею - SEG_F,
  • Сегмент G дисплею - SEG_G,
  • Сегмент H (крапка) дисплею - SEG_H (за потреби);
Розряди:
  • Перший розряд - D0,
  • Другий розряд - D1,
  • Третій розряд - D2,
  • Четвертий розряд - D3;
Зробіть як на малюнку. Червоним позначено виводи до яких підключений дисплей. LED_BLUE, LED_GREEN та USER_BUTTON це периферія плати розробника STM32VLDiscovery. Нам знадобиться для прикладу ще й USER_BUTTON. Налаштуйте ніжку до якої під'єднана кнопка на вхід:
Налаштування підключення дисплею в CubeMX
Генеруєте код для свого засобу розробки. До згенерованого проекту додаєте файли бібліотеки. LED7Seg.h до теки "INC", а LED7Seg.c до теки "SRC" проекту. Та додаєте до файлу main.c такі рядки:
/* USER CODE BEGIN Includes */
#include <stdbool.h> // для оперування змінними типу bool
#include <led7seg.h> // підключаємо бібліотеку 7-сегментного дисплею
/* USER CODE END Includes */
В головній функції main(), оголосимо змінні потрібні для прикладу:
  /* USER CODE BEGIN 1 */
 uint32_t counter = 0;   // ініціалізуємо змінну лічильника 
 bool flagButton = false;  // ініціалізуємо змінну прапорця кнопки
  /* USER CODE END 1 */
Надрукуємо початкове значення лічильника на дисплей. По суті, ми просто задали яке число має світитись на дисплеї.
  /* USER CODE BEGIN 2 */
  LED7SegPrint(counter); // надрукуємо початкове значення лічильника на дисплей
  /* USER CODE END 2 */
Після цього рядка ще нічого не з'явиться на дисплеї, бо за саме світіння потрібних сегментів відповідає інша функція - LED7SegShow().
В безкінечному циклі буде опитування кнопки і коли натиснуто кнопку до лічильника додамо одиницю:
 /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
   // як натиснули кнопку і прапорець кнопки скинуто
   if(HAL_GPIO_ReadPin(USER_BUTTON_GPIO_Port,USER_BUTTON_Pin) && !flagButton)
   {
    LED7SegPrint(++counter);  // додали одиницю до лічильника
    flagButton = true;  // встановили прапорець кнопки
   } else if(!HAL_GPIO_ReadPin(USER_BUTTON_GPIO_Port,USER_BUTTON_Pin) && flagButton){
    flagButton = false;    // коли кнопку відпустили - скинули прапорець кнопки
   }
   LED7SegShow();   // засвітили потрібні сегменти на дисплеї
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

  }
  /* USER CODE END 3 */
Також в безкінечному циклі постійно викликається функція LED7SegShow(), яка відповідає за динамічну індикацію дисплею.
Якщо потрібно на дисплей вивести якісь специфічні символи, то тоді порядок такий:
  • якщо цього символу немає в бібліотеці (початково в бібліотеці "0-9", "-" і "пусто" (всі сегменти відключено), то треба його створити. Про це далі;
  • оголосити масив uint8_t розміром на кількість задіяних сегментів;
  • заповнити масив "кодами" символів;
  • вивести це на дисплей.
Наприклад, зобразимо на дисплеї "-, NOP, NOP, -", де "NOP" - це нічого, жоден сегмент не світиться.
Оголошуємо в програмі масив:
  uint8_t digit[4] = {}; // Масив для символів в кожен розряд дисплею окремо
Заповнюємо масив "кодами" символів:
digit[0] = MINUS; // -
digit[1] = NOP;   // NOP
digit[2] = NOP;   // NOP
digit[3] = MINUS; // -
Та друкуємо це на дисплей викликавши функцію LED7SegPrintSeparate з параметрами: digit - ім'я масиву та кількість сегментів дисплею:
LED7SegPrintSeparate(digit,4);
Звісно що світінням потрібних сегментів займеться функція LED7SegShow().

Як додати користувацькі символи

Розглянемо це на прикладі. Додамо користувацький символ - літеру "А".
До файлу LED7Seg.h додати такі рядки:
#define LITA ( SEG_F_Pin | SEG_E_Pin | SEG_A_Pin | SEG_B_Pin | SEG_C_Pin | SEG_G_Pin )  // з яких сегментів складається літера "А"
#define A 12    // код літери "А", або порядковий номер у масиві (код 10 та 11 зайнятий NOP і MINUS) 
А до файлу LED7Seg.c внесемо такі зміни. До масиву digitsp[] додамо ім'я створеного символу, що назначили у заголовному файлі:
//оголошуємо масив з можливими варіантами символами на індикатор
static const uint16_t digitsp[]={DIG0,DIG1,DIG2,DIG3,DIG4,DIG5,DIG6,DIG7,DIG8,DIG9,~ALL_SEG,SEG_G_Pin,LITA};
Це і все. Тепер можна наприклад вивести таку інформацію на дисплей, наприклад, "А2-4":
uint8_t digit[4] = {};
digit[0] = 4;          // 4
digit[1] = MINUS;      // -
digit[2] = 2;          // 2
digit[3] = A;          // A
LED7SegPrintSeparate(digit,4);

Без CubeMX

Якщо ви не використовуєте CubeMX, або не зазначили там ніжки, до яких підключено дисплей, то потрібно позначити їх в заголовному файлі:
// якщо не визначили ніжки і порти до яких під'єднано дисплей у файлі main.h за допомоги CUBEMX,
// то треба ніжки і порти позначити тут відповідно до електричної схеми підключення дисплею
#if !defined SEG_A_Pin
 #define SEG_A_Pin        GPIO_PIN_0
 #define SEG_A_GPIO_Port  GPIOB
#endif

#if !defined SEG_B_Pin
 #define SEG_B_Pin        GPIO_PIN_1
 #define SEG_B_GPIO_Port  GPIOB
#endif

#if !defined SEG_C_Pin
 #define SEG_C_Pin        GPIO_PIN_2
 #define SEG_C_GPIO_Port  GPIOB
#endif

#if !defined SEG_D_Pin
 #define SEG_D_Pin        GPIO_PIN_3
 #define SEG_D_GPIO_Port  GPIOB
#endif

#if !defined SEG_E_Pin
 #define SEG_E_Pin        GPIO_PIN_4
 #define SEG_E_GPIO_Port  GPIOB
#endif

#if !defined SEG_F_Pin
 #define SEG_F_Pin        GPIO_PIN_5
 #define SEG_F_GPIO_Port  GPIOB
#endif

#if !defined SEG_G_Pin
 #define SEG_G_Pin        GPIO_PIN_6
 #define SEG_G_GPIO_Port  GPIOB
#endif

#if !defined SEG_H_Pin
 #define SEG_H_Pin        0u
 #define SEG_H_GPIO_Port  0u
#endif

#if !defined D0_Pin
 #define D0_Pin         GPIO_PIN_8
 #define D0_GPIO_Port   GPIOB
#endif

#if !defined D1_Pin
 #define D1_Pin         GPIO_PIN_9
 #define D1_GPIO_Port   GPIOB
#endif

#if !defined D2_Pin
 #define D2_Pin         GPIO_PIN_10
 #define D2_GPIO_Port   GPIOB
#endif

#if !defined D3_Pin
 #define D3_Pin         GPIO_PIN_11
 #define D3_GPIO_Port   GPIOB
#endif

#if !defined D4_Pin
 #define D4_Pin         0u
 #define D4_GPIO_Port   0u
#endif

#if !defined D5_Pin
 #define D5_Pin         0u
 #define D5_GPIO_Port   0u
#endif

#if !defined D6_Pin
 #define D6_Pin         0u
 #define D6_GPIO_Port   0u
#endif

#if !defined D7_Pin
 #define D7_Pin         0u
 #define D7_GPIO_Port   0u
#endif

#if !defined D8_Pin
 #define D8_Pin         0u
 #define D8_GPIO_Port   0u
#endif
Розрядам і сегментам які не будуть задіяні потрібно назначити значення 0u.
А до функції LED7SegInit() у файлі LED7Seg.c внести поправки, щоб порти і виводи відповідали електричній схемі, на який порт і виводи підключений дисплей. Наприклад якщо всі ніжки дисплею під'єднано до порту "B" мікроконтролера, то функція ініціалізації може виглядати так:
//Ініціалізація перифірії до якої підключено дисплей
void LED7SegInit(void)
{
 GPIO_InitTypeDef GPIO_InitStruct;

   /* GPIO Ports Clock Enable */
   __HAL_RCC_GPIOB_CLK_ENABLE();

   /*Configure GPIO pin Output Level */
   HAL_GPIO_WritePin(GPIOB, SEG_A_Pin|SEG_B_Pin|SEG_C_Pin|SEG_D_Pin
                           |SEG_E_Pin|SEG_F_Pin|SEG_G_Pin|SEG_H_Pin
                           |D0_Pin|D1_Pin|D2_Pin|D3_Pin, GPIO_PIN_RESET);

   /*Configure GPIO pins : SEG_A_Pin SEG_B_Pin SEG_C_Pin SEG_D_Pin
                            SEG_E_Pin SEG_F_Pin SEG_G_Pin SEG_H_Pin
                            D0_Pin D1_Pin D2_Pin D3_Pin*/
   GPIO_InitStruct.Pin = SEG_A_Pin|SEG_B_Pin|SEG_C_Pin|SEG_D_Pin
                         |SEG_E_Pin|SEG_F_Pin|SEG_G_Pin|SEG_H_Pin
                         |D0_Pin|D1_Pin|D2_Pin|D3_Pin;
   GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
   GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
   HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

}
Та не забувайте викликати цю функцію на початку програми, щоб ініціалізувати периферію. Все інше так само.

Файли

Архів з бібліотекою - LED7Seg
Архів з проектом для - True Studio & CubeMX