[TH] Bare Metal Cortex-M Ep.4

บทความนี้กล่าวถึงการใช้งานพอร์ตสื่อสารอนุกรม UART ซึ่งนิยมใช้มานาน และสะดวกต่อการใช้งาน โดยใช้ขา PA9 และ PA10 ต่อเข้าเป็นขา Tx และ Rx ของไมโครคอนโทรลเลอร์ทั้ง cortex-M0/M3/M4 เชื่อมต่อกับขา Rx/Tx ของตัวแปลงระดับแรงดันสัญญาณสำหรับสื่อสารผ่านทางพอร์ต USB ซึ่งตัวอย่างโปรแกรมเป็นการประมวลผลในไมโครคอนโทรลเลอร์และนำออกผลลัพธ์ผ่านทางพอร์ตสื่อสารอนุกรมที่ใช้งานโปรแกรม moserial เป็นซอฟต์แวร์สื่อสารข้อมูลดังภาพที่ 1

ภาพที่ 1 ตัวอย่างผลลัพธ์จากการทำงานของโปรแกรมตัวอย่าง

การสื่อสารผ่านพอร์ตอนุกรม

การสื่อสารผ่านพอร์ตอนุกรมต้องกำหนดอัตราความเร็วในการสื่อสารที่ตรงกันทั้ง 2 ฝั่ง ซึ่ง STM32F030F4P6 เลือกใช้ความเร็ว 38400 bps (bits-per-second) ส่วน STM32F103C8 และ STM32F401cc เลือกใช้ 115200 bps พร้อมทั้งกำหนดจำนวนบิตของข้อมูลให้มีขนาด 8 บิต มีบิตสำหรับใช้เป็นบิตระบุการสิ้นสุด 1 บิต (stop bit) และไม่ใช้พาริตีในการตรวจสอบข้อมูลระหว่างการสื่อสาร โดยบทความนี้กล่าวถึงเฉพาะการสื่อสารด้วยวิธีโพลิง (Polling) ซึ่งในการทำงานจริงสามารถทำงานได้ 3 แบบ คือ

  1. Polling
  2. Interrupt
  3. DMA (Direct Memory Access)

การตั้งค่าพอร์ตอนุกรม

ขั้นตอนการตั้งค่าการสื่อสารโดยปกติจะถูกโปรแกรม STM32CubeMX สร้างรหัสโปรแกรมการตั้งค่าให้โดยอัติโนมัติ ซึ่งอยู่ในฟังก์ชัน MX_USART1_UART_Init( void ) และมีตัวแปรภายนอกประเภท UART_HandleTypeDef ชื่อ huart1 (หรือชื่ออ่านตามที่เลือกตั้งค่า) ดังนี้ ซึ่งในบทความเลือกการทำงานของพอร์ตสื่อสารอนุกรมเป็นแบบ MultiProcessor เนื่องจากใช้สายสื่อสารเพียง 2 เส้นสำหรับการนำออกข้อมูล หรือขา Tx และนำเข้าข้อมูลผ่านทางขา Rx

UART_HandleTypeDef huart1;

static void MX_USART1_UART_Init(void)
{
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_MultiProcessor_Init(&huart1, 0, UART_WAKEUPMETHOD_IDLELINE) != HAL_OK)
  {
    Error_Handler();
  }
}

