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

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


Передмова

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

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

Запускаємо програму STM32CubeMX, обираємо свою плату або чип, та починаємо налаштовувати периферію:
Вкладка PinOut
На вкладці "Pinout" оберіть, як на малюнку: "SYS" -> Debug "Serial Wire", та "USART2" -> Mode "Asynchronous". USART2 це той порт, який під'єднаний до програматора на платі NUCLEO. А програматор з'єднаний з комп'ютером USB шнурком і бачиться комп'ютером, як диск на 156 кБ (вірно для NUCLEO плат), та віртуальний COM порт (наприклад, в мене це COM6):
Диск та COM порт програматора
Це дозволить ознайомитись з роботою UART без додаткового обладнання та дротів. 

Вкладку "Clock Configuration" програми STM32CubeMX залишив без змін. Хай буде як є. Перейдемо до вкладки "Configuration", та в зоні "Connectivity" натиснемо на "USART2" перейдемо до налаштувань USART2:
Тиснемо на USART2
У вкладці "Parametr Setting" залишаємо як є (дивись малюнок):
Установка параметрів USART
  • 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":
Налаштування DMA
Тиснемо кнопку "Add", обираємо "Select" -> USART2_RX і повторюємо це саме для USART2_TX. А так як в нашому прикладі RX (прийом) будемо слухати завжди, то режим для прийому оберемо circular (круговий). Прийняли пакет даних - знову готові приймати.
Круговий режим
Все інше залишаємо як є. І можна генерувати проект для свого засобу розробки. В мене це Atolic True Studio, у вас це може бути інший засіб розробки. З STM32CubeMX це все.

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

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

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

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

Рішення цієї проблеми я вгледів тут. І ми скористаємось вже готовим рішенням і додамо до свого проекту. Але все по порядку. Вже маємо перед собою відкритий засіб розробки з "шаблоном" програми сформований програмою STM32CubeMX. Цей "шаблон" має певні ділянки для додавання коду сформованого STM32CubeMX, так і ділянки для коду який пише розробник. Будемо додержуватись цього правила. 

В засобі розробки відкриваємо файл main.h і додаємо рядок:
/* USER CODE BEGIN Private defines */
#define MAXCLISTRING          100 // Biggest string the user will type
/* USER CODE END Private defines */
Це максимальна довжина рядку який будемо приймати за один раз. Тут все.

Відкриваємо файл main.c і додаємо трішки зміних:
/* 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 */

Далі додаємо прототипи функцій на початку:
/* USER CODE BEGIN PFP */
/* Private function prototypes -----------------------------------------------*/
void print(char string[]);
void executeSerialCommand(uint8_t string[]);
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
/* USER CODE END PFP */

В другу секцію коду для розробника додаємо передачу одного рядку тексту для перевірки передачі по UART2, та ініціацію прийому по UART2:
/* USER CODE BEGIN 2 */
  char str[] = "\r\nTransmit UART DMA is OK!\r\n";
  HAL_UART_Transmit_DMA(&huart2, (uint8_t*) str, strlen(str));

  __HAL_UART_FLUSH_DRREGISTER(&huart2);
  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 */

}

І в секцію чотири для розробника додаємо функції, прототипи яких ми додали на початку програми:
/* USER CODE BEGIN 4 */
void print(char string[])
{
 HAL_UART_Transmit(&huart2, (uint8_t*) string, strlen(string), 10);
}

void executeSerialCommand(uint8_t string[])
{
 print("\r\nMyHomeIoT> ");

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

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
   __HAL_UART_FLUSH_DRREGISTER(&huart2); // Clear the buffer to prevent overrun

 int i = 0;

 print((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
     {
         print(" \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
             print("\r\nConsole> ");
         }
     }

}
/* USER CODE END 4 */

  • Функція print друкує текстові рядки до терміналу
  • Функція 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 вимкне світлодіод. Ось результат описаної роботи:

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