[TH] Bare Metal Cortex-M Ep.5

บทความนี้กล่าวถึงการใช้งาน ADC (Analog to Digital Converter) ของ STM32 ที่มีความละเอียด 12 บิต ทำให้สามารถอธิบายค่าแรงดันแอนาล็อกได้ 4096 ระดับ (0 ถึง 4095) ของแรงดัน 0 ถึง 3V3 ซึ่งสามารถปรับระดับความละเอียดให้เป็น 12บิต 10 บิต 8 บิต หรือ 6 บิตได้เพื่อแลกกับความเร็วในการทำงาน โดยใช้ชุดคำสั่ง HAL ที่มากับชุดพัฒนาของ STM32CubeIDE/STM32CubeMX และในการทดลองได้เลือกใช้อุปกรณ์ดังภาพที่ 1 ซึ่งเป็นบอร์ดต้นแบบของ dCoreM0 ของทางทีมงานเรา

ภาพที่ 1 บอร์ดต้นแบบ dCoreM0 สำหรับทดลอง ADC

การแปลงสัญญาณแอนาล็อกเป็นดิจิทัล

การใช้วงจรแปลงสัญญาณแอนาล็อกเป็นดิจิทัลต้องกำหนดหน้าที่ของขาให้เป็น ADC_INx โดย x เป็นหมายเลขช่องสัญญาณหรือแชนเนล (Channel) ซึ่งในบทความนี้กำหนดให้ PA0 ทำงานเป็น ADC ช่อง 0 จึงกำหนดเป็น ADC_IN0 ดังภาพที่ 2

ภาพที่ 2 การตั้งค่าขาของ STM32F030F4P6

เมื่อสังเกตการตั้งค่าในส่วนของ ADC Mode and Configuration ดังภาพที่ 3 จะพบว่า IN0 ถูกทำเครื่องหมายถูกไว้ด้านหน้า และรายการ Analog มีเครื่องหมายถูกที่หน้าคำ ADC

ภาพที่ 3 หน้าจอ ADC Mode and Configuration

การกำหนดพารามิเตอร์การทำงานของ ADC เป็นดังภาพที่ 4 จะพบว่าสามารถปรับค่าแซมปลิง (Sampling) หรือความถี่ของการแปลงสัญญาณ (Sampling Time) ซึ่งกำหนดไว้ 1.5cycles หมายถึงทำการแปลงสัญญาณทุก 1.5 ลูกคลื่น และสามารถปรับเปลี่ยนได้หลายค่าดังที่แสดงในภาพที่ 5

ภาพที่ 4 หน้าจอ Parameter Settings
ภาพที่ 5 ค่าแซมปลิงไทม์ของ ADC

ค่า Sampling Time สามารถนำมาคำนวณเป็นค่าเวลาที่ถูกใช้ในการแปลงสัญญาณได้ดังสมการต่อไปนี้

tconv = (SamplingTime + 12.5) x ADCclockCycles

หัลงจากนั้นทำการตั้งค่าสัญญาณนาฬิกาดังภาพที่ 6

ภาพที่ 6 การตั้งค่าความถี่สัญญาณนาฬิกา