ขั้นตอนของการตั้งค่าประกอบด้วย 2 ส่วน คือ

  1. กำหนดการตั้งค่าในตัวแปร โดยตั้งค่าต่อไปนี้
    1. Instance สำหรับระบุหมายเลขพอร์ตสื่อสาร โดยในตัวอย่างเลือกใช้ USART1 เนื่องจากเป็นพอร์ตเดียวที่ STM32F030F4P6 มีให้ใช้งาน และใน Core-M3/M4 ล้วนรองรับพอร์ตนี้เช่นกัน
    2. Init.BaudRate สำหรับกำหนดความเร็วในการสื่อสาร โดยในตัวอย่างกำหนดอัตราการสื่อสารเป็น 115200
    3. Init.WordLength กำหนดจำนวนบิตข้อมูล ในที่นี้กำหนดเป็น UART_WORDLENGTH_8B
    4. Init.StopBits กำหนดจำนวนบิตของบิตระบุการสิ้นสุด ซึ่งกำหนดเป็น 1 บิต หรือ UART_STOPBOTS_1
    5. Init.Parity กำหนดเป็นไม่ใช้พาริตีบิต หรือ UART_PARITY_NONE
    6. Init.Mode เลือกใช้โหมดการทำงานแบบส่งและรับ UART_MODE_TX_RX
    7. Init.HwFlowCtl สำหรับกำหนดให้มีการควบคุมการนำออกหรือนำเข้าข้อมูลด้วยฮาร์ดแวร์ ซึ่งเราไม่ต้องการใช้เพราะใช้สายสื่อสารเพียง 2 เส้น จึงกำหนดเป็น UART_HWCONTROL_NONE
    8. Init.OverSampling กำหนดจำนวนบิตการเกิด over sampling เป็น 16 บิต หรือ UART_OVERSAMPLING_16
  2. ทำการเริ่มใช้งานโดยใช้คำสั่งรูปแบบต่อไปนี้ และถ้าผลลัพธ์เป็น HAL_OK หมายถึงการตั้งค่าสำเร็จ แต่ถ้าไม่ใช้หมายถึงเกิดความผิดพลาดในการเริ่มต้นทำงานตามการตั้งค่าที่ระบุ

ผลของการตั้งค่า = HAL_MultiProcessor_Init( &ตัวแปรเก็บการตั้งค่า, 0, UART_WAKEUPMETHOD_IDLELINE)

คำสั่งสำหรับส่งข้อมูลออก

รูปแบบของคำสั่งนำออกข้อมูลผ่านพอร์ตสื่อสารอนุกรมมีรูปแบบการสั่งงานดังนี้

HAL_UART_Transmit( &พอร์ตสื่อสาร, บัฟเฟอร์, ขนาดของบัฟเฟอร์, ค่าหน่วงเวลาในหน่วยมิลลิวินาที)

คำสั่งสำหรับรับข้อมูลจากพอร์ตสื่อสารอนุกรม

รูปแบบของคำสั่งนำเข้าข้อมูลจากพอร์ตสื่อสารอนุกรมเป็นดังนี้

HAL_UART_Receive( &พอร์ตสื่อสาร, บัฟเฟอร์, ขนาดของบัฟเฟอร์, ค่าหน่วงเวลา )

คำสั่งสำหรับการรับข้อมูลแบบการขัดจังหวะ

การรับข้อมูลจากพอร์ตสื่อสารอนุกรมแบบใช้การขัดจังหวะเป็นส่วนของโปรแกรมทำงานเมื่อมีการรับ/ส่งข้อมูลผ่านทางพอร์ตสื่อสารจะต้องเตรียมการดังนี้

  1. ตัวแปรสำหรับเก็บข้อมูล
  2. ฟังก์ชันสำหรับทำงานเบื้องหลัง

จากขั้นตอนทั้ง 2 สามารถเขียนเป็นได้ดังตัวอย่างดังนี้

uint8_t ตัวแปรบัฟเฟอร์[ขนาดของบัฟเฟอร์] = {0};

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
   // สิ่งที่ทำเมื่อเกิดข้อมูลเข้ามาในพอร์ตสื่อสาร
}

void main() {
//...
   HAL_UART_Receive_IT (&พอร์ตสื่อสาร, ตัวแปรบัฟเฟอร์, ขนาดของบัฟเฟอร์);
//...
}

รูปแบบของคำสั่งเป็นดังนี้

HAL_UART_Receive_IT( ตำแหน่งพอร์ตสื่อสาร, ตัวแปรบัฟเฟอร์, ขนาดของบัฟเฟอร์ )

ตัวอย่างโปรแกรม

ตัวอย่างโปรแกรมในบทความนี้เป็นนำออกข้อความไปยังพอร์ตสื่อสารอนุกรม โดยตัวอย่างมีการแปลงข้อมูลประเภทตัวเลขให้เป็นข้อความด้วยคำสั่ง sprintf( ) ทั้งนี้เนื่องจากไม่สามารถใช้คำสั่ง printf( ) ได้เหมือนการสั่ง print()/println() ใน Arduino

Cortex-M0

