บทความนี้กล่าวถึงการใช้งาน ADC (Analog to Digital Converter) ของ STM32 ที่มีความละเอียด 12 บิต ทำให้สามารถอธิบายค่าแรงดันแอนาล็อกได้ 4096 ระดับ (0 ถึง 4095) ของแรงดัน 0 ถึง 3V3 ซึ่งสามารถปรับระดับความละเอียดให้เป็น 12บิต 10 บิต 8 บิต หรือ 6 บิตได้เพื่อแลกกับความเร็วในการทำงาน โดยใช้ชุดคำสั่ง HAL ที่มากับชุดพัฒนาของ STM32CubeIDE/STM32CubeMX และในการทดลองได้เลือกใช้อุปกรณ์ดังภาพที่ 1 ซึ่งเป็นบอร์ดต้นแบบของ dCoreM0 ของทางทีมงานเรา
การแปลงสัญญาณแอนาล็อกเป็นดิจิทัล
การใช้วงจรแปลงสัญญาณแอนาล็อกเป็นดิจิทัลต้องกำหนดหน้าที่ของขาให้เป็น ADC_INx โดย x เป็นหมายเลขช่องสัญญาณหรือแชนเนล (Channel) ซึ่งในบทความนี้กำหนดให้ PA0 ทำงานเป็น ADC ช่อง 0 จึงกำหนดเป็น ADC_IN0 ดังภาพที่ 2
เมื่อสังเกตการตั้งค่าในส่วนของ ADC Mode and Configuration ดังภาพที่ 3 จะพบว่า IN0 ถูกทำเครื่องหมายถูกไว้ด้านหน้า และรายการ Analog มีเครื่องหมายถูกที่หน้าคำ ADC
การกำหนดพารามิเตอร์การทำงานของ ADC เป็นดังภาพที่ 4 จะพบว่าสามารถปรับค่าแซมปลิง (Sampling) หรือความถี่ของการแปลงสัญญาณ (Sampling Time) ซึ่งกำหนดไว้ 1.5cycles หมายถึงทำการแปลงสัญญาณทุก 1.5 ลูกคลื่น และสามารถปรับเปลี่ยนได้หลายค่าดังที่แสดงในภาพที่ 5
ค่า Sampling Time สามารถนำมาคำนวณเป็นค่าเวลาที่ถูกใช้ในการแปลงสัญญาณได้ดังสมการต่อไปนี้
tconv = (SamplingTime + 12.5) x ADCclockCycles
หัลงจากนั้นทำการตั้งค่าสัญญาณนาฬิกาดังภาพที่ 6
คำสั่ง
ขั้นตอนของการใช้ ADC ประกอบด้วย 2 ส่วน คือ
- ส่วนของการตั้งค่าที่อยู่ในฟังก์ชัน MX_ADC_Init() ซึ่งประกอบด้วย
- สร้างตัวแปรสำหรับเก็บการตั้งค่าทำงานเป็นตัวแปรประเภท ADC_ChannelConfTypeDef
- กำหนด Instance เป็น ADC1
- กำหนด Init.ClockPrescalar ตามที่กำหนดในภาพที่ 6 ซึ่งเป็นการหาร 1
- กำหนด Init.DataAlign เป็นการเรียงบิตข้อมูลจากขวาไปซ้าย
- กำหนด Init.ScanConvMode เป็นแบบ ADC_SCAN_DIRECTION_FORWARD
- กำหนด Init.EOCSelection เป็น ADC_EOC_SINGLE_CONV
- ยกเลิกการทำการรอเมื่ออยู่ในโหมด Low Power
- ยกเลิกการปิดการทำงานเมื่ออยู่ในโหมด Low Power
- ยกเลิกการหยุดการแปลงสัญญาณแบบต่อเนื่อง
- กำหนด Init.ExternalTrigConv เป็น ADC_SOFTWARE_START ซึ่งเป็นการกำหนดให้ ADC เริ่มทำงานเมื่อซอฟต์แวร์เริ่มทำงาน
- กำหนด Init.ExternalTrigConvEdge เป็น ADC_EXTERNALTRIGCONVEDGE_NONE เพราะไม่ได้ใช้งานโหมดการขัดจังหวะจึงไม่ต้องมีตัวกระตุ้นหรือเรียกฟังก์ชันตอบสนองเพื่อให้ ADC ทำงาน
- ยกเลิกการใช้งานในโหมด DMA
- กำหนด Init.Overrun เป็น ADC_OVR_DATA_PRESERVED
- สั่งให้เริ่มต้นทำงานตามที่กำหนดไว้ด้วยคำสั่ง HAL_ADC_Init( ตำแหน่งอ้างอิงADC )
- ถ้าเกิดข้อผิดพลาดจาก 14 ให้หยุดการทำงาน
- ตั้งค่าตัวแปรที่สร้างในข้อ 1 ดังนี้
- กำหนดช่องสัญญาณเป็น ADC_CHANNEL_0
- กำหนด Rank เป็น ADC_RANK_CHANNEL_NUMBER
- กำหนดค่า Sampling Time เป็น ADC_SAMPLINGTIME_1CYCLE_5
- เรียกให้ ADC ที่กำหนดในข้อ 13 ทำงานตามการตั้งค่าในขั้นตอนที่ 16 ด้วยคำสั่ง HAL_ADC_ConfigChannel( )
- ส่วนของการใช้งานที่เขียนในฟังก์ชัน 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)
{
}
}
ตัวอย่างโปรแกรม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);
}
}
สรุป
จากบทความนี้จะพบว่า 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