สำหรับบล็อกนี้ เราจะพูดเกี่ยวกับ Timer ซึ่งก็จะอ้างอิง เนื่อหาจาก
https://exploreembedded.com/wiki/AVR_Timer_programming และ
http://www.avrbeginners.net/architecture/timers/timers.html
ซึ่งเราจะเขียนถึงระดับรีจิสเตอร์ เพื่อให้เข้าใจถึงรายละเอียด และสามารถประยุกต์ใช้ทำอย่างอื่นได้ แต่ก็จะมีไลบรารี่ให้ดาวน์โหลดเช่นกันสำหรับผู้ที่ต้องการความสะดวกในการเขียนโคีด(ดูเพิ่มเติม
https://playground.arduino.cc/Code/Timer1)
สำหรับโค๊ดตัวอย่างทั้งหมดที่ใช้ในบทความจะอยู่ที่
https://github.com/Menginventor/Timer1_example
ถ้าพร้อมแล้วมาเริ่มกันเลยครับ
Timer/Counter เป็นส่วนประกอบหนึ่งของไมโครคอนโทรลเลอร์ มีหน้าที่จัดการเกี่ยวกับการทำงานที่เป็นคาบเวลา (interval) โดยการทำงานดังกล่าว จะทำงานด้วยฮาร์ดแวร์และเป็นอิสระกับการประมวลผล จึงช่วยลดภาระการประมวลผลและมีความแม่นยำมากกว่าการทำงานด้วยซอฟแวร์
รูปแบบการใช้งาน Timer/Counter
การใช้งาน Timer สามารถแบ่งเป็น 4 รูปแบบคือ
1. Timer Interrupt คือการเรียนฟังก์ชันใดๆ ทุกๆคาบเวลาที่กำหนด
2. สร้างสัญญาณ PWM
3. จับคาบเวลาของสัญญาณภายนอก
4. นับจำนวนลูกคลื่นจากสัญญาณภายนอก
ซึ่งในบทความนี้เราจะพูดถึงการใช้งาน Timer Interrupt เป็นหลัก โดยหัวข้ออื่นๆจะพูดในโอกาสต่อๆไป
Timer/Counterใน ATMEGA328 (Arduino)
ใน ATMEGA328 ประกอบด้วย Timer/Counter 3 ตัว ซึ่งที่รายระเอียดดังนี้
1. Timer0 (8-bit) ใช้สำหรับฟังก์ชัน millis() , micros() , delay() และสร้างสัญญาณ PWM
บน pin 5,6
2. Timer1 (16-bit) สร้างสัญญาณ PWM บน pin 9,10 และนิยมใช้กับไลบรารี่อื่นๆ เช่น Servo.h
3. Timer2 (8-bit) สร้างสัญญาณ PWM บน pin 3,11
ซึ่ง Timer ตัวหนึ่ง สามารถใช้กับงานได้เพียงอย่างเดียวเท่านั้น เช่นถ้าใช้ไลบรารี่ Servo ซึ่งเรียกใช้ Timer1 ก็จะทำให้ใช้ PWM pin 9 , 10 ไม่ได้ หรือถ้าเราใช้ Timer1 ใน Timer Interrupt ก็จะใช้งานทั้งไลบรารี่ Servo และ PWM pin 9 , 10 ไม่ได้ (ยกเว้น Timer0 ที่ถูกตั้งค่า configuration ให้ทำงานตามข้อ 1 ได้ทั้งหมดพร้อมกับ และห้ามแก้ไข configuration เด็ดขาด)
สังเกตว่า PWM pin จะมี OCnX กำกับไว้ทั้งหมด เช่น OC0A ที่ pin 6 คือ
"Timer/Counter 0" , "Output Compare Match" , "Output" , "A"
ดังนั้นในบนความนี้เราจะเลือกใช้ Timer1 เพราะมีความละเอียดมากที่สุดและไม่ส่งผลต่อการทำงานอื่นๆของ Arduino (สำหรับ Timer อื่นๆก็จะมีชื่อและส่วนประกอบคล้ายๆกัน)
ส่วนประกอบของ Timer1
1. Counter
Counter ก็คือตัวนับจำนวน
- จะนับจำนวนของสัญญาณที่จ่ายทาง Count
- สามารถกำหนดทิศทางการนับ (นับขึ้นนับลง) ด้วยสัญญาณ Direction
- สามารถรีเซ็ตกลับเป็น 0 ได้ด้วยสัญญาณ Clear
โดยไมโครคอนโทรลเลอร์ สามารถอ่าน และเขียนค่า Counterใหม่ได้โดยใช้รีจิสเตอร์ TCNTx ซึ่งในที่นี้คือ TCNT1 (สำหรับ Timer1) มีขนาด 16-Bit หมายความว่า TCNT1 สามารถเริ่มนับตั้งแต่ 0 ถึง 65535 แล้วจึงโอเวอร์โฟลว์กลับมาเป็น 0 วนเวียนเช่นนี้
2. Clock Select
Clock select คือตัวเลือกสัญญาณที่จะนำมานับ สามารถเลือกได้โดยการกำหนดบิท
CS (Clock Select) ได้แก่ CS10 , CS11 , CS12 (นั่นคือ Clock Select สำหรับ Timer1 บิทที่ 0 ,1 ,2 ตามลำดับ) โดยสามารถกำหนดได้ดังตาราง
สัญญาณที่นำมานับนั้น มาจาก 2 แหล่งคือ จาก System Clock ( CPU Clock) และ External Clock Source ถ้าเราเลือกใช้ System Clock เราสามารถเลือกใช้ pre-scaling ซึ่งเป็นการหารความถี่ได้ ดังที่แสดงในตาราง และถ้าเราเลือกใช้แหล่งสัญญาณจากภายนอก (External Clock Source ) เราสามารถเลือกได้ว่า จะให้ค่าใน Counter เปลี่ยนแปลงที่ขอบขึ้น หรือขอบลงของสัญญาณ
3. Control Logic
Control Logic จะเป็นตัวควบคุมรูปแบบการนับ ซึ่งจะมีผลมากในการสร้างสัญญาณ PWM โดยสามารถตั้งค่าการทำงานโดยกำหนดบิท WGM (Waveform Generation Mode) ตามตารางที่แสดงนี้
 |
Note : Bottom = 0 , Max = 0xFF (65535) |
ซึ่งในการใช้งานในโหมด Timer นั้น เราจะใช้ Mode 0 (เพราะไม่ต้องสร้าง Waveform)
ตัวอย่างที่ 1 (EX1)
จะเป็นการอ่านค่าจาก Counter มาแสดงที่ Serial Monitor โดยจะมีการ Set Register TCCR1 ซึ่งแบ่งเป็น A และ B โดยมีรายละเอียดดังนี้
สำหรับบิท COM1A1,COM1A0,COM1B1,COM1B0 จะเป็นการกำหนด Compare Output Mode ซึ่งใช้ในการสร้าง PWM ดังนั้นจะยังไม่กล่าวถึงในบทความนี้ โดยการ set ทุกบิทข้างต้นให้เป็น 0 จะเป็นการกำหนดให้ OC1A และ OC1B ไม่เชื่อมต่อกับ Pin 9,10 ทำให้สามารถใช้ Pin 9,10 ในการทำงานอื่นๆได้
สำหรับบิท ICNC1 และ ICES1 จะถูกใช้ใน Input Capture Mode จึงตั้งให้เป็นค่า 0
เนื่องจากเราใช้งาน Timer ใน Normal Mode (นับไปเรื่อยๆ) ดังนั้น WGM จะถูกตั้งเป็น 0 ทั้งหมด
เมื่อเราตรวจสอบจากตาราง Waveform Generation Mode โดยเมื่อเราเลือกใช้ Mode 0 จะพบว่าค่าสูงสุดจะมีค่า (0xFFFF) = 65535 และเกิดการ Timer Overflow Interrupt ที่ค่า Max ก็คือ 65535 เช่นกัน
การเลือก Pre-Scale
เลือกจากความละเอียดของเวลา และ ความยาวช่วงเวลา ในตัวอย่างนี้เลือก (System clock ÷ 8) จะได้ความถี่เท่ากับ 16 MHz ÷ 8 = 2 MHz ดังนั้นคาบเวลาของสัญญาณ clk จะเท่ากับ 1/2 MHz จะได้ 0.5 Microseconds และนับเวลาได้สูงสุด 32.767 Milliseconds ต่อการ Overflow
1 รอบ ดังนั้นจึงทำการเซ็ต TCCR1A = 0 และ TCCR1B = (1 << CS11)
ผลการทดลอง
จากผลการทดลองจะเห็นว่าค่า TCNT1 วิ่งอยู่ในช่วง 0 ถึง 65535
(เนื่องจากเรา print อยู่ใน loop ทำให้ไม่เห็นทุกการเปลี่ยนแปลง เพราะค่า TCNT วิ่งเร็วจนแสดงผลไม่ทัน อาจจะลองเพิ่มค่า pre-scaler เพื่อดูการเปลี่ยนแปลง)
ตัวอย่างที่ 2 (EX2)
สำหรับตัวอย่างนี้จะมีการเซ็ต Register TIMSK1 (timer1 interrupt mask register) เพิ่มเข้ามา ซึ่งมีรายละเอียดดังนี้
ในการใช้งาน Overflow Interrupt จะต้องเซ็ท TOIE1 (Timer 1 Overflow Interrupt Enable) ให้เป็น 1 ส่งผลให้เมื่อเกิด Overflow ขึ้น จะไปเรียก ISR ที่เราตั้งไว้ และแสดงค่าเวลาปัจจุบัน (micros()) ออกมา
ผลการทดลอง
จะเห็นว่าแต่ละครั้งที่ Overflow ค่า micros() จะห่างกัน 32768 microseconds ตามที่เราคำนวณ
ตัวอย่างที่ 3 (EX3)
จะเห็นว่ามาการเซ็ท WGM12 เป็น 1 ทำให้ Waveform Generation Mode ทำงานใน Mode 4 จากการตรวจสอบตารางจะพบว่า Counter จะมีค่าสูงสุดเท่ากับ OCR1A (Output Compare Register Timer 1 , A) ซึ่งเป็น Register 16-bit ให้เรากำหนดค่าได้ตามต้องการ หลังจากที่ Counter มาถึงค่าที่กำหนด ค่าจะกลับไปเป็น 0 โดยอัตโนมัติ ตามชื่อ CTC (Clear To Compare) Mode
จากนั้นเราเซ็ท OCIE1A (Output Compare Interrupt Enable) เป็น 1 ทำให้จะเกิดการอินเทอร์รัพ เรียก ISR ทุกครั้งที่ค่า Counter เท่ากับ OCR1A
จากการตั้งค่าทั้งสองนี้ เราจะได้ Interrupt ที่สามารถกำหนดคาบเวลาได้ตั้งแต่ 0 - 32768 Microseconds โดยมีความละเอียด 0.5 Microseconds ตามที่เราได้กำหนดไว้ใน Pre-scale โดยคาบเวลาในการอินเทอร์รัพแต่ละครับนั้น สามารถคำนวณได้จาก usToTicks(Time) - 1
(ส่วนที่ต้อง - 1 เพราะในการนับ 5 ครั้ง เรานับแค่ 0 - 4 ดังนั้นเราจึงเปรียบเทียบแค่ตัวที่ 4 นั่นคือ 5 - 1 นั้นเอง)
ผลการทดลอง
จากผลการทดลองจะเห็นว่าแต่ละครั้งที่มีการ Interrupt จะมีคาบเวลาเท่ากันคือ 25000 Microseconds (จะมีบางครั้งที่กลายเป็น 24996 , 25004 แต่ก็เฉลี่ยได้เท่าเดิม อาจจะเกิดจากการเหลือมของการจับเวลา) จากตรงนี้เราสามารถสร้าง Timer Interrupt ได้แล้ว ในคาบเวลาไม่เกิน 32768 Microseconds
ตัวอย่างที่ 4 (EX4)
สำหรับตัวอย่างที่ 4 เราจะขยายช่วงเวลาที่ Timer Interrupt จาก 32,768 Microseconds ให้เป็น 2,147,483,648 Microseconds หรือประมาณ 35 นาที โดยจะนำ Output Compare Interrupt มาผสมผสานกับ Overflow Interrupt และใช้ Normal Mode (ไม่ใช้ CTC)
แนวคิดคือ การ Overflow หนึ่งครั้ง เป็นเวลา 32768 Microseconds ถ้าต้องการเวลามากกว่านี้ ก็ให้โอเวอร์โฟลว์จนกว่าจะถึงค่าที่ต้องการแล้วจึงใช้ Output Compare จัดการกับเศษของคาบเวลาที่เหลือ
ผลการทดลอง
จะเห็นว่าเราสามารถสร้าง Timer Interrupt ได้แล้ว โดยมีช่วงเวลากว้างกว่าเดิม และมีความละเอียดระดับ Microsecond
บทสรุป
เราสามารถสร้าง Timer Interrupt จาก Timer ได้ ถึงแม้จะไปเหมือนกับการใช้ไลบรารี่ก็ตาม แต่ความรู้ส่วนนี้สามารถถูกพัฒนาไปใช้ต่อกับอย่างอื่นได้ เช่นการควบคุมเซอร์โว หรือโปรโตคอล การสื่อสาร ซึ่งผมอาจจะได้มีโอกาสนำมาแสดงในโอกาสต่อๆไป