เริ่มต้นด้วยการสร้างโครงงานดังภาพที่ 2 และกำหนดขา PA10 และ PA9 เป็นขาสื่อสารอนุกรมโดยใช้โหมดสื่อสารเป็น Multiprocessor Communication ดังภาพที่ 3

ภาพที่ 2 สร้างโครงงานและกำหนดขาสัญญาณนาฒิกา
ภาพที่ 3 ตั้งค่าขาสำหรับการสื่อสาร UART1

หลังจากนั้นปรับค่าสัญญาณนาฬิการดังภาพที่ 4 และเขียนโปรแกรมใน main.c ดังภาพที่ 5 เพื่อนำออกค่าที่เก็บในตัวแปร value หลังจากนั้นคอมไพล์จะพบว่าการใช้คำสั่งเกี่ยวกับการสื่อสารอนุกรมร่วมกับการแปลงสตริงทำให้หน่วยความจำของไมโครคอนโทรลเลอร์ถูกใช้ไปกว่า 45% ดังภาพที่ 6

ภาพที่ 4 ตั้งค่าสัญญาณนาฬิกา
ภาพที่ 5 ตัวอย่างการแสดงข้อความ
ภาพที่ 6 การใช้หน่วยความจำของโปรแกรมในชิพ STM32F030F4P6

ผลของการทำงานเป็นดังภาพที่ 7

ภาพที่ 7 ตัวอย่างผลลัพธ์ของการแสดงผลจากตัวอย่างโปรแกรม

ให้ปรับเปลี่ยนโค้ดโปรแกรมใน main.c เป็นดังนี้เพื่อทำการคำนวณค่า prime number และรายงานผลจำนวนตัวเลขที่พบพร้อมกับค่าเวลาที่เสียไป ดังภาพที่ 8

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * <h2><center>&copy; Copyright (c) 2021 STMicroelectronics.
  * All rights reserved.</center></h2>
  *
  * This software component is licensed by ST under BSD 3-Clause license,
  * the "License"; You may not use this file except in compliance with the
  * License. You may obtain a copy of the License at:
  *                        opensource.org/licenses/BSD-3-Clause
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <math.h>
#include <stdio.h>
#include <string.h>

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/
UART_HandleTypeDef huart1;

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART1_UART_Init(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
int counter = 0;
uint32_t t0, t1;

uint16_t isPrimeNumber(uint16_t x) {
  uint16_t i;
  for (i = 2; i < x; i++) {
    if (x % i == 0) {
      return 0;
    }
  }
  if (i == x)
    return 1;
  return 0;
}


void testPrimeNumber(uint32_t maxN) {
  counter = 0;
  t0 = HAL_GetTick();
  for (uint32_t n = 2; n < maxN; n++) {
    if (isPrimeNumber(n)) {
      counter++;
    }
  }
  t1 = HAL_GetTick();
}

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */

  testPrimeNumber(2000);


  char msg[32];
  int msgLen=0;
  sprintf(msg,"Found %d.\n",counter);
  msgLen = strlen( msg );
  HAL_UART_Transmit( &huart1, (uint8_t*)msg, msgLen, 100);
  sprintf(msg,"Done in %ld ms.\n",(t1-t0));
  msgLen = strlen( msg );
  HAL_UART_Transmit( &huart1, (uint8_t*)msg, msgLen, 100);


  /* USER CODE END 2 */

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

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
  RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL6;
  RCC_OscInitStruct.PLL.PREDIV = RCC_PREDIV_DIV1;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }
  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_1) != HAL_OK)
  {
    Error_Handler();
  }
  PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1;
  PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK1;
  if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
  {
    Error_Handler();
  }
}

/**
  * @brief USART1 Initialization Function
  * @param None
  * @retval None
  */