คำสั่ง

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

  1. ส่วนของการตั้งค่าที่อยู่ในฟังก์ชัน MX_ADC_Init() ซึ่งประกอบด้วย
    1. สร้างตัวแปรสำหรับเก็บการตั้งค่าทำงานเป็นตัวแปรประเภท ADC_ChannelConfTypeDef
    2. กำหนด Instance เป็น ADC1
    3. กำหนด Init.ClockPrescalar ตามที่กำหนดในภาพที่ 6 ซึ่งเป็นการหาร 1
    4. กำหนด Init.DataAlign เป็นการเรียงบิตข้อมูลจากขวาไปซ้าย
    5. กำหนด Init.ScanConvMode เป็นแบบ ADC_SCAN_DIRECTION_FORWARD
    6. กำหนด Init.EOCSelection เป็น ADC_EOC_SINGLE_CONV
    7. ยกเลิกการทำการรอเมื่ออยู่ในโหมด Low Power
    8. ยกเลิกการปิดการทำงานเมื่ออยู่ในโหมด Low Power
    9. ยกเลิกการหยุดการแปลงสัญญาณแบบต่อเนื่อง
    10. กำหนด Init.ExternalTrigConv เป็น ADC_SOFTWARE_START ซึ่งเป็นการกำหนดให้ ADC เริ่มทำงานเมื่อซอฟต์แวร์เริ่มทำงาน
    11. กำหนด Init.ExternalTrigConvEdge เป็น ADC_EXTERNALTRIGCONVEDGE_NONE เพราะไม่ได้ใช้งานโหมดการขัดจังหวะจึงไม่ต้องมีตัวกระตุ้นหรือเรียกฟังก์ชันตอบสนองเพื่อให้ ADC ทำงาน
    12. ยกเลิกการใช้งานในโหมด DMA
    13. กำหนด Init.Overrun เป็น ADC_OVR_DATA_PRESERVED
    14. สั่งให้เริ่มต้นทำงานตามที่กำหนดไว้ด้วยคำสั่ง HAL_ADC_Init( ตำแหน่งอ้างอิงADC )
    15. ถ้าเกิดข้อผิดพลาดจาก 14 ให้หยุดการทำงาน
    16. ตั้งค่าตัวแปรที่สร้างในข้อ 1 ดังนี้
      1. กำหนดช่องสัญญาณเป็น ADC_CHANNEL_0
      2. กำหนด Rank เป็น ADC_RANK_CHANNEL_NUMBER
      3. กำหนดค่า Sampling Time เป็น ADC_SAMPLINGTIME_1CYCLE_5
    17. เรียกให้ ADC ที่กำหนดในข้อ 13 ทำงานตามการตั้งค่าในขั้นตอนที่ 16 ด้วยคำสั่ง HAL_ADC_ConfigChannel( )
  2. ส่วนของการใช้งานที่เขียนในฟังก์ชัน main()

คำสั่งในส่วนของการทำงานประกอบด้วยการแคลิเบรตค่า การสั่งให้เริ่มทำงาน การกำหนดระยะเวลาในการอ่านค่าจากADC และการนำค่ามาจากภาค ADC มาใช้ ดังนี้

HAL_ADCEx_Calibration_Start( ค่าตำแหน่งของ ADC )

HAL_ADC_Start( ค่าตำแหน่งของ ADC)

HAL_ADC_PollForCConversion( ค่าตำแหน่งของ ADC, ค่าเวลาที่รอการทำงานอขง ADC ในหน่วยมิลลิวินาที )

ค่าที่อ่านได้ = HAL_ADCC_GetValue( ค่าตำแหน่งของ ADC )

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

ตัวอย่างโปรแกรมต่อไปนี้เป็นการอ่านค่าจากภาค ADC และทำการแปลงค่าตัวเลขให้เป็นข้อความและนำออกทางพอร์ตสื่อสารอนุกรมทุก 250 มิลลิวินาที ซึ่งได้ตัวอย่างผลลัพธ์ของการทำงานดังภาพที่ 7

#include "main.h"

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

ADC_HandleTypeDef hadc;
UART_HandleTypeDef huart1;

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

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

  char msg[32]="";
  uint8_t msgLen;
  uint16_t dValue=0;

  HAL_ADCEx_Calibration_Start(&hadc);

  while (1)
  {
		HAL_ADC_Start(&hadc);
		HAL_ADC_PollForConversion(&hadc, 1); // อ่าน 1ms
		dValue = HAL_ADC_GetValue(&hadc);
		sprintf(msg,"dValue = %d\n", dValue);
		msgLen = strlen( msg );
		HAL_UART_Transmit( &huart1, (uint8_t*)msg, msgLen, 100);
		HAL_Delay(250);
  }
}

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

  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI14|RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSI14State = RCC_HSI14_ON;
  RCC_OscInitStruct.HSI14CalibrationValue = 16;
  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_ADC_Init(void)
{
  ADC_ChannelConfTypeDef sConfig = {0};

  hadc.Instance = ADC1;
  hadc.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV1;
  hadc.Init.Resolution = ADC_RESOLUTION_12B;
  hadc.Init.DataAlign = ADC_DATAALIGN_RIGHT;
  hadc.Init.ScanConvMode = ADC_SCAN_DIRECTION_FORWARD;
  hadc.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
  hadc.Init.LowPowerAutoWait = DISABLE;
  hadc.Init.LowPowerAutoPowerOff = DISABLE;
  hadc.Init.ContinuousConvMode = DISABLE;
  hadc.Init.DiscontinuousConvMode = DISABLE;
  hadc.Init.ExternalTrigConv = ADC_SOFTWARE_START;
  hadc.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
  hadc.Init.DMAContinuousRequests = DISABLE;
  hadc.Init.Overrun = ADC_OVR_DATA_PRESERVED;
  if (HAL_ADC_Init(&hadc) != HAL_OK)
  {
    Error_Handler();
  }

  sConfig.Channel = ADC_CHANNEL_0;
  sConfig.Rank = ADC_RANK_CHANNEL_NUMBER;
  sConfig.SamplingTime = ADC_SAMPLETIME_1CYCLE_5;
  if (HAL_ADC_ConfigChannel(&hadc, &sConfig) != 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_MultiProcessor_Init(&huart1, 0, UART_WAKEUPMETHOD_IDLELINE) != HAL_OK)
  {
    Error_Handler();
  }
}

