Timer และสัญญาณนาฬิกา
เป้าหมาย
หัวข้อที่มีชื่อว่า “เป้าหมาย”เมื่อจบบทเรียนนี้ คุณจะ
- คำนวณค่าตั้ง timer จากความถี่สัญญาณนาฬิกาและช่วงเวลาที่ต้องการได้
- เลือกระหว่าง timer ของฮาร์ดแวร์กับ software timer ของ RTOS ให้เหมาะกับงาน พร้อมเหตุผล
- อธิบายผลของการตั้งอัตราการทำงานของ task เบื้องหลังต่อภาระของระบบ
ใช้เวลาประมาณ 70 นาที (แนวคิด 15 · ฝึก 25 · แล็บ 25 · เช็ก 5)
ก่อนเริ่ม
หัวข้อที่มีชื่อว่า “ก่อนเริ่ม”ทวนจากบทก่อนหน้าสองข้อ
- ตัวกันเด้งในบทเรียน 4.1 อ่านปุ่มทุก 10 ms ใครเป็นคนกำหนดจังหวะ 10 ms นั้น ตัวกันเด้งเองหรือผู้เรียก
175000 * 65535ล้น 32 บิต แล้ว65536 * 1000000ล่ะ (บทเรียน 1.1)
ดูของจริงก่อน
หัวข้อที่มีชื่อว่า “ดูของจริงก่อน”เปิด examples/08_timer_math.c ตัวเลขทุกตัวในไฟล์นี้อ่านมาจากไฟล์ที่ Device Configurator สร้างไว้ใน BSP ของ SDK
ทายก่อนรัน ว่า GENERAL_PURPOSE_TIMER ที่ BSP ตั้งไว้จะ overflow ทุกกี่มิลลิวินาที
gcc -std=c11 -Wall -Wextra -o timer_math examples/08_timer_math.c./timer_mathได้ 1000 ms พอดี และบรรทัดถัด ๆ มาแสดงว่าสัญญาณนาฬิกา 100 MHz ตัวเดียวกันนี้ ผ่านตัวหารคนละค่า กลายเป็นจังหวะของ PWM, UART 115200, I2C 400 kHz และ SPI 25 MHz ของเรดาร์ ความถี่ของ UART ไม่ลงตัวพอดี คลาดไป -0.22% ซึ่งเป็นผลของการหารเลขจำนวนเต็ม
1. จากสัญญาณนาฬิกาถึงเวลา: สองสูตร
หัวข้อที่มีชื่อว่า “1. จากสัญญาณนาฬิกาถึงเวลา: สองสูตร”บน PSOC™ Edge E84 ในแม่แบบของ SDK อุปกรณ์กลุ่มหนึ่ง (TCPWM0, SCB2 ที่เป็น UART console, SCB3 ที่เป็น SPI ของเรดาร์ และอื่น ๆ) ใช้สัญญาณนาฬิกา CLK_HF10
ซึ่งตั้งไว้ที่ 100 MHz (cycfg_clocks.c บรรทัด 153-154
และตารางกลุ่มอุปกรณ์ใน pse84_config.h)
แต่ละอุปกรณ์มีตัวหารของตัวเอง ตั้งด้วย Cy_SysClk_PeriPclkSetDivider() ซึ่งเอกสารของ PDL บอกว่าค่า N “causes integer division of (divider value + 1)”
- ความถี่ที่ timer ได้: fcnt = fsrc / (N + 1)
- เวลาต่อหนึ่งรอบของ timer ที่นับขึ้นจาก 0 ถึง period: T = (period + 1) / fcnt
ตัวอย่างจริงใน BSP: GENERAL_PURPOSE_TIMER คือ TCPWM0 counter 2 ใช้ตัวหาร 16 บิตค่า 9999 จึงได้ 100 MHz / 10000 = 10 kHz
และตั้ง period = 9999 เปิด interrupt เมื่อถึง terminal count (cycfg_peripherals.c บรรทัด 1115-1125)
10000 จังหวะที่ 10 kHz คือ 1 วินาที ที่ commit นี้ยังไม่มีโค้ดของแม่แบบเรียกใช้ timer ตัวนี้ มันเป็นของที่ BSP เตรียมไว้ให้ ฟังก์ชันที่ใช้เริ่มมันอยู่ใน
cy_tcpwm_counter.h เช่น Cy_TCPWM_Counter_Init()
Cy_TCPWM_Counter_Enable() และ Cy_TCPWM_TriggerStart_Single()
ช่วงเวลาเดียวกันทำได้หลายคู่ของตัวหารกับ period ตัวหารเล็กให้ความละเอียดสูงกว่า (แต่ละจังหวะสั้นกว่า) แต่ period ต้องใหญ่พอ และความแม่นยำของทุกอย่างในสายนี้ ไม่ดีไปกว่าต้นทาง ถ้าต้นทางคลาด 1% ทุก timer คลาด 1% ตาม ตัวอย่างสุดขั้วคือ watchdog ที่ใช้ oscillator ภายในความแม่นยำต่ำ เอกสารของ PDL ระบุความคลาด ±30% (บทเรียน 4.3)
2. timer ของฮาร์ดแวร์ หรือ software timer ของ RTOS
หัวข้อที่มีชื่อว่า “2. timer ของฮาร์ดแวร์ หรือ software timer ของ RTOS”FreeRTOS ในแม่แบบตั้ง configTICK_RATE_HZ เป็น 1000 (FreeRTOSConfig.h ของ CM33 บรรทัด 66)
หนึ่ง tick คือ 1 ms ทุกอย่างที่อิง tick จึงละเอียดได้ไม่เกินหนึ่ง tick
| ต้องการ | ใช้ | เหตุผล |
|---|---|---|
| คลื่น PWM ที่ขา วัดความกว้างพัลส์ (capture) จังหวะระดับไมโครวินาที | timer ของฮาร์ดแวร์ (TCPWM) | ฮาร์ดแวร์นับเองโดยไม่ขึ้นกับภาระของ CPU แต่มีจำนวนจำกัด และงานใน ISR ต้องสั้น |
| งานเป็นระยะระดับมิลลิวินาทีขึ้นไปใน task | vTaskDelayUntil() ใน task ของงานนั้น |
task มี stack ของตัวเอง รอหรือเรียก API ที่บล็อกได้ |
| เรียกฟังก์ชันสั้น ๆ ครั้งเดียวหรือเป็นระยะโดยไม่อยากสร้าง task | software timer ของ FreeRTOS (xTimerCreate()) |
callback ทำงานใน timer service task (ตั้ง priority 3 ใน config ของแม่แบบ) ห้ามบล็อก เพราะ timer ทุกตัวแชร์ task เดียวกัน |
| งานเป็นระยะบนหน้าจอของ CM55 | lv_timer_create() ของ LVGL |
ตัวอย่างของ SDK ฝั่ง CM55 ใช้วิธีนี้ทุกตัว เพราะงานต้องอยู่ใน GFX task และห้ามบล็อก |
เรื่องที่พลาดบ่อยคือ vTaskDelay() กับ vTaskDelayUntil() เอกสารใน task.h ของ FreeRTOS อธิบายว่า vTaskDelay() “specifies a wake time relative
to the time at which the function is called” ส่วน xTaskDelayUntil() “specifies the absolute (exact) time at which it wishes to unblock”
(task.h ของ Infineon FreeRTOS release-v10.6.202)
ลูปที่ทำงาน 5 ms แล้ว vTaskDelay(100 ms) จึงวนทุก 105 ms ไม่ใช่ 100 ms และเลื่อนไปเรื่อย ๆ แม่แบบเปิด INCLUDE_vTaskDelayUntil ไว้แล้วในไฟล์ config
3. อัตราของ task เบื้องหลัง คือภาระของทั้งระบบ
หัวข้อที่มีชื่อว่า “3. อัตราของ task เบื้องหลัง คือภาระของทั้งระบบ”task ที่อ่านเซนเซอร์ทุก P มิลลิวินาที และแต่ละรอบใช้เวลา C มิลลิวินาที ครอบครองทั้ง CPU และบัส I2C เป็นสัดส่วนราว C/P ตัวอย่าง 05_auto_push_task.c ของ SDK เล่าเรื่องจริงของ task ที่อ่านเซนเซอร์ทุกตัวแล้วส่งไป CM55 (ค่าเริ่มต้นทุก 100 ms)
- ลูปหน่วงหลังอ่าน ไม่ใช่ระหว่างอ่าน ตัวอย่างวัดจำนวนรอบจริงเทียบกับที่ควรได้ แล้วเตือนว่าถ้าได้น้อยกว่า “the reads themselves are costing more than the interval”
- หน้าผาที่ 50 ms
sensor_auto_set_rate()รับ 20 ถึง 5000 ms แต่ต่ำกว่า 50 ms ลูป “reads ONLY the accelerometer” เพราะเซนเซอร์ตัวอื่นใช้เวลาบนบัสจนอัดลงงบ 20 ms ไม่ได้ ตั้ง 20 ms เพื่อให้ทุกอย่างเร็วขึ้น ผลคือเซนเซอร์ห้าจากหกตัวหยุดส่งเงียบ ๆ - ผู้อ่านสองรายบนบัสเดียว “Two readers on one bus at two rates is a bug waiting for a deadline; one publisher and many consumers is not.”
ถ้าค่าอายุ 100 ms ใช้ได้ ให้อ่านจาก cache ด้วย
sensor_auto_get_bmi270()ซึ่งไม่แตะบัสเลย
อัตรายังผูกกับความถูกต้องด้วย โมเดล IMU ของ Edge AI ถูกฝึกด้วยข้อมูล 50 Hz ตัวอย่าง 08_deepcraft_link เตือนว่าถ้าปล่อยให้ข้อมูลมาทุก 100 ms “every verdict is computed over five times the intended span of time and is confidently wrong” อัตราที่ผิดไม่ได้แค่เปลืองพลังงาน มันทำให้ผลผิด
ตัวอย่างสมบูรณ์
หัวข้อที่มีชื่อว่า “ตัวอย่างสมบูรณ์”examples/08_timer_math.c ทำงานเป็นสามท่า
- ท่าที่ 1
divided_hz()ใช้สูตรตัวหาร N + 1 ของ PDL - ท่าที่ 2 คำนวณรอบของ
GENERAL_PURPOSE_TIMERและPWM_LED_CTRLจากค่าใน BSP (สำหรับ PWM เราสมมติว่านับ 0..period แบบเดียวกับ counter ให้ตรวจกับคู่มือของชิป) - ท่าที่ 3 คำนวณ baud ของ UART ความถี่ SCL ของ I2C และ SCLK ของ SPI จากสัญญาณนาฬิกาเดียวกัน
ลองแก้แล้วทายก่อนรัน
- ถ้าต้องการให้
GENERAL_PURPOSE_TIMERเกิดทุก 250 ms โดยไม่เปลี่ยนตัวหาร period ต้องเป็นเท่าไร - ถ้าเปลี่ยนตัวหารของ UART จาก 86 เป็น 85 baud จริงและความคลาดเคลื่อนเป็นเท่าไร ตัวไหนใกล้ 115200 กว่า
- ทำไมโปรแกรมนี้ใช้
doubleได้ แต่ฟังก์ชันแบบเดียวกันในเฟิร์มแวร์ของ SDK มักเขียนด้วยเลขจำนวนเต็ม (คำใบ้:printfของ CM33 ในแม่แบบไม่มี float)
ฝึกเติม
หัวข้อที่มีชื่อว่า “ฝึกเติม”เปิด practice/08_timer_math.c มีช่องให้เติม 4 จุด เขียนด้วยเลขจำนวนเต็มล้วนแบบเฟิร์มแวร์
divided_hz()ตามสูตรตัวหารoverflow_us()ต้องใช้ตัวกลาง 64 บิตpick_timer()หาตัวหารที่เล็กที่สุดที่ได้ช่วงเวลาพอดีเป๊ะด้วย period 16 บิต และบอกตรง ๆ เมื่อทำไม่ได้baud_error_ppm()ความคลาดเคลื่อนเป็นส่วนในล้าน
gcc -std=c11 -Wall -Wextra -o timer_math practice/08_timer_math.c && ./timer_mathสังเกตว่า test ของ 1 วินาทีคาดหวังตัวหาร 1599 กับ period 62499 ไม่ใช่ 9999 กับ 9999 แบบใน BSP ทั้งสองคู่ได้ 1 วินาทีพอดี แต่กติกาของแบบฝึกคือเลือกตัวหารที่เล็กที่สุด ข้อกำหนดที่เขียนชัดทำให้คำตอบมีคำตอบเดียว
ลองเองก่อนอย่างน้อย 15 นาที แล้วเปิด solution/08_timer_math.c
จุดที่ควรเทียบคือทุกที่ที่คูณก่อนหารใช้ uint64_t หรือ int64_t และ pick_timer() ไม่แตะค่าที่ผู้เรียกส่งมาเมื่อหาคำตอบไม่ได้
ซึ่งเป็นหลักเดียวกับผลลัพธ์ที่บอกความจริงในบทเรียน 3.2
เช็กความเข้าใจ
หัวข้อที่มีชื่อว่า “เช็กความเข้าใจ”ตอบคำถาม 5 ข้อใน quiz.yaml (บนเว็บไซต์อยู่ท้ายหน้านี้) ครอบคลุมเป้าหมายทั้งสามข้อ ตอบถูกตั้งแต่ 4 ข้อขึ้นไปถือว่าจบบทเรียน
งาน: วัดอัตราจริงของ task เบื้องหลังบนบอร์ด แล้วอธิบายส่วนต่างระหว่างอัตราที่ตั้งกับอัตราที่ได้
- build ด้วย
make build -j ENABLE_PAGE_EXAMPLES=1 SDK_EXAMPLE_CM33=cm33/sensors/05_auto_push_taskแล้ว flash ถอดสายเสียบใหม่ - จาก serial console จดค่า
rateเริ่มต้น mask ของเซนเซอร์ และบรรทัดmeasured ... cycles in 1000 ms (expected about ...)ทั้งตอนอัตราเดิมและตอนที่ตัวอย่างปรับเป็น 25 ms - คำนวณจาก “expected” กับ “measured” ว่าแต่ละรอบใช้เวลาจริงกี่มิลลิวินาที แล้วประมาณเวลาที่การอ่านเซนเซอร์กินต่อรอบ (C) จากสูตร รอบจริง = C + ค่าหน่วง
- ตอบว่าตอนอัตรา 25 ms เซนเซอร์ตัวไหนยังถูกอ่าน และทำไมจำนวนรอบที่วัดได้จึงใกล้หรือห่างจากที่คาด
- ตรวจว่าตัวอย่างคืนค่าทุกอย่างกลับเหมือนเดิม (บรรทัด
--- restored ---) ถ้าไม่ครบ ตัวอย่างจะรายงานRESTORE INCOMPLETEให้บันทึกไว้
หลักฐานที่เก็บไว้ใน portfolio: log ของตัวอย่างทั้งหมด ตารางการคำนวณในข้อ 3 และคำอธิบายข้อ 4
- เอกสาร FreeRTOS หัวข้อ software timers อธิบาย timer command queue และเหตุผลที่ callback ห้ามบล็อก
- โจทย์ท้าทาย: เขียน task ที่ทำงานทุก 20 ms ด้วย
vTaskDelayUntil()แล้ววัดด้วยxTaskGetTickCount()ว่าคลาดไปเท่าไรใน 1000 รอบ เทียบกับvTaskDelay() - เปิดหัวไฟล์ของ cy_tcpwm_counter.h อ่านโหมดการนับ (up, down, up/down) และอธิบายว่าสูตร period + 1 ต้องเปลี่ยนไหมในโหมดอื่น
บทถัดไป: บทเรียน 4.3 Watchdog
สะท้อนคิด
หัวข้อที่มีชื่อว่า “สะท้อนคิด”- งานเป็นระยะในโปรเจกต์ของคุณงานไหนที่ใช้
vTaskDelay()ทั้งที่ควรเป็นvTaskDelayUntil()และคุณจะรู้ได้อย่างไรว่ามันเลื่อน - ถ้าผู้ใช้ขอให้ “อ่านเซนเซอร์เร็วขึ้นสองเท่า” คุณจะถามอะไรกลับก่อนแก้ค่า
แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- SDK: cm33/sensors/05_auto_push_task.c (อัตราของ task เบื้องหลังและขีดจำกัด 50 ms)
- SDK: cycfg_peripheral_clocks.c ของ BSP (ตัวหารของแต่ละอุปกรณ์)
- SDK: cycfg_peripherals.c ของ BSP (ค่าตั้งของ TCPWM และ SCB)
- Infineon mtb-pdl-cat1 (Peripheral Driver Library) @ release-v3.24.0
- FreeRTOS documentation
คำถามทบทวน
ลองตอบเองก่อน แล้วค่อยเปิดดูเฉลย
-
สัญญาณนาฬิกาต้นทาง 100 MHz ผ่านตัวหารที่ตั้งค่า 4999 แล้วเข้า timer ที่นับขึ้นจาก 0 ถึง period ต้องการให้เกิดทุก 10 ms period ต้องเป็นเท่าไร (เป้าหมายข้อ 1)
- 200
- 199
- 2000
- 4999
ดูเฉลย
คำตอบ: B. 199
ค่าตัวหาร 4999 หารด้วย 5000 ได้ 20 kHz ใน 10 ms มี 200 จังหวะ และ timer ที่นับ 0 ถึง period ใช้ period + 1 จังหวะต่อรอบ period จึงเป็น 199
-
UART ใช้สัญญาณนาฬิกา 100 MHz ตัวหารค่า 86 และ oversample 10 ได้ baud เท่าไรโดยประมาณ และคลาดจาก 115200 เท่าไร (เป้าหมายข้อ 1)
- 115200 พอดี
- ราว 114943 คลาดราว -0.22%
- ราว 116279 คลาดราว +0.94%
- ราว 1149425 คลาดสิบเท่า
ดูเฉลย
คำตอบ: B. ราว 114943 คลาดราว -0.22%
100 MHz / (86 + 1) = 1,149,425 Hz หารด้วย oversample 10 ได้ราว 114,943 baud นี่คือค่าที่ BSP ของ SDK ตั้งไว้ให้ debug UART ความคลาดเกิดจากการหารที่ไม่ลงตัว ไม่ใช่ความผิดพลาด
-
งานใดเหมาะกับ timer ของฮาร์ดแวร์ (TCPWM) มากกว่า software timer ของ RTOS (เลือกได้หลายข้อ) (เป้าหมายข้อ 2)
- สร้างคลื่น PWM ที่ขาเพื่อหรี่ไฟ
- วัดความกว้างพัลส์ระดับไมโครวินาที
- ส่งข้อมูลขึ้นคลาวด์ทุก 30 วินาที
- ตรวจสถานะแบตเตอรี่ทุก 5 วินาทีแล้วอัปเดตตัวแปร
ดูเฉลย
คำตอบ: A. สร้างคลื่น PWM ที่ขาเพื่อหรี่ไฟ · B. วัดความกว้างพัลส์ระดับไมโครวินาที
PWM และการวัดระดับไมโครวินาทีต้องการฮาร์ดแวร์ที่นับเองโดยไม่ขึ้นกับภาระของ CPU ส่วนงานระดับวินาทีทำใน task หรือ software timer ได้ ความละเอียดหนึ่ง tick (1 ms ในแม่แบบ) เหลือเฟือ
-
task หนึ่งทำงาน 8 ms แล้วเรียก vTaskDelay(pdMS_TO_TICKS(50)) ทุกรอบ รอบจริงยาวเท่าไร และควรใช้อะไรแทนถ้าต้องการรอบละ 50 ms พอดี (เป้าหมายข้อ 2)
- 50 ms ใช้แบบเดิมได้
- ราว 58 ms ใช้ vTaskDelayUntil() ซึ่งกำหนดเวลาตื่นแบบสัมบูรณ์
- 42 ms ใช้ software timer
- ราว 58 ms ใช้ vTaskDelay(42) ตายตัว
ดูเฉลย
คำตอบ: B. ราว 58 ms ใช้ vTaskDelayUntil() ซึ่งกำหนดเวลาตื่นแบบสัมบูรณ์
vTaskDelay นับจากตอนที่เรียก จึงบวกเวลาทำงานเข้าไปทุกรอบ vTaskDelayUntil นับจากเวลาตื่นครั้งก่อน รอบจึงคงที่ ส่วนการลดค่าหน่วงเองใช้ไม่ได้เพราะเวลาทำงานไม่คงที่
-
ผู้ใช้ตั้ง sensor_auto_set_rate(20) เพื่อให้ค่าเซนเซอร์ทุกตัวบนหน้าจอเร็วขึ้น จากตัวอย่าง 05_auto_push_task ของ SDK จะเกิดอะไร (เป้าหมายข้อ 3)
- ทุกเซนเซอร์อัปเดตเร็วขึ้นห้าเท่า
- ฟังก์ชันคืน error เพราะต่ำกว่าขั้นต่ำ
- ต่ำกว่า 50 ms ลูปอ่านเฉพาะ accelerometer เซนเซอร์อื่นหยุดส่งโดยไม่มี error ใด ๆ
- บอร์ดรีเซ็ต
ดูเฉลย
คำตอบ: C. ต่ำกว่า 50 ms ลูปอ่านเฉพาะ accelerometer เซนเซอร์อื่นหยุดส่งโดยไม่มี error ใด ๆ
งบเวลา 20 ms ไม่พอสำหรับเวลาบนบัสของเซนเซอร์ทุกตัว task จึงเข้าโหมดอ่าน IMU อย่างเดียว ตัวอย่างเขียนไว้ว่า quietly stops publishing five of your six sensors. Nothing returns an error อัตราที่สูงขึ้นมีราคาเป็นภาระของบัสและ CPU เสมอ
อ้างอิงบทเรียนนี้
ถ้านำบทเรียนนี้ไปสอน ทำสไลด์ หรือทำเอกสารต่อ ให้อ้างอิงด้วยข้อความนี้ ถ้าดัดแปลงเนื้อหา ให้เติม (ดัดแปลง)ต่อท้ายชื่อบทเรียน
"Timer และสัญญาณนาฬิกา" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0
ข้อความอ้างอิงภาษาอังกฤษ: "Timers and clocks" from TESA Open Knowledge by the Thai Embedded Systems Association (TESA), https://github.com/tesaiot/tesa-qualification-program, licensed under CC BY-NC 4.0
ลิงก์บทเรียน: https://tesaiot.github.io/tesa-qualification-program/courses/embedded-c-foundations/m04-peripherals/l02-timers-and-clocks/
บทเรียนนี้ดัดแปลงจากต้นฉบับด้านล่าง เมื่ออ้างอิงให้คงเครดิตต้นฉบับไว้ด้วย
https://github.com/tesaiot/tesaiot-pse84-devkit-sdk/tree/ef72c1b658178eee8c38b1e47d28b006f80a59b5 · SDK examples and docs are linked at this commit, not copied into this course. Lessons quote short excerpts (at most 25 lines) with a link to the file at this commit and the credit (Apache-2.0, tesaiot-pse84-devkit-sdk).
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0
เนื้อหาเผยแพร่ภายใต้ CC BY-NC 4.0 นำไปใช้ต่อในงานที่ไม่ใช่เพื่อการค้าได้ โปรดอ้างอิงสมาคมสมองกลฝังตัวไทย (TESA) ทุกครั้ง · วิธีอ้างอิง TESA