static void MX_USART1_UART_Init(void)
{

  /* USER CODE BEGIN USART1_Init 0 */

  /* USER CODE END USART1_Init 0 */

  /* USER CODE BEGIN USART1_Init 1 */

  /* USER CODE END USART1_Init 1 */
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 38400;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  huart1.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;
  huart1.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
  if (HAL_MultiProcessor_Init(&huart1, 0, UART_WAKEUPMETHOD_IDLELINE) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN USART1_Init 2 */

  /* USER CODE END USART1_Init 2 */

}

/**
  * @brief GPIO Initialization Function
  * @param None
  * @retval None
  */
static void MX_GPIO_Init(void)
{

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOF_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();

}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/
ภาพที่ 8 ผลลัพธ์ของการหา Prime Number ด้วยชิพ STM32F030F4P6

Cortex-M3

เมื่อนำตัวอย่าง Prime Number มาใช้กับชิพ STM32F103C8 ให้เริ่มสร้างโครงงานใหม่ดังภาพที่ 9 และตั้งค่าขาตามภาพที่ 10 พร้อมทั้งกำหนดการทำงานของสัญญาณนาฬิกาตามภาพที่ 11

ภาพที่ 9 สร้างโครงงานสำหรับ Cortex-M3
ภาพที่ 10 การตั้งค่าขาของ STM32F103C8
ภาพที่ 11 การตั้งค่าสัญญาณนาฬิการของ STM32F103C8

เมื่อคอมไพล์โปรแกรมจะพบว่าการใช้หน่วยความจำน้อยกว่าทั้งนี้เพราะ STM32F103C8 มีปริมาณรอมและแรมมากกว่าดังภาพที่ 12

ภาพที่ 12 การใช้หน่วยความจำของ STM32F103C8

รหัสของโปรแกรมเป็นดังนี้

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * <h2><center>&copy; Copyright (c) 2021 STMicroelectronics.
  * All rights reserved.</center></h2>
  *
  * This software component is licensed by ST under BSD 3-Clause license,
  * the "License"; You may not use this file except in compliance with the
  * License. You may obtain a copy of the License at:
  *                        opensource.org/licenses/BSD-3-Clause
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <math.h>
#include <stdio.h>
#include <string.h>
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/
UART_HandleTypeDef huart1;

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART1_UART_Init(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
int counter = 0;
uint32_t t0, t1;

uint16_t isPrimeNumber(uint16_t x) {
  uint16_t i;
  for (i = 2; i < x; i++) {
    if (x % i == 0) {
      return 0;
    }
  }
  if (i == x)
    return 1;
  return 0;
}


void testPrimeNumber(uint32_t maxN) {
  counter = 0;
  t0 = HAL_GetTick();
  for (uint32_t n = 2; n < maxN; n++) {
    if (isPrimeNumber(n)) {
      counter++;
    }
  }
  t1 = HAL_GetTick();
}
/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */

  testPrimeNumber(2000);
 
  char msg[32];
  int msgLen=0;
  sprintf(msg,"Found %d.\n",counter);
  msgLen = strlen( msg );
  HAL_UART_Transmit( &huart1, (uint8_t*)msg, msgLen, 100);
  sprintf(msg,"Done in %ld ms.\n",(t1-t0));
  msgLen = strlen( msg );
  HAL_UART_Transmit( &huart1, (uint8_t*)msg, msgLen, 100);

  /* USER CODE END 2 */

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

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }
  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

/**
  * @brief USART1 Initialization Function
  * @param None
  * @retval None
  */
static void MX_USART1_UART_Init(void)
{

  /* USER CODE BEGIN USART1_Init 0 */

  /* USER CODE END USART1_Init 0 */

  /* USER CODE BEGIN USART1_Init 1 */

  /* USER CODE END USART1_Init 1 */
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_MultiProcessor_Init(&huart1, 0, UART_WAKEUPMETHOD_IDLELINE) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN USART1_Init 2 */

  /* USER CODE END USART1_Init 2 */

}

/**
  * @brief GPIO Initialization Function
  * @param None
  * @retval None
  */
static void MX_GPIO_Init(void)
{

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOD_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();

}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/

ตัวอย่างผลลัพธ์ของการทำงานเป็นดังภาพที่ 1 ซึ่งจะพบว่าความเร็วในการทำงานของ STM32F103C8 นั้นสูงกว่า STM32F030F4P6 มาก และราคาสูงกว่าด้วยเช่นกัน

ตัวอย่างการรับค่า

ส่วนหลักของตัวอย่างการรับค่าเป็นดังนี้

int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
  char msg[32];
  int msgLen=0;

  sprintf(msg,"...Say something...\n");
  msgLen = strlen( msg );
  HAL_UART_Transmit( &huart1, (uint8_t*)msg, msgLen, 100);

  strcpy(msg,"");
  HAL_UART_Receive(  &huart1, (uint8_t*)msg, 32, 5000 );
  for (int i=0; i<32; i++) {
	  if (msg[i] == '\n') {
		  msg[i] = 0;
		  break;
	  }
	  if (msg[i] == '\r') {
		  msg[i] = 0;
		  break;
	  }
  }
  msgLen = strlen( msg );
  HAL_UART_Transmit( &huart1, (uint8_t*)msg, msgLen, 100);
  HAL_UART_Transmit( &huart1, (uint8_t*)"\nEnd of program.\n", 17, 100);
  /* USER CODE END 2 */

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

จากตัวอย่างด้านบนเมื่อคอมไพล์และอัพเข้าชิพเมื่อทดสอบโปรแกรมจะเป็นดังภาพที่ 13

ภาพที่ 13 ตัวอย่างการรับข้อมูลจากพอร์ตสื่อสารอนุกรม

จากภาพที่ 13 จะพบว่า ในรอบแรกที่โปรแกรมทำงานได้รอเป็นเวลา 5 วินาที เพราะกำหนดค่า 5000 ไว้ในคำสั่ง HAL_UART_Receive( ) แต่ผู้ใช้ไม่ได้นำเข้าข้อมูลการแสดงผลจึงเป็นบรรทัดว่างและรายงาน End of program ส่วนการทำงานในรอบที่ 2 ได้กรอก Hello หลังจากนั้นเมื่อหมดเวลา 5 วินาทีจะรายงานคำว่า Hello ให้เห็นและจบโปรแกรม

ตัวอย่างโปรแกรมการรับข้อมูลจากการขัดจังหวะ

ตัวอย่างโปรแกรมการรับข้อมูลแบบใช้การขัดจังหวะช่วยการนำเข้าข้อมูลมีข้อดีคือสามารถรับข้อมูลได้โดยไม่ต้องรอให้หมดช่วงเวลาที่ระบุไว้หเหมือนในการทำโพลิง แต่การออกแบบโปรแกรมจะซับซ้อนขึ้นเนื่องจากมีส่วนของโปรแกรมทำงานเป็นเบื้องหลัง ซึ่งโค้ดของโปรแกรมเป็นดังนี้ (ตัวอย่างนี้ใช้ STM32F103C8) ซึ่งฟังก์ชัน HAL_UART_RxCpltCallback( ) จะเริ่มต้นเมื่อมีการเรียก HAL_UART_Receive_IT( ) ในฟังก์ชัน main( ) หลังจากนั้นแม้เข้าสู่การวนรอบไม่รู้จบใน while(1) โปรแกรมยังทำงานตอบสนองจากการรับค่าจากพอร์ตสื่อสารตลอดไป

#include "main.h"

UART_HandleTypeDef huart1;

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART1_UART_Init(void);

uint8_t uartBuffer[32] = {0};

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    HAL_UART_Transmit(&huart1, uartBuffer, 32, 100);
    HAL_UART_Receive_IT(&huart1, uartBuffer, 32);
}

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();

  HAL_UART_Receive_IT (&huart1, uartBuffer, 32);

  while (1)
  {
  }
}

