середа, 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 функцію для своїх потреб. Файли з кодом по мотивам відео - тут.


2 коментарі:

  1. Андрію. Не працює код, коли роблю менший бітрейт.

    Ставлю брекпоінт напроти : executeSerialCommand(rxString);
    Запускаю відлагодження, відправляю "on" і дивлюся що потрапило до rxString але там тільки "n" . Так само із "off" отримую в тільки "ff". Тобто перший символ кудись дівається, але він точно приходить, бо ехо відпрацьовує його назад в комп. Що б то могло бути?

    ВідповістиВидалити