static void MX_GPIO_Init(void)
{
  __HAL_RCC_GPIOF_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();

}

void Error_Handler(void)
{
  __disable_irq();
  while (1)
  {
  }
}
ภาพที่ 7 ตัวอย่างผลลัพธ์ของการทำงาน

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

ด้านล่างนี้เป็นโค้ดในฟังก์ชัน main() ของตัวอย่างโปรแกรมการอ่านข้อมูล 10 ชุดและหาค่าเฉลี่ยของข้อมูลทั้ง 10 และมีการเก็บค่าน้อยสุดและมากที่สุดของค่าที่เคยเกิดขึ้นมาทั้งหมด โดยตัวอย่างผลลัพธ์ของการทำงานเป็นดังภาพที่ 8

int main(void)
{
  uint16_t dValue[10];
  uint8_t dIndex=0;
  uint32_t dSum = 0;
  uint32_t dAverage=0;
  uint16_t dMin=4096, dMax=0;
  char msg[32]="";
  uint8_t msgLen;

  HAL_Init();

  SystemClock_Config();

  MX_GPIO_Init();
  MX_ADC_Init();
  MX_USART1_UART_Init();
  HAL_ADCEx_Calibration_Start(&hadc);

  while (1)
  {
		HAL_ADC_Start(&hadc);
		HAL_ADC_PollForConversion(&hadc, 1); // อ่าน 1ms
		dValue[dIndex] = HAL_ADC_GetValue(&hadc);
		dSum += dValue[dIndex];
		if (dValue[dIndex]>dMax){
			dMax = dValue[dIndex];
		}
		if (dValue[dIndex]<dMin) {
			dMin = dValue[dIndex];
		}
		dIndex++;
		if (dIndex == 10) {
			dAverage = (uint32_t)(dSum/10);
			dSum = 0;
			dIndex = 0;
			sprintf(msg,"Min=%d, Max=%d, Average=%ld\n", dMin,dMax,dAverage);
			msgLen = strlen( msg );
			HAL_UART_Transmit( &huart1, (uint8_t*)msg, msgLen, 100);
		}
		HAL_Delay(5);
  }
}
ภาพที่ 8 ตัวอย่างผลลัพธ์ของตัวอย่างที่ 2

สรุป

จากบทความนี้จะพบว่า ADC ของ STM32 ทั้ง Cortex-M0/M3 และ M4 มีความละเอียด 12 บิตเท่ากัน และทำงานได้ 3 ลักษณะตือ Polling, Interrupt และ DMA โดยในบทความนี้กล่าวถึงการทำงานแบบ Polling จึงต้องกำหนดระยะเวลาการรอการอ่านค่าจาก ADC ก่อนดำเนินการอ่านค่ามาใช้งาน ซึ่งผู้อ่านน่าจะพบข้อสังเกตว่า การทำงานลักษณะนี้ต้องเสียเวลาการรอ ทั้งที่สามารถให้โมดูลทำงานแบบเบื้องหลังด้วยการขัดจังหวะเหมือนในบทความก่อนหน้านี้เรื่อง UART หรืออ่านค่าข้อมูลโดยตรงจากหน่วยความจำ หรือ DMA (Direct Memory Access) ซึ่งเป็นการเข้าถึงข้อมูลโดยไม่ต้องผ่านการทำงานกับ CPU ทำให้เข้าถึงได้เร็วมาก เป็นต้น สุดท้ายนี้ขอให้สนุกกับการเขียนโปรแกรมครับ

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