void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

static void MX_USART1_UART_Init(void)
{
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_MultiProcessor_Init(&huart1, 0, UART_WAKEUPMETHOD_IDLELINE) != HAL_OK)
  {
    Error_Handler();
  }
}

static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOC_CLK_ENABLE();
  __HAL_RCC_GPIOD_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);

  /*Configure GPIO pin : PA8 */
  GPIO_InitStruct.Pin = GPIO_PIN_8;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

}

void Error_Handler(void)
{
  __disable_irq();
  while (1)
  {
  }
}


จากตัวอย่างโปรแกรมจะพบว่าเมื่อรันจะไม่มีข้อมูลใดแสดงเพราะในฟังก์ชันตอนสนองการขัดจังหวะที่เขียนนั้นเหมือนการทำ echo คือ เมื่อมีข้อมูลเข้าให้ส่งออกและรอรับรอบถัดไป จึงได้ผลลัพธ์ดังภาพที่ 14 เมื่อส่ง https://www.jarutex.com จะแสดงข้อความ https://www.jarutex.com และรอรับข้อมูลถัดไป พร้อมทั้งวนทำซ้ำอย่างนี้จนกว่าจะตัดการเชื่อมต่อ

กรณีที่ต้องการทำความเข้าใจให้มากขึ้นให้ผู้อ่านลองตัดบรรทัดเรียกใช้ HAL_UART_Receive_IT() ในฟังก์ชัน HAL_UART_RxCpltCallback( ) แล้วเปรียบผลการทำงานระหว่างมีกับไม่มีการเรียกใช้คำสั่ง HAL_UART_Receive_IT()

ภาพที่ 14 ตัวอย่างการทำงานแบบการขัดจังหวะ

การขัดจังหวะกับ STM32F030F4P6

กรณีที่ใช้กับ STM32F030F4P6 โดยกำหนดการใช้งานขาดังภาพที่ 15 พร่อมทั้งเลือกโหมดเป็น Async. ดังภาพที่ 16 หลังจากนั้นเลือก NVIC ให้ทำงานดังภาพที่ 17

ภาพที่ 15 การกำหนดขาทำงานของ STM32F030F4P6
ภาพที่ 16 การเลือกโหมดทำงานเป็น Asynchronous

ภาพที่ 17 กำหนดให้ NVIC ทำงาน

โค้ดตัวอย่างการรับค่าตัวอักษร 1 ตัวเป็นดังนี้

#include "main.h"
#include <string.h>
#include <stdio.h>

UART_HandleTypeDef huart1;
uint8_t uartBuffer[32] = {0};
char msg[32];
int msgLen=0;

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART1_UART_Init(void);

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    HAL_UART_Transmit(&huart1, uartBuffer, strlen(( char *)uartBuffer), 100);
    HAL_UART_Receive_IT(&huart1, uartBuffer, 1);
}

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();

  sprintf(msg,"...Say something...\n");
  msgLen = strlen( msg );
  HAL_UART_Transmit( &huart1, (uint8_t*)msg, msgLen, 100);
  HAL_UART_Receive_IT (&huart1, uartBuffer, 1);
  while (1)
  {
  }
}

void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
  RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};

  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL6;
  RCC_OscInitStruct.PLL.PREDIV = RCC_PREDIV_DIV1;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_1) != HAL_OK)
  {
    Error_Handler();
  }
  PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1;
  PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK1;
  if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
  {
    Error_Handler();
  }
}

static void MX_USART1_UART_Init(void)
{
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 38400;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  huart1.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;
  huart1.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
  if (HAL_UART_Init(&huart1) != HAL_OK)
  {
    Error_Handler();
  }
}

static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  __HAL_RCC_GPIOF_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();

  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);

  GPIO_InitStruct.Pin = GPIO_PIN_5;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

}

void Error_Handler(void)
{
  __disable_irq();
  while (1)
  {
  }
}

สรุป

จากบทความนี้จะพบว่าการนำออกข้อมูลเพื่อสื่อสารกับอุปกรณ์อื่นผ่านทางพอร์ตอนุกรมเป็นวิธีการที่ไม่ยุ่งยากแต่ผู้ออกแบบระบบต้องเพิ่มวงจรแปลงแรงดันการสื่อสารเพิ่มเข้ามา โดยในตัวอย่างนี้ใช้การสื่อสารกับคอมพิวเตอร์จึงต้องอาศัยวงจร USB-to-RS232 เพื่อสื่อสารกับคอมพิวเตอร์แบบ RS232 ผ่านทางพอร์ต USB และวิธีการเขียนโปรแกรมเป็นแบบ Polling เมื่อพิจารณาจากตัวอย่างโปรแกรมรับค่าจะพบว่า ต้องรอให้ครบตามเวลาที่กำหนดไว้จึงจะแสดงข้อมูลที่นำเข้าไป ซึ่งไม่สะดวกนัก แต่เมื่อใช้การรับข้อมูลแบบการขัดจังหวะ ผู้เขียนโปรแกรมสามารถอ่านบัฟเฟอร์ได้ตลอดเวลาโดยไม่ต้องขอการรับค่าในแบบโพลิง สุดท้ายขอให้สนุกกับการเขียนโปรแกรมครับ

(C) 2020-2021, โดย อ.ดนัย เจษฎาฐิติกุล/อ.จารุต บุศราทิจ
ปรับปรุงเมื่อ 2021-07-24, 2021-07-25, 2021-08-13