->> หมวดหมู่และหัวข้อบทความ << คลิ๊ก
บันทึกส่วนตัว ขุดวิธี หัดเขียน atmega328p ด้วยภาษาC -GCC avr-libc (atmel studio ,arduino) ให้ได้ ASM ที่ต้องการ
--ส่วนนี้เป็นบันทึกความเข้าใจส่วนตัว ไม่ได้เน้นครอบคลุมเนื้อหาทั้งหมด ซึ่งผมทำได้แค่ไหนก็แค่นั้น
แต่เนื่องจากผมกำลังหัดเขียนภาษาC ซึ่งผมก็ได้อ่านและทำความเข้าใจชนิดตัวแปร ไวยากรณ์ การวนหลูป เงื่อนไข โอเปอเรเตอร์ ก็พอเข้าใจแล้ว ดาต้าชีทของ atmega328p ก็อ่านไปพอสมควร ภาษาASMของMCUก็ขุดจนพอเข้าใจโครงสร้างเมมโมรี่ รีจิสเตอร์และชุดคำสั่งถ้าจะเขียนด้วยภาษาASMเสียเลยก็ทำได้ทันทีซึ่งผมก็ชอบด้วยซึ่งก็เคยเขียนอยู่มันง่ายมากๆ
แต่พอจะเริ่มเขียนC ด้วย atmel studio ก็เริ่มงงทันที โดยมีปัญหาดังนี้ เช่น
1. ชื่อตัวแปรที่อ้างอิงรีจิสเตอร์ต่างๆใน ไมโครคอนโทรลเลอร์ และการให้ค่า การเรียกใช้ เรียกชื่ออะไร
2. ค่าเริ่มต้นที Atmel studio หรือที่เราต้องกำหนดมีอะไรบ้าง เช่นการกำหนดตำแหน่งอินเตอรัพ การให้ค่าสแตก การกำหนดพอร์ท การกำหนดค่ารีจิสเตอร์ การใช้ไทมเมอร์ภายใน การฟ้องอินเตอรัพและขัดจังหวะการทำงาน ในหรือด้วยภาษาC ทำอย่างไร
3. การกำหนดเมมโมรี่ หรือการโหลดข้อมูลเข้าเมมโมรี่ของไมโครคอนโทรลเลอร์ด้วยภาษาC การใช้พ้อยเตอร์ และตัวแปรอาเรย์ เข้าไปเก็บข้อมูล ณ ตำแหน่งหน่วยความจำที่เราต้องการทำอย่างไร ประกาศตัวแปร หรือค่าaddress ของอาร์เรย์ หรือพ้อยเตอร์ตรงไหน ทำได้แค่ไหน
ปัญหาเหล่านี้บางปัญหาหาอ่านในตำราภาษาไทยไม่ได้เลย คงต้องไปหาอ่านเอาของต่างประเทศ ไปขุดจากประสบการณ์ที่เขาเขียนกัน หรือขุดทดลองเขียนเอาเอง ซึ่งปวดหัวมากมายเลยต้องบันทึกความปวดหัวเหล่านี้เอาไว้ไม่ให้ปวดหัวอีกต่อไป --
ความรู้ใหม่
.ต่อไปนี้คงเป็นการเรียนวิธีการเขียนภาษาC ด้วยการแปลงเป็นasm แล้วอ่านโค๊ดasmเรียนรู้ว่าCที่เขียนทำได้ตามที่เราต้องการหรือไม่ ในที่นี้จะบันทึกเฉพาะที่ใช้งานได้จริง ...
กำลังดำน้ำอยู่ครับ
30/8/60
เรียนC -GCC เขียน atmega328p ด้วย ATmel Studio
..ภาษาC ในที่นี้คงเป็นC++ หรือ G++ ซึ่ง Atmel studio หรือ arduino IDE ต่างก็ใช้ C-compilerของ GNU ซึ่งอยู่ในส่วน GCC และใช้ avr-libc ซึ่งภายในไลบารี่มีฟังก์ชั่นที่จำเป็นในการใช้งานไมโครคอนโทรลเลอร์ในตระกูลAVR แต่มันไม่ได้เขียนด้วยภาษาที่ผู้เริ่มต้นใช้งานจะเข้าใจได้ง่ายๆ ต้องขุดเอาเองสร้างสมมติฐานแล้วก็ทดลองใช้งานสรุปผล หรือหาทีปรึกษาเรียนเองซึ่งหาได้ในเว็บ avr freak ,stack overflow ,Youtube ,เว็บให้ความรู้ต่างๆ ซึ่งมีคนมาแปะปัญหาเอาไว้มากมาย ..
สิ่งที่พอค้นคว้าเบื้องต้นได้ สำหรับ ATMEL Studio 7.0 สามารถอ่านเนื้อหาหลักใน www.atmel.com/webdoc/GUID-ECD8A826-B1DA-44FC-BE0B-5A53418A47BD/
AVR 8-bit GCC Toolchain 3.5.3 with upstream versions:
gcc 4.9.2 (The GNU Compiler Collection)
4.9.2 เข้าใจว่าเป็นเวอชั่น ปี2014
avr-libc 2.0.0 [?atmel] คือไลบารี่C ที่เกี่ยวข้องกับการเขียนAVR
avr-libc 1.8.0 manual pdf
avr-libc 2.0[?site GNU]
(Binutils 2.26 , gdb 7.8)
ปัญหาต่อมาคือ G++ หรือ GCC มันไม่ใช่ภาษาC++ซะทีเดียว เท่าที่สังเกตุบางอย่างก็หนักไปทาง ภาษาC มากกว่า บางอย่างก็มีในเฉพาะภาษาC++ คือมันมีรูปแบบภาษาหรือไวยากรณ์ส่วนใหญ่ในC ส่วนไลบารี่ต่างๆที่ใช้เฉพาะกับไมโครคอนโทรลเลอร์จะถูกพัฒนาต่อยอดหรือตัดทิ้งเพิ่มเติมอะไรจากภาษาCก็สุดแต่ทางผู้พัฒนาภาษาสร้างสรรค์ขึ้นมาเป็นavr-libc และเขาเรียกภาษาตัวเองว่า GCC หรือ G++ ซึ่งผมอาจจะทดสอบลองเขียนและยกบางตัวอย่างที่ทำได้หรือไม่ได้ในGCC เช่นการกำหนดตัวแปรในฮีปจะใช้คำสั่งในภาษาC เช่น malloc ,free เป็นต้น แต่ไม่สามารถใช้โอเปอเรเตอร์ในภาษาC++คือ new กับ delete ที่จะจัดการสร้างตัวแปรในฮีปได้ เป็นต้น
..สำหรับ arduino มีที่น่าสนใจ http://garretlab.web.fc2.com/en/arduino/inside/index.html..
7878
เขียนภาษา C (Atmel studio 7.0) ให้เป็น ASM ที่ต้องการ | ||
คอลัมด้านซ้ายเป็นคำถามที่ต้องการแก้ปัญหา ส่วนด้านขวาเป็นวิธีการแก้ปัญหาละกัน ปัญหาส่วนใหญ่คือเมื่อเราเริ่มต้นจะเขียนCแล้วไม่รู้จักอะไรๆในC เราก็จะตั้งตำถามเพื่อหาคำตอบเอาเองซึ่งบางทีอาจจะเป็นคำตอบเชิงสมมติซึ่งจะเป็นตัวหนังสือสีแดงออกในแนวว่าคิดว่า แต่ถ้าลงด้วยการแก้ไขแสดงว่าผ่านการค้นคว้าหาคำตอบแล้ว ในส่วนที่บอกว่าทดลองเขียนก็จะระบุชัดเจนว่าทดลองเขียนแล้ว |
||
[ปัญหา] | [แก้ไข] / [คิดว่า] | |
1. | [ชื่อรีจิสเตอร์IO ในC] 1. เรียกยังไง? เก็บอยู่ตรงไหน? 2. ทดลองการเรียกค่า ใส่ค่าให้ดูหน่อย ขอตัวอย่างทั้งC และ ASM |
1. [ที่เก็บชื่อรีจิสเตอร์] ให้ใส่ #include <avr/io.h> ในส่วน header file แล้วมันจะโยงไปที่เก็บ C:\Program Files (x86)\Atmel\Studio\7.0\toolchain\avr8\avr8-gnu-toolchain\avr\include\avr\iom328p.h ซึ่งชื่อรีจิสเตอร์ส่วนใหญ่ที่เรียกใช้งานได้ในCจะตั้งค่าตามชื่อรีจิสเตอร์ในดาต้าชีท Atmega328p (ยกเว้นบางชื่ออาจไม่ตรงแต่มีคอมเม้น) ซึงเมื่อเปิดไฟล์ออกมาจะมีภาษาแมคโคร หรือภาษาpreprocessor ประกอบด้วยคำสั่ง #define ที่กำหนด เช่น // Registers and associated bit numbers #define PORTB _SFR_IO8(0x05) #define PORTB0 0 #define PORTB1 1 #define PORTB2 2 #define PORTB3 3 #define PORTB4 4 #define PORTB5 5 #define PORTB6 6 #define PORTB7 7 .... // Interrupt Vectors [???เข้าใจว่าเป็นตัวฟ้องอินเตอรัพ] // Interrupt Vector 0 is the reset vector. #define INT0_vect_num 1 #define INT0_vect _VECTOR(1) //^ External Interrupt Request 0 .... /* Constants */ #define SPM_PAGESIZE 128 #define RAMSTART (0x100) #define RAMEND 0x8FF // ^ Last On-Chip SRAM Location #define XRAMSIZE 0 #define XRAMEND RAMEND #define E2END 0x3FF #define E2PAGESIZE 4 #define FLASHEND 0x7FFF ... |
2. | ..ต่อ [ชื่อรีจิสเตอร์IO ใน C] ... #define SLEEP_MODE_IDLE (0x00<<1) #define SLEEP_MODE_ADC (0x01<<1) ... เนื่องจากไม่เข้าใจภาษาแมคโครเลยจึงค้นดูว่าหมายถึงอะไรและสรุปความจากข้อมูล ดังต่อไปนี้ โดยแนบลิงค์ที่น่าสนใจและอ้างอิงมา ดังนี้ 1. [forum:avrfreaks , IO Port address decoding] 2. [บทความ, understanding more about AVR Programming] 3. [pdf, AVR Registers and AVR GNU CC Defines] 4. [forum:arduino , Trick for getting PORT / SFR_IO address] /*คำว่า... */ #define PORTB _SFR_IO8(0x05) #define PORTB0 0 ..... ..... #define PORTB7 7 เป็นการกำหนด ค่า address IO ให้กับชื่อที่ตั้ง เช่นชื่อ PORTB และเมื่อค้นต่อไปก็มีนิยามในภาษาC เพิ่มเติม จากไฟล์ ..\sfr_defs.h ซึ่งถูก include เข้าไปใน avr\io.h /* avr/sfr_defs.h - macros for accessing AVR special function registers */ #define _SFR_IO8(io_addr) _MMIO_BYTE((io_addr) + __SFR_OFFSET) #define _MMIO_BYTE(mem_addr) (*(volatile uint8_t *)(mem_addr)) ทั้งสองไฟล์จะอิงการให้ค่าที่อยู่เป็น2พวก ในภาษา macro เช่น #define PORTB _SFR_IO8(0x05) --ให้ค่าที่อยู่ด้วย IO addr ผ่านฟังก์ชั่น _SFR_IO8() #define WDTCSR _SFR_MEM8(0x60) --ให้ค่าที่อยู่ด้วย memory addr ผ่าน _SFR_MEM8() -------------------------------------------------------------------------- SFR_IO8() ต่างกับ SFR_MEM8() อย่างไร โดยหลักการ ปรกติในดาต้าชีท atmega328p เป็นชื่อIOสากล ถ้าดูในชุดคำสั่งMCU จะพบว่า PORTB มีที่อยู่ในแรมคือ 0x25 จากที่อยู่ 0x000-0x8FF แต่มีที่อยู่ IO 0x05 จากที่อยู่IOตำแหน่งที่ 6 จาก IO 224ไบท์ ซึ่ง PORTB 1. เป็น I/O bit access ใช้คำสั่งASM cbi/sbi/sbic/sbisได้ ^ซึ่งต้องใช้ที่อยู่ IO 32ไบท์แรก (0-31) ในการออกคำสั่ง 2. ใช้คำสั่ง IN,OUT with I/O space addr or name ^ซึ่งต้องใช้ที่อยู่ IO 64 ไบท์แรก (0-63) ในการออกคำสั่ง 3. และใช้คำสั่ง LD.. , ST.. ได้ ^** ใช้ที่อยู่mem หรือที่อยู่แรม 0x000-0x8FF ในการออก คำสั่งเพื่อโหลดข้อมูลมาใช้หรือเก็บข้อมูลกลับ WDTCSR มีที่อยู่IO 0x60 ซึ่งมีค่าเกินที่อยู่IO 64ไบท์แรก จึงสามารถใช้คำสั่ง จำพวกที่3เท่านั้นคือ LD... และST... ใช้เวลา 2 cycleขึ้นไป ในการโหลดข้อมูล เข้ามาคำนวนในR0-R31 หรือเก็บข้อมูลกลับไป ถ้าคอมไพล์เลอร์อาจเลือก LDS , STS คือโหลดข้อมูลโดยตรง หรือใช้ LD.. และ ST.. มาใช้โดยการใช้รีจิสเตอร์พ้อยเตอร์ชี้ไปที่เมมก็ได้ [เข้าใจว่า...คงใช้หลักการที่ บางIO register สามารถใช้ชุดคำสั่งได้3แบบบ้าง 2แบบบ้าง และแบบเดียวบ้าง จึงยัดฟังก์ชั่นในไฟล์ iom328p.h และ sfr_defs.h ด้วยการใช้ภาษาแมคโครเพื่อบอกให้Cเข้าใจและเลือกชุดคำสั่งในการโหลดหรือstoreข้อมูลได้อย่างถูกต้องตามหลักภาษาของเครื่องหรือภาษาแอสเซมบลี้(ASM)] ----------------------------------------------------------------- [คิดว่า ...เท่าที่ลองสำรวจดูพบว่าiom328p.h ไม่ได้มีชื่อรีจิสเตอร์ทุกตัวให้เรียกใช้งาน เช่น ไม่พบ SREG และลูกๆของมันเช่น Z หรือ zero flag แล้วจะทำไงต่อครับ??] [ตอบ-อยู่ใน common.h] ----------------------------------------------------------------- ความหมายของไฟล์แต่ละไฟล์ ที่รวมใน io.h iom328p.h -- กำหนดชื่อที่อยู่รีจิสเตอร์รวมถึงขนาด(8,16บิท) และชื่อแฟลกเฉพาะ ของatmega328p [...จึงไม่พบรีจิสเตอร์ทั่วไปอย่างSREG] sfr_defs.h -- macros for accessing AVR special function registers [^เข้าใจว่าเป็นการกำหนดขอบเขตให้ภาษาCรู้ว่าที่อยู่เมมที่กำหนดให้ จะใช้ ชุดคำสั่งพวกไหนได้บ้างตามที่อยู่ของรีจิสเตอร์ว่าอยู่ในพวกไหนใน3จำพวก] portpins.h -- กำหนดชื่อ PORTn, DDn, PINn ,PORTxn Pxn แบบสั้นและยาว เพื่อให้เวลาพิมพ์ใช้แทนกันได้หมด เช่น PORTA0 , PA0 common.h -- กำหนดเลือกชื่อที่อยู่รีจิสเตอร์รวมถึงขนาดรีจิสเตอร์และชื่อแฟลกทั่วไปใน ตระกูล avr เช่น SP,SPL , EEAR ,SREG , แฟลกZ คือ SREG_Z , XL,ZH , RAMPX version.h -- define avr lib version และไฟล์ที่เกี่ยวกับ fuse.h , lock.h [****ในบางครั้งAtmel studio 7.0 ภาษาC เราอาจต้องอินครู๊ดไฟล์เพิ่มจาก io.h เช่นต้อง #include <avr/common.h> ซ้ำต่ออีกครั้งในเฮดเดอร์ เพื่อทำให้ฟังก์ชั่นแยกเยะ ตัวแปรของ Atmel ทำงานได้ดีขึ้น เพราะถ้าไม่เพิ่มเข้าไปเช่น เวลาเราเขียนคำว่า SREG สีของตัวแปรจะไม่ถูกแยกออกว่าเป็นตัวแปรที่ถูก define แล้วใน header*****] |
|
3. | [ตัวแปร IO ใช้ยังไง] | |
ตัวอย่างการใช้ รีจิสเตอร์ IO ช่วยทดลองการเรียกค่า ใส่ค่าให้ดูหน่อย ขอตัวอย่างทั้งC และ ASM ได้ทำการคอมไพล์ดูพบว่าเราไม่สามารถประกาศตัวแปร IO ได้เลย เนื่องจากตัวแปรIO register ได้ถูกกำหนดชื่อเอาไว้จากfile io.h ลิงต์ไปยัง iom328p.h และไฟล์อื่น ซึ่งตัวแปรไอโอทั้งหมดทั้งที่ใช้งานได้ในระดับไบท์หรือบิทได้ถูกกำหนดขึ้นทั้งหมดแล้ว ไม่สามารถเพิ่มเติมหรือเปลี่ยนแปลงได้โดยมือใหม่อย่างเราๆ GCC เวอชั่นนี้ เราทำได้แต่กำหนดตัวแทนชื่อตัวแปรเท่านั้น เช่น #define eeprom_ctrl EECR // กำหนดด้วยภาษาแมคโคร แล้วนำ ตัวแปร eeprom_ctrl ไปใช้ในภาษาCได้ คอมไพล์เลอร์เพียงเปลี่ยน eeprom_ctrl เป็น EECR เท่านั้น ไม่สามารถประกาศเป็นตัวแปรทับดังนี้ได้ unsigned char eeprom_ctrl ; // คอมไพล์ไม่ผ่าน ??? ???เด๋วนะรู้สึกจะประกาศตัวแปรโลคอลได้ เป็นลักษณะโหลดค่าคงที่มาใช้เท่านั้น unsigned char eeprom_ctrl = EECR; ขอยกตัวอย่างก่อนอธิบายนะครับ EECR เป็นIOรีจิสเตอร์ที่ทำหน้าที่เกี่ยวกับการสั่งให้MCU อ่านหรือเขียน EEPROMที่อยู่ในตัวAtmega328pเอง ซึ่งต้องเกี่ยวข้องกับรีจิสเตอรอีก3ตัวคือ -EEARH (0x22) ที่อยู่ชี้ไบท์สูงของeeprom -EEARL (0x21) ที่อยู่ชี้ไบท์ต่ำeeprom ที่อยู่IO = 0x21 -EEDR (0x20) ที่เก็บข้อมูลเวลาอ่านหรือเขียน eeprom ^ 3IOข้างบน ในภาษา asm I/O no bit access but IN OUT , LD.. ST.. -EECR (0x1F) ตัวคอนโทรลอ่าน-เขียน ^ ส่วนตัวนี้ สามารถใช้คำสั่งลงระดับบิทได้ bit access,IN OUT , LD.. ST.. [... ส่วนขั้นตอนการอ่านเขียนต้องไปหาอ่านดูในบทก่อนๆที่เคยเขียนไว้ ในกรณีนี้เราไม่ได้ใช้ ไลบารี่ EEPROM.h และเป็นการยกตัวอย่างในการให้ค่ารีจิสเตอร์IOเท่านั้น ไม่ได้สนใจอินเตอรัพและการรอจังหวะให้เขียนหรืออ่านอีอีพรอมได้ซึ่งมันทำงานช้ากว่าMCUมากต้องมีเวลาการรอทำงาน ซึ่งโค๊ดตัวอย่างนี้อาจไม่สมบูรณ์ ] ในที่นี้เราต้องการสั่งให้เขียน eeprom ที่ตำแหน่ง0x02 ด้วยค่า 0x55 1ไบท์ #define eeprom_ctrl EECR #define eep_data_write EEDR #define eep_addr_high EEARH #define eep_addr_low EEARL int main(void) { eep_addr_low = 0x02; // LDI R24,0x02 ให้ค่า2 แก่ R24 OUT 0x21,R24 ให้R24แก่ EEARL (0x21) EEARH = 0x00; OUT 0x22,R1 ให้R1=0แก่ EEARH eep_data_write = 0x55; LDI R24,0x55 ให้0x55 แก่ R24 OUT 0x20,R24 ให้ค่า กับ EEDR (0x20) eeprom_ctrl =0; OUT 0x1F,R1 ให้0 แก่ EECR (0x1F) eeprom_ctrl |= 0b00000100; SBI 0x1F,2 ให้บิทที่2ของEECR เป็น1 เพื่อยืนยันการเริ่มต้นว่าต้องการเขียน คำสั่งต่อไปนี้ให้ผล SBI 0x1F,2 เหมือนกัน // EECR |= (1<<EEMPE) ; ในiom328p.h นิยาม EEMPE คือ2 -บิท2 ของEECR // EECR |= (1<<2) ; เอาค่า()มา or หรือยูเนี่ยนกับ EECR นิยามค่าคือ (นำค่า1 หรือ 0b0000 0001 มาวนบิทไปทางซ้าย2ครั้ง จะได้ค่าเป็น 0b0000 0100 ) ซึ่งโดยความหมายเหมือนกันกับ EECR |=0b0000 0100 eeprom_ctrl |= 0b00000010; SBI 0x1F,1 ให้บิทที่1ของEECR=1เริ่มเขียน คำสั่งต่อไปนี้ มีผลASM เหมือนกัน // eeprom_ctrl |=(1<<EEPE); นิยามของ EEPE คือ1 บิทที่1ของEECR // EECR = EECR | (1<<1) ; กรณีเขียนแบบยาว } ------------------------------------------------------------- การให้ค่าไบท์หรือบิท ในรีจิสเตอร์IO อย่างที่ได้เกริ่นไปแล้วว่า ชนิดIO แบ่งเป็น 3 ชนิดคือ แบ่งชนิดที่ใช้คำสั่งเครื่องได้ 3 แบบ ซึ่งทำให้เมื่อแปลงเป็นASM จะแตกต่างกันไป 1. IO (0x00-0x1F) ใช้คำสั่งเกี่ยวกับบิทได้โดยตรง , IN OUT , LD.. ST.. ได้ เช่น PORTB (0x05) 2. IO (0x20-0x3F) ใช้คำสั่งได้2พวกที่เรียกและคืนข้อมูล IN OUT, LD.. ST.. ได้ เช่น SREG (0x3F) หรือ Status register ^2พวกบน ซึ่งเมื่อมีการเรียกข้อมและคืนข้อมูลด้วยภาษาASM จะใช้เพียง IN OUT เท่านั้น เพราะสะดวกและคำสั่งสั้นและเร็ว1cycle แต่ต้อง ใส่ค่าIO space address 0-63 แทน mem address 3. IO mem(0x60-0xC6) ใช้คำสั่งเรียกและคืนข้อมูลด้วยที่อยู่เมม ได้เพียง LD.. ST.. เช่น WDTCSR (0x60 mem addr) Watchdog Timer control register --------------------------------------------------- เราจะมาทดลอง เมื่อต้องการให้ค่าIO ทั้งสามชนิดจะต้องใช้โอเปอร์เรเตอร์อย่างไร ที่ทำให้แปลงเป็น ASM แล้วได้ใจความที่สั้นกระชับและเหมาะสม สมมติตัวแทนIO เป็น แบ่งเป็นแต่ละชนิดIO จะต้อง ก. ให้ค่าใหม่เป็นไบท์กับIO นั้น ในภาษาC จะใช้คำสั่งให้ค่าโดยตรง เช่น PORTB = 0x04; ข. ให้ค่าบิทใดบิทหนึ่งในIOเป็น0 หรือ1 หรือหลายๆบิท เป็น0หรือ1 ในภาษาC จะให้ค่าโดยอ้อมโดยคล้ายเรียกข้อมูลออกมาแล้วใช้โอเปอเรเตอร์ดังนี้ & ( and แอนด์ หรือ แอนด์โลจิก) กับ0 คือการกำหนดให้ค่าในบิทในไบท์เป็น0 -------------------------------- ข1. &สำหรับ 1 บิท โค๊ดต่อไปนี้มีความหมายเหมือนกันทุกประการ PORTB = PORTB & 0b11011111 ; กำหนดให้บิทที่5 เป็น 0 PORTB &= 0b11011111 ; เขียนแบบย่อ1 PORTB &= ~(1<<PORTB5) ; แบบย่อ2 --PORTB5 มีค่า 5 ^ = PORTB มา&กับ ~(เอา 0b00000001 มาเลื่อนบิทไปทางซ้าย 5ครั้ง) = PORTB & ~(0b00100000) = PORTB & 0b11011111 แบบย่อ2ไม่แนะนำสำหรับมือใหม่ เพราะ อ่านแล้วจะงงมาก --------------------------------- ข2. &มากกว่า 1 บิท โค๊ดต่อไปนี้มีความหมายเหมือนกันทุกประการ PORTB &= 0b11011101 ; ให้บิทที่1 และ5 เป็น0 PORTB &= ~((1<<5)|(1<<1)) ; เขียนไม่ยาก แต่เข้าใจยากนิดนึง --------------------------------------- | ( or ออ หรือ ออโลจิก ) กับ1 คือกำหนดให้ค่าบิทใดๆในไบท์เป็น1 -------------------------------- ข3. or สำหรับ1บิท โค๊ดต่อไปนี้มีความหมายเหมือนกันทุกประการ PORTD = PORTD | 0b00001000 ; ให้บิทที่3เป็น 1 บิทอื่นไม่เปลี่ยน PORTD |=0b00001000 ; เขียนแบบย่อๆ PORTD |=(1<<3) ; แบบย่อที่นิยมมาก --------------------------------- ข4. or มากกว่า1บิท เหมือนกันทุกแบบ PORTD |= 0b10001001 ; ให้บิทที่7 ,3และ0เป็น 1 PORTD |=(1<<7)|(1<<3)|(1<<0) ; แบบย่อที่นิยม [...ในการนี้ขอยกตัวอย่างเพียงการให้ค่าIO โดยตรงและการให้ค่าบิทเป็น0 น่าจะเพียงพอแล้ว...] --------------------------------------------------------------------------------- ก. IO ชนิด1 ชนิด2 ชนิด3 PORTB=6; / SREG=6; / WDTCSR=7; IO addr=0x05 IO addr=0x3F mem addr=0x060 ============================================= LDI R24,0x06 / LDI R24,0x06 / LDI R24,0x07 OUT 0x05,R24 / OUT 0x3F,R24 / STS 0x0060,R24 4byte-2clock 4b-2cl 6b-3cl ---------------------------------------------------------------------------------- ข1. unsigned char i= 0b11101111; //(i=0xEF) local variable ให้บิทที่ 4 เป็น0 PORTB &= i ; / SREG &= i ; / WDTCSR &= i; ============================================= CBI 0x05,4 / IN R24,0x3F / LDI R30,0x60 / ANDI R24,0xEF / LDI R31,0x00 / OUT 0x3F,R24 / LDD R24,Z+0 ^มีคำสั่งที่สั้นกว่ามาก เช่น / ANDI R24,0xEF BCLR 4 แต่GCCไม่ได้ใช้ / STD Z+0,R24 2byte-1clock 6b-3cl 10b -7cl ---------------------------------------------------------------------------------- [กรณีที่ต้องการเปลี่ยน บิทใดๆใน SREG ให้เป็น1หรือ0 ด้วยคำสั่งASM BSET s , Flag set โดยที่ s{0..7} sคือตำแหน่งบิทใน SREG BCLR s , Flag clear 2byte-1clock ต้องเขียนด้วยภาษาC อย่างไร? ยังหาวิธีไม่ได้ ] ข2. unsigned char i= 0b11100111; // (i=0xE7) local variable ให้บิทที่ 3,4 เป็น0 PORTB &= i; / SREG &= i ; / WDTCSR &= i; ============================================= IN R24,0x05 / IN R24,0x3F / LDI R30,0x60 ANDI R24,0xE7 / ANDI R24,0xE7 / LDI R31,0x00 OUT 0x05,R24 / OUT 0x3F,R24 / LDD R24,Z+0 / ANDI R24,0xE7 / STD Z+0,R24 6byte-3clock 6b-3cl 10b -7cl ---------------------------------------------------------------------------------- ค. การหมุนบิทวนไปทางซ้ายหรือทางขวา หรือหมุนและผ่านค่าcarry(ตัวทด)เข้าไปในตัวแปร ซึ่งปรกติก็ใช้ได้ทั้งตัวแปรIO หรือตัวแปรสแตกติกหรือโกบอลทั่วไป หรือตัวแปรรีจิสเตอร์ ซึ่งการใช้งานก็มีหลากหลายตามต้องการ เช่นนำตัวทดไปตัดสินใจทำอะไรสักอย่างเมื่อหมุนค่า ออกจากไบท์ ถ้าในสมัยก่อนก็จะส่งไปอีอีพรอมหรือการติดต่อชนิด2สาย การส่งข้อมูลทีละบิท จะต้องกระทำโดยผ่านตัวทดเป็นต้น หรือไฟหมุนวิ่งวนรอบก็ใช้หลักการคล้ายๆการวนบิทผ่านตัว ทดที่ต้องการ ซึ่งคำสั่งทางภาษาของ ASM เมื่อใช้กับ PORTD จะเป็น ASR หมุนไปทางขวา>> บิท0เข้าC บิท7มีค่าเท่าเดิม ldi r16,$10 ; Load decimal 16 into r16 asr r16 ; r16=r16 / 2 ldi r17,$FC ; Load -4 in r17 asr r17 ; r17=r17/2 LSL หมุนไปทางซ้าย<<ผ่านค่าเป็น0เข้าไปแทนที่บิท0 LSR หมุนไปทางขวา>>ผ่านค่า0เข้าไปแทนแทนที่บิท7 ROL หมุนไปทางซ้าย<<โดยผ่านcarryเข้าไปแทนที่บิท0 ROR หมุนไปทางขวา>>โดยผ่านcarryเข้าไปแทนที่บิท7 SWAP สลับที่4บิทสูงและต่ำกัน แต่ภาษาC เขียนอย่างไรครับท่าน กำลังเขียน |
||
[Flag IO ใช้ยังไง] |
บ่อยครั้งที่เราสับสนในการเรียกแฟลกIO นำแฟลกในตัวแปรไอโอมาใช้งาน การให้ค่าโดยตรงผ่านแฟลกIOสามารถทำได้ไหม หรือการเปรียบเทียบทางโลจิก เช่น ในSREG เรารู้ว่า Carry หรือ C หรือ SREG_C อยู่ในบิทที่0ของรีจิสเตอร์SREG ซึ่งมีค่าไม่0ก็1 แต่เมื่อเราจะเอามาใช้อย่างง่ายๆให้มันเหมือนกับเป็นตัวแปรlogicกลายเป็นสิ่งที่ทำไม่ได้ง่ายๆ ตัวอย่างเช่น if (SREG_C) { } // จะคอมไพล์ไม่ผ่าน เราไม่สามารถให้ค่าโดยตรงได้ เช่น SREG_C =1; // คอมไพล์ไม่ผ่านเพราะอะไร [เราจะใช้Flag IO อย่างไร] |
|
..ต่อ Flag IO ใช้อย่างไร ภาษาGCC ,C ถ้าเราสังเกตดูจะพบว่า SREG ไม่ใช่ตัวแปรในCแต่เรียกเสมือนว่าเป็นตัวแปรIO และ SREG_C ไม่ได้ถูกประกาศให้ เป็นตัวแปรlogic ก็จะไม่สามารถใช้อย่างโลจิกได้เลย ซึ่งกระบวนการทำงานที่เป็นภาษาแมคโครแท้จริง จะมีส่งผ่านฟังก์ชั่นที่มีSREGจาก common.h เข้าไป ไฟล์sfr_defs.h แล้วมันจะส่งค่า io address 1 ไบท์ออกมาเท่านั้น -ในที่นี้ SREG ถูก define ดังนี้ -ในที่นี้ Atmega328p มีค่า AVR_ARCH=5 ที่มา [GNU,การใช้tool และ AVR_ARCH] # define SREG _SFR_IO8(0x3F) //ใน common.h ส่งฟังก์ชั่นออกไป # define SREG_C (0) //ใน common.h ค่าเท่ากับ0 # define __SFR_OFFSET 0x20 # define _SFR_IO8(io_addr) ((io_addr) + __SFR_OFFSET) # define _SFR_IO8(io_addr) _MMIO_BYTE((io_addr) + __SFR_OFFSET) # define _MMIO_BYTE(mem_addr) (*(volatile uint8_t *)(mem_addr)) [..คิดว่า ^4อันหลังมาจาก sfr_defs.h ซึ่งทำหน้าที่ให้ค่าที่อยู่เป็นตัวเลข unit8_t ออกมาแทนคำ SREG .. คือผมไม่เข้าใจภาษาเบื้องหลังเหล่านี้เท่าไหร่นักและหาผู้อธิบายไม่ได้] ที่มาหรือลิงค์ที่น่าสนใจ [avrfreaks: How does the macro MMIO_WORD() actually work ] [site:garretlab , _MMIO_BYTE()] [arduino : Trick for getting PORT / SFR_IO address ,ยังไม่ได้ทดลองผล] ฉะนั้นแล้ว SREG_C เวลาใช้งานในทางโลจิกอาจต้องใช้งานอ้อมๆผ่าน ตัวแปรIO เช่น if (SREG &(1<<SREG_C)) { } //คอมไพล์ผ่าน ------------------------------------------------------ IN R0,0x3F //โหลดค่าR0จากที่อยู่ SREG(0x3F) SBRC R0,0 // ถ้าบิทที่0ในR0=1 ให้ทำงาน{} ไม่งั้นก็กระโดดข้าม{} [^ในความเป็นจริงถ้าให้ง่ายและสั้น GCCควรใช้ คำสั่ง BBRC ซึ่งสามารถตรวจสอบ SREG_C แล้ว กระโดดข้ามได้ทันทีที่=0 ซึ่งเข้าประโยคเงื่อนไข if ในกรณีนี้จะสั้นและไม่ต้องโหลดข้อมูลเข้าR0 แต่เนื่องจากวิธีคิดของGCC เป็นแบบองค์รวมคือใช้ได้หมดทุกตัวแปร การแปลแบบนี้ก็ถือว่ายังทำได้ดี ] การใช้บิทในIO หรือบิทในตัวแปรVolatile อื่น ในเงื่อนไขลอจิก [ถ้าไม่เข้าใจตัวแปรVolatile ให้ข้ามไปอ่านเรื่อง การประกาศตัวแปรกับการจองพื้นที่ในเมมโมรี่ และเบื้องหลังการทำงาน ให้เข้าใจเสียก่อน ] การใช้งานในลักษณะการใช้บิทหนึ่งๆในไบท์IO หรือตัวแปรVolatileชนิดโกบอลและชนิดstatic (ตัวแปร1ไบท์ เช่นชนิด volatile unsigned charเป็นต้น) มาเป็นเงื่อนไขทางโลจิก ใน keyword ประเภท if() ,switch() หรืออื่นๆเป็นต้น ต้องเข้าใจการเขียนและตรวจสอบตามนิยามไวยากรณ์ในภาษาC ดังนี้ ------------------------------------------------------- ลอจิกหรือเงื่อนไขการตรวจสอบบิทหนึ่งๆในไบท์เท่ากับ1หรือไม่ เช่น SREG (IO register), volatile unsigned char i;(i ,ประกาศในหรือนอกฟังก์ชั่นก็ได้ เป็นโกลบอลหรือสแตติก หรือตัวแปรธรรมดาในฟังก์ชั่นที่อิงกับIOรีจิสเตอร์ ก็ได้ แต่ถ้าเป็นตัวแปรธรรมดาที่ไม่ใช้volatileจะให้แค่ค่าแต่ไม่ได้มีกระบวนการจริง ) เป็นต้น ให้ b เป็น ค่าบิท ที่มีค่าอยู่ระหว่าง 0-7 ประกาศเป็น unsigned char b;เป็นตัวแปรโลคอลทั่วไป 1 ในif() วงเล็บลอจิกนี้ บิทb ในไบท์ i นั้นเป็น <>0 , จริง(1) , true(1) หรือไม่ 2 ในif() วงเล็บลอจิกนี้ บิทb ในไบท์ i นั้นเป็น 0 , เท็จ(0) , false(0) หรือไม่ สมมติให้ b=3; i=0xFF; ไวยากรณ์การเขียนสำหรับตรวจสอบบิทในไบท์ว่าจริงหรือเท็จเขียนได้ดังนี้ if( i & (1<<b)) หรือ if(SREG & (1<<b)) if( i & (1<<3)) หรือ if(SREG & (1<<3)) [เราต้องรู้ความหมายเองว่า บิทที่3ของSREG หรือของตัวแปรอื่นๆ เอาไปใช้ทำอะไร] if( มีความหมายว่า ให้ตรวจสอบค่าในวงเล็บว่า จริงหรือไม่ ,<>0 หรือไม่ โดยการเอาค่าใน i มา & กับค่า (0b0000 0001 โดยวนบิทไปทางซ้าย3ครั้ง)) (0b0000 0010 วนไปซ้ายครั้งที่1) (0b0000 0100 วนไปซ้ายครั้งที่2) if( ตรวจสอบ (i= 0b1111 1111) & (0b0000 1000 วนไปซ้ายครั้งที่3)) if( ตรวจสอบการ&ถ้าไม่เท่ากับ0แสดงว่าจริง ในที่ได้(0b0000 1000) ซึ่งไม่เท่ากับ0) -------------------------------------------- ----------------------------------------- ลอจิกหรือเงื่อนไขการตรวจสอบหลายๆบิทใน1ไบท์IO หรือ 1ไบท์Volatile ก็ทำในทำนองเดียวกัน กับที่ผ่านๆมาซึ่งจะให้ค่าแบบสั้นหรือแบบยาวก็ได้ตามแต่สะดวก แต่นิยามยังเหมือนเดิมคือ if(เงื่อนไข) { } 1 เงื่อนไขต้องมีค่า <>0 ,จริง ,true(1) จะทำในวงเล็บ 2 เงื่อนไขวงเล็บ = 0 ,เท็จ ,false(0) จะข้ามวงเล็บ volatile unsigned char i; ตย. if(EEDR & 0b10001000) หรือ if( i &((1<<7)|(1<<3))) ซึ่งแบบด้านซ้ายจะง่ายกว่าและดูไม่ซับซ้อน ------------------------------------------------- เอามาลองเขียนดู เมื่อแปลงเป็น ASM ----------------------------------------------------------------------- ตย.นี้แสดงการทำงานในวงเล็บ if() เท่านั้น ซึ่งเอาไปใช้ตรวจสอบบิทไม่ได้ เพราะ SREG เปลี่ยนแปลงตลอดเวลาที่เรากระทำการตรวจสอบโลจิกในตัวอย่าง int main(void) SREG = 0b0000 0000 { SREG = 0xff; 40 SER R24 0000 0000 ให้ค่า0xff แก่R24 41 OUT 0x3F,R24 1111 1111 เก็บค่าใน SREG(addr0x3F) SREG =(SREG)&(1<<3); 42 IN R24,0x3F 1111 1111 ให้ค่าR24จาก SREG addr 43 ANDI R24,0x08 1110 0001 R24=0xFF & 0b0000 1000 44 OUT 0x3F,R24 1000 1000 เซฟลงใน SREG if (SREG) 45 IN R24,0x3F 1000 1000 โหลดSREG ลง R24 ^R24 ที่ได้มากลับเท่ากับ 0x08 =0b00001000 46 TST R24 0000 1000 R24=0x08 & 0x08 ^ ทดสอบดูว่า R24เท่ากับ0หรือไม่ ถ้าเท่าZ=1 47 BREQ PC+0x03 กระโดดไป 4A ถ้าZ=1 { PORTB |=(1<<PORTB5);} 48 SBI 0x05,5 ให้บิท5=1 PORTB 49 RJMP PC+0x0002 กระโดดไป 50 else asm("nop\n"); 4A NOP } 50 .... [^บรรทัดที่45 เมื่อโหลดค่าจาก SREG ลง R24 เมื่อทดลองในdebuger ของ atmel กลับพบข้อ ผิดพลาดคือR24 อ่านค่าจาก flag I หรือบิทที่7ของSREG เป็น0 ทุกครั้งโดยไม่ทราบสาเหตุ] -------------------------------------------------------------------------- ตย.การตรวจสอบบิทที่3 หรือ SREG_V ใน IO SREG ว่าเป็น1 หรือไม่แล้วกระทำการ... int main(void) { SREG = 0xff; 40 SER R24 ให้ค่า0xff แก่R24 41 OUT 0x3F,R24 เก็บค่าใน SREG(addr0x3F) if (SREG&(1<<3)) 42 IN R0,0x3F โหลดSREG ลง R0 ^ R0ในดีบักเกอร์=0b0111 1111 แต่ที่SREG=0xff 43 SBRS R0,3 ข้ามไป45 ถ้า บิทที่3ในR0=1 44 RJMP PC+0x0003 (บิท3ในR0=0) กระโดดไป 47 { PORTB |=(1<<PORTB5);} 45 SBI 0x05,5 ให้บิท5=1 PORTB 46 RJMP PC+0x0002 กระโดดไป 48 else asm("nop\n"); 47 NOP } 48 .... [^ทดสอบใน debuger ของ atmel พบว่า บรรทัด43 IN R0,0x3F ลองอ่านค่าในR0พบว่า ได้ค่า R0 = 0b 0111 1111 โดยไม่ทราบสาเหตุ แต่เมื่อลองเปลี่ยนจาก SREG ไปใช้ EEDR ไม่พบความผิดปรกติ ได้R0=0xffตามปรกติ ไว้จะนำไปลองกับ arduino IDE เพื่อตรวจสอบว่าผิดพลาดตรงไหน...] [ ได้ทดลองเพิ่มเติมกับ SREG ในdebuger atmel พบว่าสามารถให้ค่าโดยตรงกับ SREG_I หรือบิท บิทที่7ของSREG ได้โดยตรง SREG=0b10000000; หรือใช้ sei(); ก็ได้ซึ่งเป็นฟังก์ชันเอาไว้ เปิดโกบอลอินเตอรัพในภาษาC แต่เมื่อมีการอ่านค่าเข้าในรีจิสเตอร์คำนวนเช่นRx พบว่าไม่สามารถ อ่านค่าได้เฉพาะบิทที่7จะเป็น0ทุกครั้ง และเมื่อป้อน SREG=0; พบว่าค่าอื่นๆเท่ากับ0หมดยกเว้น บิทที่7 ถ้าจะเคลียร์ SREG_Iต้องใช้คำสั่ง cli();เท่านั้น ] =============================================== เนื่องจากการให้ค่าบิทใดๆให้เท่า0หรือ1 ในไบท์หนึ่ง และการอ่านค่าbitนั้นมาใช้นั้นเขียนค่อนข้างยาว มีผู้เขียนเป็นภาษาแมคโครเป็นในรูปแบบฟังก์ชั่นก็สามารถเขียนได้สั้นและใช้งานได้ดีเช่นกัน ทีมา--> [avrfreaks:Interrupts and global variables] มาเรียนรู้ภาษาแมคโครกัน เพิ่มโค๊ดเหล่านี้ลงไป //**** เมื่อใช้งานกับ IO register จะเทียบเท่ากับตัวแปรvolatileชนิดโกบอลและสแตติก คือทุกบรรทัดที่GCCแปลจะให้ความสำคัญกับวลีทุกคำและแปลตามลำดับ แม้แต่บรรทัด ที่ติดกับบรรทัดที่มีตัวแปรvolatileนี้หรือตัวแปรIOนี้ก็จะถูกแปลเรียงลำดับเช่นกัน // **** แต่ถ้าใช้งานกับตัวแปรโกบอลหรือสแตติกเฉยๆ จะถูกรวบรัดและสรุปการแปล โดยไม่ให้ความสำคัญกับลำดับหรือวลีใด และทำการเรียงสับเปลี่ยนให้เหมาะสม ที่สำคัญคือผลลัพธ์ได้ถูกต้องก็พอ #define BIT(x) (0x01 << (x)) #define setBit(p,m) ((p) |= BIT((m))) #define clearBit(p,m) ((p) &= ~BIT((m))) #define getBit(p,m) ((p) & BIT((m))) unsigned char test=0; int main(void) { setBit(PORTB,0); 48 SBI 0x05,0 EEDR=0xff; 49 SER R24 4A OUT 0x20,R24 clearBit (EEDR,0); IN R24,0x20 4C ANDI R24,0xFE 4D OUT 0x20,R24 asm("nop\n"); 4E NOP setBit (test,7); setBit (test,1); setBit (test,0); 4F NOP setBit (test,6); setBit (test,5); setBit (test,4); 50 LDI R24,0xE0 asm("nop\n"); test&=0b11111110; 51 STS 0x0100,R24 test&=0b11111101; test&=0b11111011; 53 NOP test&=0b11110111; test&=0b11101111; asm("nop\n"); } กำลังเขียน |
||
b= | [ค่าเริ่มต้นของ C]-atmel เมื่อยังไม่ได้พิมพ์อะไรเลยในmain() | 0000 JMP 0x0034 กระโดดออกจากพื้นที่ฟ้องอินเตอรัพ -------เคลียร์ SREG------------------------------------ 0034 CLR R1 R0←0x00 0035 OUT 0x3F,R1 Out I/O SREG←R0 -------ให้ค่า SP = 0x08FF ตำแหน่งบนสุดของแรม-------- 0036 SER R28 R28←0xFF 0037 LDI R29,0x08 R29←0x08 0038 OUT 0x3E,R29 Out I/O SPH←R29 0039 OUT 0x3D,R28 SPL←R28 |
5. | [เขต,ชนิด หน่วยความจำ] | |
6 | ชนิดหน่วยความจำและการกำหนดเขต ในGCC ในคู่มือ avr-libC มีการกำหนดเขตเมมโมรี่ [คิดว่า-โดยใช้ฟังก์ชั่น malloc()] ที่จะต้องจัดสรร โดยการแบ่งชนิดหน่วยความจำออกเป็นหลายๆจำพวกอย่างชัดเจนเพื่อให้เป็นไปตามหลักเกณฑ์ในภาษาC และไม่ให้เกิดการชนกันในหน่วยความจำเฉพาะMCUในตระกูลavr ชนิดหน่วยความจำ เช่น - หน่วยความจำที่เกิดจาก ตัวแปรที่กำหนดค่าเริ่มต้น(Initialized variable) .data variables - หน่วยความจำ ตัวแปรพวกที่ไม่กำหนดค่าเริ่มต้น(Uinintialize variable) .bss variables - หน่วยความจำฮีป(Heap) หรือ dynamic memory - และหน่วยความจำสแตก(Stack) ![]() ข้อมูลต่อไปนี้บางส่วนอ่านและสรุปมาจากลิงค์ต่อไปนี้ ให้ไปศึกษาเพิ่มเติมนะครับ [wiki:Uninitialized variable] , [stackoverflow:What is a Memory Heap?] , [บทความ C ,CBootCamp,Memory : Stack vs Heap] , [youtube:Pointers and dynamic memory - stack vs heap] , [youtube:Dynamic memory allocation in C - malloc calloc realloc free] ตัวอย่างการประกาศตัวแปรที่ไม่กำหนดค่าเริ่มต้น>> [avrfreak:static variable] [คำนิยาม mem section, Avr libc] 1. หน่วยความจำที่เกิดจากตัวแปรที่กำหนดค่าเริ่มต้น เช่น ตัวแปรที่กำหนดค่าแล้วในการประกาศตัวแปรชนิดstaticทั้งแบบโลคอลหรือแบบโกลบอลและมีการทำงานจะเกิดการบันทึกในflash program(progmem) และจะถูกจองเมมโมรี่ในส่วน.data section และถือว่าเป็นตัวแปรเมมโมรี่ทุกครั้งที่มีการเปลี่ยนค่าจะถูกยัดค่าใหม่เก็บกลับเข้าที่เดิมเสมอ เรียกตัวแปรเหล่านี้ว่า.data variables static unsigned char xx =4; // การให้ค่าแล้วและเท่า4 ถือว่าเป็น .data var // จะเป็นตัวแปรในเมมโมรี่ ที่อยู่เริ่มต้น =0x100 2. หน่วยความจำที่เกิดจากตัวแปรที่ไม่กำหนดค่าเริ่มต้น เช่นตัวแปรทีไม่กำหนดค่าที่เป็นตัวแปรโกบอลหรือตัวแปรสแตติก (Uninitialized global or static variables) ตัวแปรเหล่านี้จะถูกจองพื้นที่ในส่วนที่เรียกว่า .bss section ตัวแปรเรียกเหล่านี้อีกอย่างว่า .bss variable และถูกให้ค่าเป็น0ในตัวแปรเหล่านั้น 3. หน่วยความจำฮีป(Heap)หรือหน่วยความจำไดนามิก (Dynamic memory) [คิดว่า....เป็นหน่วยความจำที่เป็นอิสระเป็นเมมว่างๆสำหรับMCUใช้สอย โดยจะถูกสร้างตัวแปรต่างๆและจองเมมโมรี่จากMCUเองในภายหลังเมื่อทำงานตามโปรแกรม ซึ่งการประกาศใช้มักจะใช้กับตัวแปรอารเรย์หรือตัวแปรstructและควบคู่กับตัวแปรพ้อยเตอร์ โดยเมื่อใช้งานตามโปรแกรมแล้วไม่ได้ใช้อีกก็สามารถลบการจองหน่วยความจำนั้นไว้ การจองพื้นที่เป็นไปโดยอัตโนมัติและไม่สามารถระบุaddressที่ต้องการจองได้ และหน่วยความจำอาจไม่ต่อเนื่องกันไปจากล่างขึ้นบนแต่มีวิธีการคำนวนเฉพาะทางแล้วให้ค่าที่อยู่เริ่มต้นสำหรับการจอง การจองหรือสร้างหน่วยความจำตัวแปรประเภทนี้จะใช้ฟังก์ชั่นจำพวก malloc() calloc() realloc() และ free() ซึ่งโดยทั่วไปกูรูแนะนำว่าถ้าไม่ได้มีปัญหาเกี่ยวกับตัวแปรที่ต้องการประกาศใช้และลบออกแล้วประกาศใหม่และรู้จำนวนข้อมูลที่แน่นอนเขาแนะนำให้ใช้ตัวแปรโกบอลหรือstaticจะเหมาะสมกว่า เพราะพวกนี้ต้องรู้ปัญหาของตัวแปรไดนามิก การประกาศใช้ การลบออกที่ถูกต้องเพื่อป้องกันการชนกันหรือป้องกันว่าไม่มีเมมพอเพียงให้ใช้ ซึ่งมีความยุ่งยากมากสำหรับมือใหม่และไมโครคอนโทรลเลอร์ขนาดเล็กที่มีSRAMเล็กๆไม่ควรใช้หรือหาใช้ได้น้อยมาก ลิงค์ที่พวกกูรูเขาคุยกัน avrfreaks:Why using malloc? ....] โดยดีฟอลท์จะถูกกำหนดค่าที่อยู่เริ่มต้นต่อจากส่วน.bss แล้วค่อยๆเพิ่มขึ้นไปหา0x8FF แต่ก่อนใช้งานตัวแปรไดนามิกที่อยู่อยู่ในฮีปต้องกำหนดเขตหรือขีตเส้นด้วยคำสั่ง... เพื่อขีดเส้นเมมโมรี่ส่วนฮีปไม่ให้ไปชนกับพื้นที่stack [เข้าใจว่า....คงมีโค๊ดเพิ่มในMCUเพื่อระบุเขต ,การคิดคำนวนและการจองเมม(เมื่อมีการประกาศใช้ตัวแปร),การให้ค่าเริ่มต้น ,การลบ ส่วนที่ยากคือการคิดคำนวนตำแหน่งเมมแล้วคืนค่าที่อยู่แรกให้พ้อยเตอร์ ยิ่งถ้ามีตัวแปรไดนามิกเยอะๆและมีขนาดเกือบเท่ากับพื้นที่ฮีปจะยิ่งใช้งานยากและเกิดการไม่มีแรมใช้สอย หรือเกิดเมมโมรี่ลีกส์ในกรณีที่เขียนโปรแกรมไม่ดีมีการลบไม่ดีทำให้ซ้อนกันมีข้อผิดพลาด....] 4. หน่วยความจำสแตก(Stack) ซึ่งใช้ในการเก็บค่าที่อยู่ของพ้อยเตอร์เดิมก่อนกระโดดเข้าโปรแกรมย่อย(เก็บทีละ2ไบท์ ในASMจะทำการเก็บค่าเข้าสแต็กอัตโนมัติเมื่อมีการใช้คำสั่งเรียกโปรแกรมย่อยประเภทCALLทั้งหลาย และคืนค่าให้โดยคำสั่งRET) และเก็บค่า local (automatic) variables (ในภาษาasmใช้คำสั่ง push ,pop ในการเก็บค่าตัวแปรโลคอล) ซึ่งส่วนใหญ่กำหนดค่าเริ่มต้นของstackอยู่บนสุดของแรมในที่นี้คือ0x8FF แล้วค่อยๆเลื่อนลงมาทีละ2ไบท์หรือ1ไบท์ |
|
7 | เกี่ยวกับ volatile variable http://www.avrfreaks.net/forum/whats-volatile-variable http://www.avrfreaks.net/comment/121830#comment-121830 |
|
[การประกาศตัวแปรกับการจองพื้นที่ในเมมโมรี่ และเบื้องหลังการทำงาน] เมื่อทดลองคอมไพล์Cจากตัวแปรเหล่านี้ออกมาพบว่า 1. ประกาศตัวแปรโลคอล หรือประกาศตัวแปรในฟังก์ชั่น main() หรือฟังก์ชั่นย่อยอื่น จะไม่ถูกจองพื้นที่อยู่ในเมมโมรี่ ,ไม่ได้เป็นตัวแปรเมมโมรี่ และไม่ถูกเก็บค่าเอาไว้ใช้งานต่อไป เช่น ประกาศ unsigned char xx =5; ถ้าไม่ได้นำไปใช้งานอาจได้รับคำเตือน หรือไม่ก็ได้ (unused variable 'xx' ) แต่จะไม่ปรากฎโค๊ดใดๆเกี่ยวกับตัวมันหรือค่าของมันหลังคอมไพล์ คำว่าไม่ได้ใช้งานในข้อ1นี้ มีความหมายว่าถ้าไม่ได้นำตัวแปรxxไปใช้กับIOหรือแม้จะมีบรรทัดคำนวน แต่ไม่ได้มีผลอะไรที่จะสั่งไมโครคอนโทรลเลอร์ถือว่าไม่ได้ใข้งานจะถูกตัดทิ้งทั้งหมด 1.1 void main (void) { unsigned char xx =4;} //ไม่ได้ใช้งาน 1.2 unsigned char xx =4; xx=xx+1; //ไม่ได้ใช้งาน 1.3 unsigned char xx ; xx=4; xx=xx+1; PORTB=5; // ไม่ได้ใช้งาน 1.4 unsigned char xx =4; xx=xx+1; PORTB=xx; //xx ได้ใช้งานแต่ โค๊ดเป็น LDI R24 , 0x04 //R24←0x04 จะเห็นว่าค่าในxxถูกใช้งาน แต่xxไม่ ไม่ได้ถูกจองในแรมใดๆ ใช้แต่ค่าของมันเท่านั้น OUT 0x05 , R24 //PORTB←R24 [คิดว่า แสดงว่า GCC ฉลาดไม่ได้จองหรือสร้างตัวแปรในเมมโมรี่ซี้ซั้ว ซึ่งตัวแปรโลคอลทั่วไปจะถูกใช้คำนวนทันที ไม่ได้เก็บเป็นตัวแปรเมมโมรี่ สิ่งใดแทนค่าเป็นตัวเลขแล้วยัดเข้าไปในIOregได้เลยมันจะหาinstructrionเหมาะๆสั้นๆใส่ให้ ข้อสังเกตอีกอย่างGCCไม่ได้สร้างชุดคำสั่งตามลำดับบรรทัดของภาษาCแต่มีลำดับวิธีคิดที่จะทำให้สร้างชุดคำสั่งที่สั้นลงและไม่เยิ่นเย้อมากนักโดยส่วนใหญ่] ----------------------------------------------------------------------- 2. ประกาศตัวแปรโกบอล (ตัวแปรนอกฟังก์ชั่นและอยู่ในส่วนหัว โดยเป็นตัวแปรที่ใช้งานได้ทุกที่ในโปรแกรม) และถ้าคอมไพลเลอร์ตรวจพบว่ามีการใช้งาน(ดูข้อ3) มีผลทำให้เกิดการจองพื้นที่ในหน่วยความจำทันที โดยตัวแปรถ้ามีการกำหนดค่าเริ่มต้น(Initialized variable)จะเก็บไว้ในส่วนพื้นที่พวกกลุ่มตัวแปรdata แต่ถ้าไม่ได้มีการกำหนดค่าเริ่มต้น(Unitialized variable)การจองเมมจะเกิดขึ้นในส่วนของbss section และถูกกำหนดให้ค่าเท่ากับ0แทนการไม่กำหนดค่า ต่อไปนี้คือการประกาศตัวแปรโลก(Global variable) ซึ่งจะเกิดขึ้นนอกฟังก์ชั่นและ ก่อนmain() 2.1 unsigned char aa[2] ={1,2}; //.data memory addr =0x101-0x102 2.2 unsigned char bb[2] ; // .bss mem addr =0x105 3. ประกาศตัวแปร static ในฟังก์ชั่น(local) หรือ นอกฟังก์ชั่น (global) และถ้าคอมไพเลอร์ตรวจพบว่ามีการใช้งาน(ในข้อ2-3นี้ถ้ามีการกระทำใดๆกับตัวแปรมันเองโดยไม่ได้ยุงกับIOก็ตามถือว่าถูกใช้งานเพราะถือว่าต้องอ่านแล้วทำจากนั้นจึงเก็บค่าเข้าเมมที่ตำแหน่งเดิมเพื่อรอใช้งานต่อไป แต่ถ้าประกาศแล้วไม่ได้มีการใช้โอเปอเรตอร์ใดๆใช้ถือว่าไม่ได้ใช้งานจะถูกตัดทิ้ง) มีผลทำให้เกิดการจองพื้นที่หน่วยความจำทันที ซึ่งอาจอยู่ในส่วนของ .data section หรือ .bss ก็ได้แล้วแต่การกำหนดค่า ต่อไปนี่ถูกกำหนดค่าต่อจากข้อ2 ต่อไปนี้ประกาศ static ภายในฟังก์ชั่นย่อย 3.1 static unsigned char xx =4; //กำหนดค่า จะถูกเก็บใน .data section addr=0x100 3.2 static unsigned char yy; //Unitialized var เก็บใน .bss section addr=0x104 [จะเห็นว่าเมื่อตรวจดูตัวแปรจริงใน .data section ที่เราประกาศ2.1และ3.1 จะมีแค่3ตัว แต่ที่เก็บใน.data เมื่อคอมไพล์Cจะฟ้องว่าเท่ากับ4ไบท์ ซึ่งตัวคอมไพล์จะเพิ่มค่าให้เองเป็น4ไบท์ เท่าที่ทดลองดูถ้าเป็นchar และประกาศตัวแปรได้จำนวนตัวแปรเป็นเลขคี่ ตัวคอมไพล์จะเพิ่มไบท์อีกตัวในprogram memให้อัตโนมัติและให้ค่าเป็น0 แล้วจึงเริ่มพื้นที่ .bss section ต่อไป] ใน ASM code จะอธิบายหลักการของการโหลดข้อมูลตัวแปรเข้าแรมสรุปได้ดังนี้ การโหลดข้อมูลชุด.data 4ไบท์เข้าแรม จะใช้พ้อยเตอร์Zชี้ที่โปรแกรมflashโหลดข้อมูลจากflash แล้วใช้พ้อยเตอร์Xชี่ไปที่แรมโดยเริ่มจาก0x100แล้วstoreข้อมูลจากZไปยังX=0x100 ส่วนข้อมูล.bss บนprogram flash จะไม่มีการบันทึกใดๆไว้เลยจึงไม่ได้ใช้พ้อยเตอร์Zอ่านข้อมูลแ ต่ส่งข้อมูล0เข้าไปในแรมเลยเป็นจำนวน3ไบท์ด้วยพ้อยเตอร์Xแทน unsigned char aa[2] ={1,2}; unsigned char bb[2] ; void lp1(void){ static unsigned char xx =4; static unsigned char yy ; xx++; yy=yy+0xA; for(unsigned char i =0;i<2;i++) { aa[i]=aa[i]+5; bb[i]=bb[i]+8; } } // เมื่อคอมไพลพบว่ามีการใช้เมมเพื่อประกาศตัวแปร .data=4byte , .bss=3 ไบท์ ต่อไปนี้คือASMยกเอาเฉพาะส่วนการโหลดข้อมูลเข้าตัวแปรในเมม ส่วน .data และ .bss ---การวนหลูปเพื่อโหลดข้อมูลเข้า .data section----------------- -0----กำหนดค่าเริ่มต้นของพ้อยเตอร์และตัวเปรียบค่า---- 003A LDI R17,0x01 R17 ←0x01 ,เอาไว้เปรียบเทียบค่า 003B LDI R26,0x00 พ้อยเตอร์X =0x100 =256 ,XL←0x00 003C LDI R27,0x01 (ชี้ที่แรม .data) ,XH←0x01 003D LDI R30,0x0C พ้อยเตอร์Z =0x10C =268 ,ZL ←0x0C 003E LDI R31,0x01 (ชี้ที่ flash program) ,ZH←0x10 003F RJMP PC+0x0003 Relative jump -1-----โหลดค่าจาก flash เข้า แรม ------------------------- 0040 LPM R0,Z+ R0←(Z) ,Z=Z+1 โหลดค่าจากโปรแกรมเมม 0041 ST X+,R0 (X)←R0, X=X+1 เก็บเอาไว้ในเมม.data 0042 CPI R26,0x04 เปรียบเทียบค่า R26-0x04 เพื่อหาดูว่ายืมตัวทดCมาหรือไม่ 0043 CPC R27,R17 ลบค่า R27-0x01-C เพื่อดูค่าZ=0 ปรากฎหรือไม่ 0044 BRNE PC-0x04 ถ้าค่าที่ลบกันยังไม่เท่ากับ0 ให้กระโดดไป0040 // สรุปทำอย่างนี้ 4 รอบ เริ่ม0x100 จบลงที่ เมมaddr 0x103 ----หลูปโหลดข้อมูล 0 เข้า .bss section โดยใช้ พ้อยเตอร์ X อย่างเดียว------------ -3-----ค่าเริ่มต้น-------------- 0045 LDI R18,0x01 Load immediate R18 ←0x01 0046 LDI R26,0x04 Load immediate X←0x104 ,XL←0x04 0047 LDI R27,0x01 Load immediate ,XH←0x01 0048 RJMP PC+0x0002 Relative jump -4-----โหลดค่า0 เข้าแรม-----โดยที่เดิม R1=0 อยู่แล้ว ----- 0049 ST X+,R1 Store indirect and postincrement (X)←R1, X=X+1 004A CPI R26,0x07 Compare with immediate ,flagC from R26-0x07 004B CPC R27,R18 Compare with carry , flagZ from R27-R18-C 004C BRNE PC-0x03 Branch if not equal , โดดไป0049 ถ้า Z<>1 // ทำอย่างนี้ 3 รอบ เพื่อโหลด0 เข้า .bss เริ่มที่ 0x104 ****พบว่า ตำแหน่งหรือ addressในแรมที่GCCจองให้สำหรับตัวแปรจะมีค่าเปลี่ยน แปลงไปตามขนาดและชนิด เราไม่สามารถกำหนดว่า ที่อยู่นี้ของเป็นตัวแปร ตัวนี้ชื่อนี้ และที่อยู่ถัดไปขอเป็นตัวแปรอีกตัว อันเป็นคุณลักษณะของภาษาC ที่ตัวคอมไพล์จัดสรรที่อยู่ของแรมแต่ละตัวแปรที่เราประกาศไปโดยอัตโนมัติ**** [คิดว่า...เราไม่สามารถกำหนดได้เลยหรือว่า แรมตำแหน่งนี้ หรือรีจิสเตอร์คำนวนตัวนี้ อยากผูกกับตัวแปรนี้เท่านั้น เพราะเท่าที่ทราบพวก R0-R25 เมื่อนำมาใช้กับการ อินเตอรัพที่ต้องการความเร็วซึ่งเป็นสิ่งจำเป็นมาก เพราะเรากำลังถูกขัดจังหวะ การทำงานที่ค้างอยู่ซึ่งบางขณะรอได้อย่างมากไม่เกิน2-3 cycle น่าจะมีวิธีนะครับผมว่า ขอขุดกันต่อไป ...] [Stackoverflow :How can I bind a variable to an unused AVR I/O register using avr-gcc?] --------------------------------------------------------------------------- 4. ประกาศตัวแปรโกบอลรีจิสเตอร์ Register variable ที่ผูกกับรีจิสเตอร์ตัวใดตัวหนึ่ง จะไม่มีผลกับการจองเมมช่วงบนทั้งในส่วน .data หรือ.bss โดยต้องประกาศนอกฟังก์ชั่นเท่านั้นและต้องไม่กำหนดค่าเริ่มต้น การผูกRกับตัวแปรทำให้สามารถประมวลและคำนวนได้เร็วขึ้นมาก ตย.ต่อไปนี้ไม่สามารถใช้ได้ในGCC เวอชั่นนี้ register char i; // คอมไพล์ไม่ผ่าน ขึ้น register not specificed for i register char i=0; // คอมไพล์ไม่ผ่าน ขึ้น register not specificed for i register char i asm('r3')=5; //ไม่ผ่าน globle register var has initial value หลักการจากGCCคือต้องประกาศเป็นตัวแปรโกบอลเท่านั้นไม่ประกอบกับstatic เมื่อมีการผูกกับตัวแปรแล้วจะไม่มีการจองเมมในที่อื่นและมันจะเป็นตัวแปรชนิดUnitialized varที่ไม่ใส่ค่า0ให้อีกด้วย รีจิสเตอร์ไม่สามารถเซฟและรีสโตร์ค่าได้โดยฟังก์ชั่นใดๆ และมันจะถูกผูกอยู่ตลอดไปแม้ไม่ได้ใช้อีก ประกาศตัวแปร Register อย่างไรให้ผูกกับรีจิสเตอร์คำนวนที่ต้องการ เนื่องจากเป็นมือใหม่ ก็เลยมุ่งค้นคว้าดังนี้ 1 [site:Atmel, How to permanently bind a variable to a register?] 2 [site:Atmel, C Names Used in Assembler Code] 3 [GNU,คำจำกัดความในการประกาศตัวแปรรีจิสเตอร์ Defining Global Register Variables] 4 [arduino,"register" keyword doesn't seem to reserve a register for the variable] 5 [GNU , การใช้งานรีจิสเตอร์คำนวนR0-R31ของGCC , Register layout] 6 [Avrfreaks,avr-gcc produces awful code when using global register variables] 7 [avrfreaks, Binding a variable to a register] 8 [avrfreaks, ปัญหาใช้ไม่ถูก Defining Global Register Variables] /*atmel บอกให้ประกาศดังนี้ แล้วจะสามารถผูกตัวแปร counter กับR3 ได้ และแนะนำให้ใช้ R2-R7 จะปลอดภัยกว่า ไม่ควรใช้ R8-R15 อันนี้ก็ขึ้นกับ GCC ว่ามันจะเลือกRใดไปใช้ สรุปคือมีวิธีแต่ยังไม่ฟันธงว่าGCCต้องทำตาม ถ้าต้องการให้ปลอดภัยจริงๆต้องตรวจดูหลังคอมไพล์แล้วว่าตัวแปรRนั้นไม่ ได้ถูกใช้กับโค๊ดอื่นๆในฟังก์ชั่น ถ้าถูกใช้อาจต้องแซมด้วย push pop คือ save ,restore ด้วยภาษา asm */ register unsigned char counter asm("r3"); //asm เป็น keyword ในC++ /* แต่ในหลักภาษา C การประกาศตัวแปรเช่น register char i; เป็นการสั่งให้คอมไพล์C ให้ตัวแปรนั้นเป็นรีจิสเตอร์แต่ไม่ระบุว่าRตัวไหน */ asm volatile("clr r3"); // สามารถ เคลียร์ค่าR3 ด้วยภาษาinline asm ได้ เราสามารถแทรกโค๊ดASMใดๆผ่านหัว preprocessor ได้ โดยประกาศภาษาแมคโคร ในรูปแบบฟังก์ชั่นได้ แล้วสามารถนำฟังก์ชั่นนั้นไปใช้แทรกในระหว่างC code ได้เลย เช่น #define nop() asm("nop\n nop\n nop\n nop\n") #define push_r3() asm("push r3\n") #define pop_r3() asm("pop r3\n") // ในที่นี้เมื่อพิมพ์ภาษาC เช่น register unsigned char counter asm("r3"); ผูกcounter เข้า กับR3 int main(void) // เมื่อแปลงภาษาเป็น ASM พบว่า { nop(); // NOP NOP NOP NOP counter=5; LDI R24,0x05 ให้5แก่R24 MOV R3, R24 ให้ค่า R24 กับR3 ^จะเห็นได้ว่า GCC ทำR3เหมือนตัวแปรทั่วไป ซึงหลักการคิดของGCC แต่ถ้าเป็นพวกเราคิดก็ควรให้ค่า 5 แก่ R3 เลยจะบรรทัดเดียวจบ ****แต่ LDI หรือ Lood immediate ใช้ได้แค่ R16-R30เท่านั้น** [วิธี] ฉะนั้นถ้าผูกลองกับR16 GCC จะแปลงเป็น LDI R16,0x05 push_r3(); PUSH R3 เซฟR3=5 บน stack asm volatile("clr r3"); CLR R3 ให้ค่า0 แก่ R3 :R3จะ=0 // หรือเพิ่มโค๊ดอื่นๆ .... ............ ........ pop_r3(); POP R3 คืนค่าสแตกแก่R3 :R3=5 } [ปัญหาคือเราต้องเข้าใจภาษา asm ก่อนใช้งาน ไม่งั้นวุ่นวายตายเลย เช่นการเซฟข้อมูลR3ลงstack หรือคืนค่าให้R3 ต้องเข้าใจการใช้งาน ของstack คือ LIFO-last in first out เข้าทีหลังต้องออกก่อน เหมือนยัดลูกเหล็กเข้าท่อที่ฝั่งตรงข้ามปิดตายจะเอาลูกเหล็กมาใช้ก็ต้อง เอาอันสุดท้ายที่เข้าออกมาใช้ก่อน เป็นต้น] ------------------------------------ ในการนี้ เราจะลองเปรียบเทียบให้เห็น ในบรรทัด counter=5; เพื่อให้เข้าใจ วิธีคิดของ GCC อีกครั้งหนึ่ง พบว่าเราบวกลบคูณหารอะไรมันก็จะ....... .... ก็ยังผูกตัวแปร counter กับ R3 เหมือนเดิม พบว่าเมื่อแปลงASM counter =5; // --ไม่มีASM แต่GCCคำนวนในใจว่าR3=5 counter =counter+ 5; -----" "-- GCC คำนวนในใจว่า R3=10 counter =counter*3; LDI R24,0x1E ให้ค่า30(1E)แก่ R24 MOV R3, R24 ให้R24 แก่ R3 ***วิธีคิดของGCC เมื่อเป็นตัวแปรทั่วไป มันจะสนใจที่จะให้ผลที่ลัดสั้นที่สุด เพื่อนำไปสั่งการกับIOregister หรือนำผลลัพธ์ ไปคำนวนกับตัวแปรอื่นต่อไป**** --------------------------------------------------------------- ***ถ้านำตัวอย่างข้างบนมาประกาศตัวแปรและเพิ่มคำว่า Volatile จะมีผลดังนี้ register volatile unsigned char counter asm("r16"); /* ^ warning optimization may eliminate reads and/or writes to register variables [-Wvolatile-register-var] */ [ตัวแปรVolatile ชนิด register เท่าที่ดูเขาคุยๆกันเหมือนจะไม่ใช้กันเพราะGCC เตือน จะเห็นตัวอย่างอย่างข้างล่าง แม้แต่ละบรรทัดจะมีการทำงานแยกกันแต่วิธีกลับลัดสั้น ไม่มีการบวก หรือคูณจริงๆให้เห็น แต่ให้ค่าลงไปเลย แต่ถ้าเป็นแปรVolatileโกบอลหรือstatic การบวกหรือคูณกันก็ยังมีขั้นตอนที่เพิ่มขึ้น ] int main(void) { counter=5; // LDI R16,0x05 ให้ค่า5 แก่ R16 nop(); // nop nop nop nop ไม่ต้องทำอะไร 4 cycle counter =counter+ 5; LDI R16,0x10 ให้ค่า10 แก่ R16 counter =counter*3; LDI R16,0x1E ให้ค่า30 แก่ R16 } จะเห็นว่า มีการเตือนแต่คอมไพล์ผ่าน ในที่นี้ใช้r16 ถ้ามันใช้งานได้จะได้โค๊ดด้านบน ข้อแตกต่างที่สำคัญคือ เมื่อใช้งานตัวแปรVolatileโกบอลหรือสแตติก GCCจะสร้าง โค๊ดที่ทำใกล้เคียงตามบรรทัดที่เราสั่ง แต่ถ้าเป็นVolatileรีจิสเตอร์โค๊ดจะสั้นที่สุดที่ตัวแปร นั้นสามารถจะทำเวลาได้โดยแปลทีละบรรทัดและต้องการผลลัพธ์ที่ตรงโดยไม่สนวิธีการ --(ในกรณีที่ผูกกับvolatile R16 คำสั่งส่วนใหญ่จะเป็นหนึ่งบรรทัด) --(ในกรณีที่ผูกกับvolatile R3 บางคำสั่งอาจเป็นสองบรรทัดแทน เหมือนตัวอย่างก่อนๆ) --(ในกรณีที่เป็นตัวแปรVolatile ที่จองในเมม คำสั่งอาจมีมากถึง3บรรทัด เป็นต้น) ในบางครั้งเราจำเป็นต้องใช้ตัวแปรชนิดนี้ เช่น ในกรณีเปลี่ยนค่าแฟลกIO รีจิสเตอร์ที่สำคัญๆ ที่มีผลเพื่อป้องกันความผิดพลาด จากการส่งข้อมูล MCUจะบังคับให้ผู้โปรแกรมต้องเปลี่ยนแฟลก2ครั้งภายในเวลา 4 cycle ถ้าเกินกว่านั้นฮาร์ดแวร์ก็จะไม่ทำงานตามที่สั่ง เป็นต้น 5. การประกาศตัวแปร volatile ตัวแปรที่เปลี่ยนค่าได้เองโดยฮาร์ดแวร์หรือเบื้องหลัง โดยปรกติจะประกาศVolatileกับตัวแปรโกบอลหรือสแตติกชนิดอื่นๆทั่วไปก็ได้ (จะมีผลจองแรม .data หรือ.bss) โดยคำว่าVolatile หมายถึงที่มีค่าเปลี่ยนแปลงได้เองโดยการทำงานเบื้องหลังของMCUโดยไม่เกี่ยวกับโปรแกรมที่มันกำลังทำงานทีละบรรทัด ตัวแปรvolatileเมื่อถูกสั่งให้ทำงานด้วยGCCมันจะทำงานตามใกล้เคียงตามขั้นตอนที่มีตัวแปรvolatileเหล่านั้นเป็นส่วนใหญ่ ทุกครั้งที่มีผลเปลี่ยนแปลงกับตัวแปรvolatileมันจะถูกเก็บค่าคืนให้เหมือนกับโปรแกรมที่เราเขียนไว้ทุกครั้งก่อนทำงานบรรทัดต่อไป ***เราไม่สามารถประกาศVolatileกับตัวแปร IO register ได้**** [คิดว่า..ดูจากการแปลงโค๊ดตัวแปร IO register ถือว่าเป็นตัวแปร Volatile อยู่แล้ว] [เข้าใจว่าGCCจะให้ความสำคัญกับการแปลโค๊ดตามลำดับของบรรทัดโปรแกรมที่มีตัวแปรvolatileและบรรทัดอื่นๆที่อยู่ติดกับบรรทัดที่มีตัวแปรvolatileทั้งบนและล่างด้วยเพื่อต้องการผลเปลี่ยนแปลงของเบื้องหลังให้เป็นไปอย่างที่คนเขียนโปรแกรมต้องการ เมื่อแปลงเป็นASMจะได้ขั้นตอนตามที่ต้องการ] ต่อไปนี้เป็นตัวอย่าง ตัวแปรโกลบอล volatile ทั่วไป ที่อยู่ใน .bss section volatile unsigned char counter ; int main(void) { counter =0b00010000; // LDI R24,0x10 ให้ค่า 0x10 แก่R24 STS 0x0100,R24 เก็บค่าR24 เข้าแรม 0x100 counter2 += 1; LDS R24,0x0100 โหลดแรม0x100 แก่ R24 SUBI R24,0xFF บวก1 (หรือลบด้วยFF) STS 0x0100,R24 เก็บเข้าแรมที่เดิม (R24=0x11) nopp(); nop nop nop nop counter |= 1; LDS R24,0x0100 โหลดจากแรม แก่R24 ORI R24,0x01 ยูเนี่ยนด้วย1 (R24=0x11) STS 0x0100,R24 เก็บเข้าแรม counter &= 0b00000001; LDS R24,0x0100 โหลดแรมที่เดิม ANDI R24,0x01 แอนด์ด้วย1 (R24=1) STS 0x0100,R24 เก็บ counter=6; LDI R24,0x06 STS 0x0100,R24 counter =counter*3; LDS R24,0x0100 R24=0b00000110 MOV R25,R24 LSL R25 R25=0b00001100 เลื่อนไปซ้าย ADD R24,R25 R24=0b00010010 บวกกันได้18 } STS 0x0100,R24 --------------------------------------------------------------------- 6. การประกาศตัวแปรอาเรย์โกบอลหรือstatic หรือค่าคงที่ที่เป็นอาเรย์กลุ่มข้อมูลชุดที่อยู่ติดกันและต่อเนื่องกันไป หรือการกำหนดค่าให้กับตัวแปรอาเรย์เป็นชุดๆต่อเนื่อง มีผลกับการจองหน่วยความจำดังนี้ 6.1 ตัวแปรอาเรย์ชนิดโกบอลหรือstatic ตอนประกาศตัวแปรถ้ามีการกำหนดค่าให้ตอนประกาศจะถูก จองหน่วยความจำในส่วน.data memoryยาวต่อเนื่องกันไปและข้อมูลค่าเริ่มต้นจะถูกเก็บไว้ใน ส่วนflash program แต่ถ้าไม่ได้กำหนดค่าก็จะถูกจองในเมมโมรี่ส่วนของ.bss memory 6.2 ประกาศอาเรย์ค่าคงที่ โดยเขียนCต้องการให้ค่าคงที่อาเรย์แก่ตัวแปรอาเรย์(6.1) 6.2.1 ให้ค่าตัวแปรอาเรย์บางส่วนหรือทั้งหมดแบบสุ่มไม่ได้เรียงตามลำดับกันไปด้วยหลูปfor หรือแม้ว่าแต่ละบรรทัดถูกเรียงตัวเลขอินเด็กซ์ตามๆกันไปถือว่าเป็นการให้ข้อมูลแยกกัน 6.2.2 ให้ค่าตัวแปรอาเรย์บางส่วนด้วยหลูปfor โดยไม่ครบจำนวนอินเด็กซ์ของตัวแปรอาเรย์ การให้ค่าชุดตัวแปรอาเรย์ด้วยชุดอาเรย์คงที่ด้วยวิธีดังกล่าวทั้งสองแบบ ถือว่า ชุดค่าคงที่อาเรย์ จะไม่มีนัยสำคัญของการใช้เมมโมรี่ และไม่มีการเก็บข้อมูลในflash อีกด้วย **การให้ค่าแบบนี้ GCC จะไม่จองเมมโมรี่ ในส่วนใดๆทั้งสิ้นและที่ประกาศค่าคงที่ ที่ต้องการให้เก็บไว้ในflashก็กลับไม่ได้เก็บเอาไว้อีกด้วย*** static char str [10]="1234567891"; //.data memory = 10 byte+progmem10byte const uint8_t DIGITS_7SEG[] = { 0b00111111, 0b00000110, 0b01011011, 0b01001111, 0b01100110, 0b01101101, 0b01111101, 0b00000111, 0b01111111, 0b01101111, 0b00111111, 0b00000110, 0b01011011, 0b01001111, 0b01100110, 0b01101101, 0b01111101, 0b00000111, 0b01111111, 0b01101111 }; ข้อมูลที่ต้องการบันทึกเก็บไว้ในส่วนโปรแกรมแฟลช เป็นจำนวน20byte? จะถูกเก็บหรือไม่? -6.2.1 ตัวอย่าง--------------------------------- int main(void) // EX1 ตามโค๊ดที่เขียนจะมีram+progmen=10+10 byte จาก str[] // การให้ค่าตัวแปรอาเรย์ด้วยค่าคงที่อารเรย์ที่ไม่มีนัยสำคัญทางหน่วยความจำ { str [0] = DIGITS_7SEG[0]; LDI R30,0x00 LDI R31,0x01 LDI R24,0x3F โหลดค่าคงที่เข้าR24 STD Z+0,R24 ให้ค่าR24 ที่เมม 0x100 (.data) str [1] = DIGITS_7SEG[1]; LDI R24,0x06 STD Z+1,R24 str [2] = DIGITS_7SEG[2]; LDI R24,0x5B STD Z+1,R24 str [3] = DIGITS_7SEG[3]; LDI R24,0x4F STD Z+1,R24 } //^ถ้าดูตามโค๊ดแสดงว่าข้อมูลDIGITS_7SEG[] ไม่ได้ถูกบันทึกลงในแฟลช เพราะการโหลดข้อมูลจาก flash ต้องใช้คำสั่ง LPM และใช้พ้อยเตอร์Z ประกอบ // ถ้าทำการให้ข้อมูลไปเรื่อยๆจนครบ 10 อีลีเม้นท์ พบว่า // *****ใช้โค๊ดไปถึง 44 ไบท์ทีเดียว****** -6.2.2 ตัวอย่าง--------------------------------------- int main (void) // โค๊ดต่อไปนี้ มีการให้ค่าชุดค่าคงที่แก่บางตัวแปรอาเรย์str บางส่วนด้วยforดังนี้ { for (int i = 0 ; i < 4 ; i++) // ให้ค่าอินเด็กซ์ 0-3 คือ 4 ค่า จาก10ค่าของ str[] str [i] = DIGITS_7SEG[i]; } //เมื่อแปลงเป็น ASM กลับได้โค๊ดยาวดังนี้ LDI R30,0x00 LDI R31,0x01 LDI R24,0x3F STD Z+0,R24 LDI R24,0x06 STD Z+1,R24 LDI R24,0x5B STD Z+2,R24 LDI R24,0x4F STD Z+3,R24 //^จะเห็นได้ว่าเป็นการให้ค่าคงทีโดยตรง ไม่ได้โหลดจากข้อมูลที่เราเก็บไว้มาใช้ //^ถ้าเราให้ i เป็น 9 จะไม่ครบ10อีลีเม้นของอาเรย์str ก็ยังคงเป็นเช่นเดิมครับ ... .... LDI R24,0x4F STD Z+8,R24 //สรุปใช้โค๊ดไป 40 ไบท์ถ้าi=0ถึง8 // ^ เป็นการให้ค่าโดยตรง ไม่ได้บันทึกค่าเอาไว้ในflash หรือ แรม ---------------------------------------- [ถ้าต้องการเขียนภาษาC ให้แปลเป็น asm โดยใช้พ้อยเตอร์Zชี้ที่คำสั่งLPM และใช้พ้อยเตอร์X ชี้ที่ เมมเริ่มที่0x100 แล้วลอกข้อมูลสัก10ไบท์จาก ทีZชี้ไปยังที่X ต้องทำอย่างไรใครรู้ช่วยบอกที *** ต่อไปนี้เป็นตัวอย่าง ASM แต่ หาวิธีเขียนจากC ไม่ได้ LDI R17 , 0x01 ให้R17=1 เอาไว้เปรียบเทียบค่าเมื่อครบ10ไบท์ LDI R26 , 0x00 LDI R27 , 0x01 ^ให้ค่าตำแหน่งram 0x100 แก่X [1c+1c] LDI R30 , 0xA1 LDI R31 , 0x00 ^ให้ค่าตำแหน่งข้อมูลโปรแกรม 0xA1 แก่Z [1+1c] RJMP st1 ตอนเริ่มให้กระโดดไปA1: lp2: LPM R0,Z+ โหลดflashมาเก็บที่R0 [3clock] ST X+,R0 เก็บค่าจากR0ไปไว้ที่mem [2clock] st1: CPI R26,0x0A เปรียบเทียบR26-0xA แล้วมีflagC(ตัวยืม)เกิดขึ้นหรือไม่ CPC R27,R17 R27-R17(0x01)-flagC(1หรือ0) ถ้าได้ทั้งหมด0 Z=1 BRNE lp2 กระโดดไปB2 ถ้า Z<>1 (คือR27ยังไม่เท่ากับ1จะไปB2) out3: out3 ในกรณีZ=1 เกิดขึ้นเมื่อ R26==10 และ R27==1 *******ใช้โค๊ดไป 22 byte********* ............คือหาวิธีเขียนด้วยภาษาCไม่ได้ ต้องทำอย่างไรดีครับท่าน ] [ เข้าใจว่า ภาษาGCC มีวิธีการประกาศค่าคงที่ให้เก็บไว้ในโปรแกรมเมมหรือแฟลชโปรแกรม ต้อง เรียกหรือใส่แอทริบิวท์ progmem ผ่าน ไฟล์เฮดเดอร์ที่GCC สร้างไว้ใน <avr/pgmspace.h> ไว้ผมจะค่อยๆขุดมันออกมาใช้ให้ได้ในภายหลัง ในการนี้เข้าใจสั้นๆว่า สามารถเก็บค่าคงที่ใน แฟลชโดยgccจะไม่ต้องจองเมมในส่วน.data หรือ .bss แต่อย่างใด] หาอ่านได้ที่ แล้วว่างๆมาขุดเนื้อหาต่อ 1 [avrfreaks :ติวเรื่อง [TUT] [C] GCC and the PROGMEM Attribute ] 2 [avrfreaks : How to store struct elements in program memory (flash)? ] 3 [avrfreaks : Getting Data out of a PROGMEM struct - w/ pointer ] ----------------------------------------------------------- 6.3 ประกาศค่าอาเรย์คงที่ ที่มีผลกับหน่วยความจำแรมและแฟลชโปรแกรม โดยที่main()มีการให้ค่าคงที่อาเรย์บางส่วนอย่างต่อเนื่อง(คัดลอกข้อมูล)แก่ตัวแปรอาเรย์(6.1) ทุกอินเด็กซ์ด้วยหลูปfor GCCจะบันทึกข้อมูลของค่าคงที่อาเรย์จำนวนมากเท่าที่เราต้องการ ให้เก็บในflash และGCCจะทำการจองพื้นที่อาเรย์คงที่เผื่อไว้โดยเขียนโค๊ดคัดลอกข้อมูลจาก โปรแกรมเม็มมาเก็บไว้ในแรมส่วน .data memory ให้อีกด้วย ในที่นี้สมมติว่า DIGITS_7SEG[] มีค่า20ไบท์ ในตอนเริ่มโปรแกรมส่วนของข้อมูลทั้ง20ไบท์จะถูกโหลดด้วยคำสั่งLPMมาเก็บใน แรมให้เลย สมมติว่าข้อมูลอาเรย์มีจำนวนมากสัก500ไบท์GCCก็จะใช้พื้นที่ในเมมมากตามนั้น [ปัญหาคือ บางทีเราต้องการให้คัดลอกข้อมูลจากflashมาเก็บไว้ในตัวแปรอาเรย์เท่าที่ใช้งาน เท่านั้น แต่GCC ดันเอาข้อมูลค่าคงที่ทั้งหมดมาทำเป็นตัวแปรอาเรย์แทน ซึ่งอาจจะผิดจุดประสงค์ ของผู้เขียนโปรแกรม เช่นเราแสดงตัวอักษรของจอLCD หรือตัวเลขของ7segment ไม่ใช่ ต้องการคัดลอกข้อมูลทั้งหมดมาเป็นตัวแปรอาเรย์เพื่อที่จะใช้ได้ง่ายๆเป็นต้น..] static char str [10]="1234567891"; // ถูกจองใน.data 10ไบท์+เขียนในflash10 const uint8_t DIGITS_7SEG[] ={.....เหมือนตย. ใน 6.2 จำนวน20ไบท์ } // ข้อมูลใน DIGITS_7SEG[] 20ไบท์ถูกเขียนในflashโปรแกรม // ถูกจองพื้นที่ในแรมอีก 20ไบท์ สำหรับ DIGITS_7SEG[] และตอนเริ่มต้นโปรแกรมGCC มีการคัดลอกค่าคงที่ DIGITS_7SEG[] ทั้งหมดลงแรมทีจองอีกด้วย ลักษณะคล้ายตัวแปรอาเรย์แต่มีค่าคงที่ int main (void) //โค๊ดต่อไปนี้ มีการให้ค่าชุดค่าคงทีอาเรย์แก่ชุดตัวแปรอาเรย์strทั้งหมด ด้วยหลูปดังนี้ { for (int i = 0 ; i < 10 ; i++) str [i] = DIGITS_7SEG[i]; } //เมื่อแปลงเป็น ASM จะได้โค๊ดดังนี้ LDI R30,0x0A LDI R31,0x01 LDI R24,0x00 LDI R25,0x00 ldzz: LD R18,Z+ MOVW R26,R24 SUBI R26,0x00 Subtract immediate SBCI R27,0xFF Subtract immediate with carry ST X,R18 Store indirect ADIW R24,0x01 Add immediate to word CPI R24,0x0A Compare with immediate CPC R25,R1 Compare with carry BRNE ldzz Branch if not equal (pc-0008) //^ จะเห็นว่าใช้จำนวนไบท์ไป 26 ไบท์แต่มีข้อสังเกตดังนี้ // 1. เป็นการใช้คำสั่ง LD คือโหลดจากแรม ไป ST คือคัดลอกไปเก็บไว้ในแรมอีกที่ // 2. ยังไม่ใช่คำสั่ง LPM คือโหลดจากflash แล้ว ST ลอกไปเก็บไว้ในตัวแปรอาเรย์ |
||
|
||
[แอททริบิวท์ PROGMEM ในGCC] | ||
[การสร้างตัวแปรที่เรียกใช้ข้อมูลที่เก็บเอาไว้บนพื้นที่แฟลชหรือโปรแกรมเมมโมรี่ program memory (progmem) โดยตรง โดยไม่ต้องสร้างแรมสำรองเอาไว้ให้เปลืองพื้นที่เอาไว้ก่อน] สาระสำคัญคือ เดิมถ้าประกาศตัวแปรอาเรย์เพื่อเก็บข้อมูลลงในแฟลชมีผลทำให้มีการจองพื้นที่ในแรมเพื่อดึงข้อมูลทั้งหมดในแฟลชมาลงเอาไว้ในแรม ซึ่งบางทีเราต้องการใช้ข้อมูลจากแฟลชชนิดที่เราอ้างอินเด็กซ์หรือออฟเซ็ทหมายเลขข้อความหรือหน้าแล้วนำมาเก็บเอาไว้ในส่วนของแรมเพียง10-100ไบท์เพื่อใช้แสดงผลกับผู้ใช้งาน แต่มีข้อมูลที่เราต้องการค้นหาจากแฟลชด้วยการอิงเลขออฟเซ็ทข้อความหรือเพจซึ่งเก็บเอาไว้มากกว่า500-3000ไบท์ ซึ่งถ้าเป็นatmega328pก็อาจไม่มีแรมมากพอที่จะเก็บได้มากขนาดนั้น โดยปรกติถ้าเขียนโปรแกรมด้วยASMโดยตรงเราสามารถทำได้ทันทีด้วยเริ่มแรกคือการให้ค่าอินเด็กซ์ข้อความแล้วนำไปคำนวนเพื่อหาที่อยู่ของข้อมูลแฟลช จากนั้นก็ให้ค่าprogram mem address ผ่านพ้อยเตอร์Z แล้วจึงโหลดด้วยคำสั่งมาLPMมาเก็บไว้ในR0เพื่อนำไปรวมข้อมูลต่อไปแล้วส่งไปเก็บในแรมผ่านพ้อยเตอร์Xเป็นต้น เข้าใจว่าทางGCC จัดทำ แอททริบิวท์(คุณลักษณะ) PROGMEM เพื่อช่วยให้เราติดต่อดึงข้อมูลจาก แฟลชเมมโมรี่ หรือโปรแกรมเมมโมรี่ในภาษาC แต่ไม่แน่ใจว่าสามารถทำได้มากขนาดไหน มีข้อจำกัดอะไรบ้าง แล้วค่อยๆขุดกันไปดู ข้อมูลต่างๆขุดได้มาจาก 1 [avrfreaks :ติวเรื่อง [TUT] [C] GCC and the PROGMEM Attribute ] 2 [avrfreaks : How to store struct elements in program memory (flash)? ] 3 [avrfreaks : Getting Data out of a PROGMEM struct - w/ pointer ] 4 [GCC : Data in Program Space ] 5 [GCC : pgmspace.h File Reference ] ทางGCCบอกว่าAVRเป็นสถาปัตยกรรมจากฮาร์วาร์ดที่แยกโค๊ดจัดเก็บในแฟลชและข้อมูลจะถูกเก็บในแรมโดยแยกที่อยู่กัน ส่วนภาษาCถูกออกแบบจากสถาปัตยกรรมฟอนนอยมันน์ที่เก็บโค๊ดและดาต้าอยู่บนที่อยู่เดียวกัน ทางGCCจึงสร้างคีย์เวิร์ด __attribute__ ที่เรียกว่า PROGMEMเพื่อจัดการปัญหานี้ส่วนใหญ่ซึ่งสร้างเป็นภาษาแมคโครเพิ่มเติมจากCมาตรฐาน ผ่านเฮดเดอร์ไฟล์ pgmspace.h สำหรับการประกาศฟังก์ชั่น ตัวแปร และ types ซึ่งโดยทั่วไปแอททริบิวต์PROGMEMเป็นการบอกคอมไพล์เลอร์ว่าเป็นประกาศข้อมูลซึ่งเป็นชนิดข้อมูลที่ให้เก็บไว้ในพื้นที่แฟลชโดยตรงหรือสามารถเรียกใช้ข้อมูลจากพื้นทีดังกล่าวแฟลชโดยตรง การใช้ คีย์เวิร์ด CONST เพื่อบังคับให้คอมไพลเลอร์เก็บข้อมูลค่าคงที่ในแฟลชเป็นการละเมิดคำจัดกัดความการใช้งาน CONSTในภาษาC ที่หมายถึง เป็นค่าที่เอาไว้อ่านค่าเท่านั้น เมื่อCONSTอยู่ในฟังก์ชั่นหรือพารามิเตอร์ใดก็เป็นการบอกให้คอมไพล์เลอร์รู้และจัดการให้ถูกต้องว่าเป็นข้อมูลอ่านและใช้ในบริเวณนั้นเท่านั้น ไม่ได้มีความหมายให้เก็บค่าในแฟลชแต่อย่างใด เท่าที่อ่านดูของGCC[4][5] พบว่า PROGMEM มีความสามารถมากพอสมควร ดังนี้ 1. การเจาะจงบันทึกค่าคงที่อาเรย์ชนิดที่เก็บข้อมูลลงบนแฟลช ด้วยPROGMEM #include <avr/pgmspace.h> unsigned char DIGITS_7SEG[20] PROGMEM = ;error ต้องใส่ const ด้วย ;ลองรันด้วยatmel studio พบว่า เออเร่อขัดกับนิยามPROGMEM ต้องใส่ const const unsigned char x12[20] PROGMEM = { "01234567890122256789" }; static char str [10]="9876543210"; int main(void) { for (int i = 0 ; i < 3 ; i++){str [i] = x12[i];} } //^ พบว่า ไม่มีการจองแรมสำหรับ x12 มีแต่แรมสำหรับ str ในส่วนdata 10 byte พบว่า ในพื้นที่flash มีข้อความ 30 31 32 33 34 35 36 37 38 39 = "0123456789" //^ พบว่า ในเมน ไม่ได้มีการคัดลอกข้อมูลจากอาเรย์ x12[0-2]>>str[0-2] // แต่อย่างใด มีแต่การส่งข้อมูลโดยตรงไปยังแรมของ str[0-2] โดยให้ค่าข้อมูลดิบๆโดยตรง // โดยไม่ได้ใช้คำสั่ง LPM แต่อย่างใดดังนี้ LDI R30,0x00 LDI R31,0x01 ;ให้ค่าพ้อยเตอร์Z ชี้ไปที่แรม str[0] LDI R24,0x3F STD Z+0,R24 ;ให้ค่า0x3F เอาไปเก็บที่ str[0] LDI R24,0x06 STD Z+1,R24 ;ให้ค่า0x06 เอาไปเก็บที่ str[1] LDI R24,0x5B STD Z+2,R24 ;ให้......................... str[2] // ต้องใช้รูปแบบคำสั่งเฉพาะจึงจะคัดลอกข้อมูลจากแฟลชลงแรมได้ // -- ในกรณีที่ กำหนด for (int i = 0 ; i < 9 ; i++) จะเหมือนกรณีข้างบนคือให้ค่าข้อมูล โดยตรงด้วยคำสั่ง LDI โดยไม่ได้ใช้คำสั่ง LPM --ในกรณี ลบบรรทัด str [i] = x12[i]; มีผลทำให้ str และ x12 ไม่เกิดการใช้งาน ทำให้ไม่เกิดการบันทึกข้อมูลของx12 ลงแฟลชแต่อย่างใด และ str[] ก็ไม่มีการจองเมมโมรีหรือแรมในส่วน.data section แต่อย่างใด --แต่เมื่อ กำหนด for (int i = 0 ; i < 10 ; i++) จะเกิดเออเร่อขึ้น กลายเป็น การให้ ให้ค่าที่อยู่บนflash หรือ &x12[0] แก่ พ้อยเตอร์Z แต่ใช้คำสั่ง LD R24,Z+ เพื่อโหลดข้อมูลจากแรมแทนแฟลช*****เออเร่อ***** ST X+,R24 ถ่ายไปยัง แรมตำแหน่ง &str[0] แทน *****ซึ่ง เป็น เออเร่อ เพราะยังใช้ไม่ถูกภาษาที่กำหนดใน PROGMEM*** 2. ต้องใช้คำสั่งและรูปแบบเฉพาะ,การอ้างอิงที่อยู่พ้อยเตอร์ จึงจะคัดลอกข้อมูลแฟลชลงแรมได้ โดยปรกติC,C++เดิม โอเปอเรเตอร์ที่เกี่ยวกับการอ้างอิงที่อยู่,พ้อยเตอร์จะมี2 ชนิดคือ * , & แต่ใน AVR-GCC อาจไม่สามารถใช้โอเปอเรเตอร์เหล่านี้โดยตรงต้องใช้ผ่านฟังก์ชั่นหรือแอททริบิวท์ ที่มีรูปแบบเฉพาะตัวของGCC ใน PROGMEM * (โอเปอเรเตอร์ดอกจัน) หรือ โอเปอเรเตอร์indirect วางหลัง type เพื่อประกาศเป็น ตัวแปรพ้อยเตอร์ , ใช้เข้าถึงข้อมูลทางอ้อมเพื่ออัพเดทข้อมูลได้ เช่น int *px ; ประกาศตัวแปรพ้อยเตอร์ชื่อpx ซึ่งชึ้ที่อยู่ตำแหน่งข้อมูลชนิดint ซึ่งpxตอนนี้จะมีค่า0 หรือมีที่อยู่0 int x =10; ประกาศตัวแปรที่เก็บค่าข้อมูล int และมีค่าเริ่มต้น=10 & (โอเปอร์เรเตอร์แอนด์) ใช้กำหนดค่าที่อยู่ของตัวแปรข้อมูลนั้นให้กับตัวแปรพ้อยเตอร์ จากตัวอย่างข้างบน 2 ตย. เราจะกำหนดค่าที่อยู่ของxให้พ้อยเตอร์pxผ่าน&ได้ดังนี้ px = &x; ให้ค่าที่อยู่ของข้อมูลx แก่ พ้อยเตอร์px ซึ่งเราไม่ทราบว่าpx มีค่า เท่าไหร่จนกว่าจะพิมพ์ออกมา เนื่องจากCเป็นผู้จัดสรรตำแหน่งให้ ถ้าเราพิมพ์ px จะได้ค่า 0x0100 ออกมาเป็นต้น แต่ถ้าเราพิมพ์ *px จะได้ค่า 10 ออกมา *px= 15; ให้ค่า15 กับตำแหน่งที่อยู่ที่ตอนนี้pxกำลังชี้ มีผลเป็นx=15 เป็นการกำหนดค่าโดยอ้อมของx ผ่านpx ด้วย* อาเรย์จะมีลักษณะเฉพาะของพ้อยเตอร์ดังนี้ ถ้าเราให้ตัวแปรอาเรย์เฉยๆโดยไม่มี[อินเด็กซ์] แก่ตัวแปรพ้อยเตอร์ จะมีผลทำให้มีการให้ค่าพ้อยเตอร์ของตัวแปรอาเรย์ที่อินเด็กซ์0 แก่ตัวแปรพ้อยเตอร์ทันที ดังนี้ int y[5] = {11,12,13,25,36}; px=y ; // ให้ค่าพ้อยเตอร์Y[0] แก่ px px=&y[3]; // ให้ค่าพ้อยเตอร์y[3] แก่ px 2.1 การสร้างตัวแปรอาเรย์ข้อมูลชุดลงแฟลช ที่ง่ายต่อการอิงค่าอินเด็กซ์หรือออฟเซทของข้อมูล โดยปรกติเมื่อเรามีชุดข้อมูลค่าคงที่ซ้ำๆ เช่นเรามี 7segment 10 digit การเขียนโปรแกรม ด้วยภาษาเครื่องASM ผู้เขียนจะสร้างอินเด็กซ์ข้อมูล หน้าหรือเพจขึ้น เพื่อกระโดดไปอ่านข้อ มูลทีละ10ไบท์จากแฟลชได้อย่างถูกต้องมาเก็บไว้ที่แรม จึงจำเป็นต้องแบ่งจัดเก็บข้อมูลเป็น ทีละ 10 ไบท์ จึงสามารถอ้างอิงที่อยู่ อินเด็กซ์หรือออฟเซ็ทได้ง่ายขึ้น เดิมเราสร้าง const char x12[20] PROGMEM ={ "01234567890122256789" }; ซึ่งเป็นข้อมูล 2 ชุดของ 7segmet 10 digit ซึ่งเมื่อนำมาใช้ เราต้องหาค่าออฟเซ็ท ทีละ10 เช่นข้อมูลชุดแรกเริ่มที่ x12[0-9] และข้อมูลชุดที่สองคือ x12[10-19] ซึ่งเมื่อเราสร้างหรืออ้างอิงตัวแปรพ้อยเตอร์GCC เมื่อแปลงเป็นASM จะเขียนได้ยาวและ สับสนกว่าเล็กน้อย แต่ถ้าเขียนเองด้วยภาษาเครื่องอาจจะสั้นและง่ายกว่า ในGCCจึงควรสร้างเป็น const char x1[] PROGMEM ={ "0123456789" }; &x1=0x0073 const char x2[] PROGMEM ={ "0122256789" }; &x2=0x0068 มีกี่ชุดที่ต้องการก็สร้างเท่านั้นชุด เมื่อพวกมันถูกใช้งานในcode GCC จะเก็บข้อมูล เหล่านี้ลงแฟลชโดยสร้างที่อยู่ให้อัตโนมัติโดยที่เราไม่รู้ว่าได้ค่าอะไร ในที่สมมติว่า ที่อยู่ที่เก็บไบท์แรกของx1 =0x0073 =&x1 บนแฟลช ที่อยู่ x2 =0x0068 =&x2 2.2 การสร้างตัวแปรอาเรย์พ้อยเตอร์อิงที่อยู่ของแฟลช อิงข้อมูลอาเรย์ค่าคงที่ที่สร้างเอาไว้ ที่อยู่ข้อมูลที่เราต้องการบนแฟลช ในที่ใช้ atmega328 มีขนาดที่อยู่ 2ไบท์ หรือ16บิท ฉะนั้นตัวแปรพ้อยเตอร์ 1 ข้อมูล ต้องมีอย่างน้อย2ไบท์ หรือเป็นพวก unsigned int เป็นต้น เราสามารถสร้างอาเรย์พ้อยเตอร์ได้ดังนี้ const char * const px[] PROGMEM = { x1, x2 }; หรือ PGM_P const px[] PROGMEM = { x1, x2 }; ต้องใส่ const หน้า px[] ไม่งั้นคอมไพล์ไม่ผ่าน ซึ่งนิยามแมคโคร PGM_P คือมีค่า= const char* การสร้างอาเรย์พ้อยเตอร์ PROGMEM มีจุดประสงค์เพื่อบอกให้คอมไพลเลอร์ รู้แล้วแปลงค่าผ่านค่าตัวเลขแอดเดรสในASMไปใช้โดยตรง โดยไม่มีการบันทึก ค่าพ้อยเตอร์ดังกล่าวลงไปบนหน่วยความจำแฟลชและแรมแต่อย่างใด ในกรณีนี้px แต่ละอินเด็กซ์ จะมีค่าข้อมูลที่อยู่ขนาด 16 บิทและแก้ไขไม่ได้ โดยอาเรย์พ้อยเตอร์ px จะเก็บค่า ที่อยู่ของข้อมูล x1 ,x2 ในแฟลชเอาไว้ ถ้าเราสร้างเปรียบเทียบตัวแปรพ้อยเตอร์(แรม)ธรรมดาอีกตัว ดังนี้ static char * printpx = &x1 ; เป็นการจองแรมใน.data 2ไบท์ และให้ค่าที่อยู่แฟลชของx1 = 0x0073 (C ให้ที่อยู่อัตโนมัติ) เราสามารถให้ค่าแก่ printpx ได้ดังนี้แต่ไม่สามารถนำไปใช้อะไรได้ เพราะการกระทำการใดๆจะทำกับแรมแทนไม่สามารถคัดลอกจากแฟลชได้ printpx = px[1] ; ได้ค่าที่อยู่แฟลช ของx2มา = 0x0068 printpx = &x1[] ; ได้ค่าที่อยู่แฟลช ของx1มา = 0x0073 printpx = &x2[1] ; ได้ค่าที่อยู่แฟลช ของx2[1] = 0x0069 *printpx = "9" ; ให้ค่า"9" แก่แรม ตำแหน่งที่ 0x0069 ซึ่งจะทำการกับแรมที่ตำแหน่งดังกล่าวแทน 2.3 การคัดลอกข้อมูลจากแฟลชลงแรม ด้วยคำสั่งเฉพาะทาง (ภาษาเครื่องจะใช้คำสั่ง LPM) ในแอททริบิวต์ PROGMEM เราต้องใช้คำสั่งที่สร้างด้วยmacro ใน avr/pgmspace.h และอาจใช้ฟังก์ชั่นเฉพาะทางที่ง่ายและสะดวก ที่GCC จัดทำเอาไว้ในการคัดลอก ข้อมูลแฟลชลงแรม ผ่านพ้อยเตอร์PROGMEM ที่ได้ประกาศเอาไว้ มีมากมายหลายคำสั่ง http://www.nongnu.org/avr-libc/user-manual/pgmspace_8h.html ยกตัวอย่าง เช่น ขอยกเอาโค๊ดเดิมที่ยกตัวอย่างไปแล้วทั้งหมดมาอีกครั้งหนึ่ง #include <avr/pgmspace.h> const unsigned char x12[20] PROGMEM = { "01234567890122256789" }; const char x1[10] PROGMEM ={ "0123456789" }; const char x2[10] PROGMEM ={ "0122256789" }; const char * const px[] PROGMEM = { x1, x2 }; static char str [10]="9876543210"; int main(void) { // 1. ต้องการก็อปปี้ข้อมูลจาก x1 ไป ยัง str ทำอย่างไร // 2. ต้องการก็อปปี้x12 เฉพาะx12[10-19]ไปยัง str ทำอย่างไร } --------- ต่อไปนี้เป็นตัวอย่าง คำสั่งแมคโคร หรือ ฟังก์ชั่น ในPROGMEM-------- pgm_read_byte(ที่อยู่แฟลช16บิท) คำสั่งแมคโครที่อ่านค่า1ไบท์จากแฟลช #define pgm_read_byte_near(address_short) __LPM((uint16_t)(address_short)) int main(void) { // 1. ต้องการก็อปปี้ข้อมูลจาก x1 ไป ยัง str ทำอย่างไร for (int i = 0 ; i < 10 ; i++) {str[i] = pgm_read_byte(&x1[i]);} /* LDI R30,0x68 LDI R31,0x00 ให้ค่าที่อยู่แฟลชZ=0x0068 LDI R26,0x00 LDI R27,0x01 ให้ค่าที่อยู่แรม X =0x0100 l1:LPM R24,Z โหลดข้อมูลที่Zชี้ แก่ R24 ST X+,R24 เก็บค่าR24 แก่ที่Xชี้ แล้ว เพิ่มที่อยู่Xอีก1 ADIW R30,0x01 เพิ่มค่าที่อยู่Z อีก1 LDI R24,0x01 ให้ค่า 1 แก่ R24 CPI R26,0x0C R26-0x0C CPC R27,R24 R27-R24-C BRNE l1 กระโดด ไป l1 ถ้า (R27-R24-C <>0) } int main(void) { // 2. ต้องการก็อปปี้x12 เฉพาะx12[10-19]ไปยัง str ทำอย่างไร for (int i = 0 ; i < 10 ; i++) {str[i] = pgm_read_byte(&x12[i+10]);} /* จะเห็นว่าที่จริง GCC ก็เขียนได้สั้นดี แต่ASM มีโค๊ดเพิ่มอีก 3 บรรทัด จึงนิยมให้แยกข้อมูลเป็นx1[10] และ x2[10] แทนที่จะเป็น x12[20] LDI R26,0x00 LDI R27,0x01 LDI R24,0x00 LDI R25,0x00 l2: MOVW R30,R24 SUBI R30,0x8E SBCI R31,0xFF LPM R30,Z ST X+,R30 ADIW R24,0x01 CPI R24,0x0A CPC R25,R1 BRNE l2 } char * strcpy_P (char * ปลายทางแรม, const char * ต้นทางแฟลช) ฟังก์ชั่นสำหรับก็อบปี้ข้อมูลstring รวมถึงข้อมูล \0ด้วย (_P ,program space) ซึ่งstrcpy_p คัดลอกจาก พ้อยเตอร์แฟลชต้นทาง -> พ้อยเตอร์แรมปลายทาง ซึ่งต้อง strcpy_P(buffer, (PGM_P)pgm_read_word(&(string_table[i]))); int main(void) { // 1. ต้องการก็อปปี้ข้อมูลจาก x1 ไป ยัง str ทำอย่างไร กก กก |
||
ตัวอย่าง แมคโคร หรือ ฟังก์ชั่น ที่น่าสนใจใน pgmspace.h | ||
^ | |
PSTR(s) ใช้ไงหว่า นิยามแมคโครคือ #define PSTR(s) คือ ((const PROGMEM char *)(s)) |
|
ก่อนจะทำความเข้าใจ ฟังก์ชั่นใน pgmspace.h ที่ส่วนใหญ่เป็นฟังก์ชั่นดำเนินการเกียวกับตัวอักษร ในแฟลชเมมโมรี่และอาจเกี่ยวข้องยาวไปถึงแรม เรามาลองทำความเข้าใจตรงนี้ก่อน แต่เดิมฟังก์ชั่นที่ดำเนินการเกี่ยวกับสตริง ที่อยู่ในเฮดเดอร์ไฟล์ string.h [avr-libc reference] เข้าใจว่าเป็นไฟล์มาตรฐานและมีฟังก์ชั่นส่วนขยายในภาษาC มีฟังก์ชั่นสำเร็จรูปมากมายไม่จำเป็นต้องเขียนเอง เช่น string.h ฟังก์ชั่น ***ไม่แน่ใจว่า ffs() เป็นฟังก์ชั่นอะไร เพราะอ่านแล้วยังไม่เข้าใจ? -กลุ่มฟังก์ชั่นเกี่ยวกับการคัดลอกข้อมูล จากแรมไปแรม เช่น 1 memccpy() คัดลอกเมมปรียบเทียบหยุด (memory compare copy) void * memccpy (void *, const void * , int , size_t) ทำการคัดลอกแรม โดยกำหนดจำนวนไบท์ที่คัดลอกlen ที่อยู่ต้นทางscr ปลายทางdest ตัวอักษรที่ต้องการval ซึ่งจะหยุดคัดลอกก็ต่อเมื่อ 1 - หาตัวอักษรที่ต้องการพบจะหยุดทันที และฟังก์ชั่นจะคืนค่า ที่อยู่Val+1 2 - ถ้าหาไม่พบก็คัดลอกตามจำนวนlenแล้วหยุด และฟังก์ชั่นจะคืนค่า NULL void * memccpy (คืนค่าที่อยู่16บิทที่&Val+1เมื่อพบตัวอักษร หรือคืนNULLเมื่อไม่พบ) ( void * (ที่อยู่ต้นทาง หรือค่าตัวชี้ 16บิท , dest) , const void * (ที่อยู่ปลายทาง ซึ่งเป็นค่าคงที่? 16บิท ,src) , int (ค่าตัวอักษร-characterที่หาพบแล้วให้หยุดการคัดลอก ,Val) , size_t (จำนวนกี่ไบท์ หรือจำนวนตัวอักษร เมื่อครบจะหยุดคัดลอก ,len) ) 2 memcpy() คัดลอกเมม (memory copy) คัดลอกแรมที่อยู่ต้นทาง ปลายทาง จำนวนไบท์ ? function returns a pointer to dest. เมมไม่ควรจะซ้อนทับกัน void * memcpy (void * dest, const void * scr , size_t len) 3 memmove() ย้ายคัดลอกเมม เหมือนกับ memcpy() แต่ใช้กับเมมที่อาจจะซ้อนทับตำแหน่ง void * memmove (void *, const void *, size_t) 4 strcmp() คัดลอกสตริง รวมถึง'\0' ใช้ก๊อปอาเรย์ชนิดcharสองชุด ปลายทางต้องใหญ่พอ ที่จะก็อปปีได้และพื้นที่สตริงไม่ควรซ้อนทับกัน ? function returns a pointer to the destination string dest. char * strcpy (char * dest , const char * scr ) 5 strlcpy() คัดลอกสตริงกำหนดขนาด ปรกติการก็อบปี้ข้อมูลจากscr จะกำหนดการคัดลอก ?ที่siz-1 เนื่องจากอักขระสุดท้ายของstringอาเรย์จะต้องเก็บค่าnull ซึ่งฟังก์ชั่นทั่วไปๆจะหยุดเมื่ออ่านเมื่ออ่านได้ค่าNULLเว้นแต่ให้siz==0 strlcpyจะคืนค่าความยาวของสตริงต้นทางscr ถ้าค่าความยาวที่คืนมาก>=siz คำที่เหลือจะถูกตัดออกไปโดยไม่ถูกก็อปปี้ size_t strlcpy (char * dest , const char * scr , size_t siz) 6 strncpy() คัดลอกสตริงกำหนดขนาด คัดลอกจากต้นทางไม่เกินlenที่กำหนด if there is no null byte among the first n bytes of src , the result will not be null-terminated. In the case where the length of src is less than that of n , the remainder of dest will be padded with nulls ?function returns a pointer to the destination string dest. char * strncpy (char * dest , const char * scr , size_t len) - กลุ่มฟังก์ชั่นอื่นๆ scan ,compare ,find ,fillเติมค่าคงที่ ,Concatenateตัดแบ่ง , locateหาค่าที่อยู่ของค่าที่ต้องการในสตริง ,คำนวนความยาว ,Duplicateทำซ้ำสตริง ,ปรับเป็นอักษรตัวเล็ก-ใหญ่ ,กำหนดความยาวของสตริงโดยเติม'\0'ลงไป ,กลับการเรียงลำดับสตริงจากหน้าไปหลัง ,Parse a string into tokens. pgmspace.h แมคโคร และฟังก์ชั่น ที่อยู่ในเฮดเดอร์ไฟล์ pgmspace.h [link:avr-libc reference] ด้วยคำสั่งฟังก์ชั่นที่เกี่ยวกับการคัดลอกเมมและอื่นๆซึ่งทำงานใกล้เคียงกันกับข้อมูลอักขระ โดยเฉพาะฟังก์ชั่นในstring.h แต่ในpgmspace.hฟังก์ชั่นส่วนใหญ่จะชื่อคล้ายกันแต่มีคำว่า _p ลงท้ายฟังก์ชั่นมีผลเป็นฟังก์ชั่นที่เกียวพันกับพื่นที่แฟลช ซึ่งมีทั้งการคัดลอกแฟลชลงเมม อีกทั้งยังมีแมคโครบางอันที่เพิ่มเติมเข้ามา ซึ่งน่าจะแบ่งเป็นพวกๆได้ดังนี้ - กลุ่มแมคโครที่เป็นการประกาศให้บันทึกลงหน่วยความจำแฟลชหรือเกี่ยวข้องกับที่อยู่บนแฟลช 1 PROGMEM ใช้ประกาศค่าพร้อมกับตัวแปรค่าคงที(ส่วนใหญ่เป็นอาเรย์) เพื่อให้คอมไพล์เลอร์ ทราบว่าข้อมูลในชื่อเหล่านี้เป็นส่วนที่จะบันทึกลงในแฟลช แต่คอมไพล์เลอร์อาจไม่ส่ง ให้บันทึกลงแฟลชก็ได้ถ้าไม่มีส่วนของโปรแกรมที่เกี่ยวข้องกับข้อมูลนั้นๆเลย #define PROGMEM __ATTR_PROGMEM__ โดยวิธีการประกาศ จะประกาศคล้ายกับตัวแปรโกบอล แต่จะมีคำว่า const และPROGMEM ***การกำหนดขนาดอีลีเม้นท์ในอาเรย์มีผลทำให้GCC ไม่ใส่ค่าNULL หรือ '\0' ปิดท้ายในแฟลช*** ***'\0' สำคัญ? ถ้าเราใช้ฟังก์ชั่นที่เกี่ยวกับสตริง ถ้าไม่มีค่านี้ปิดท้ายจะทำให้บางฟังก์ชั่นใช้ไม่ได้ เช่นฟังก์ชั่นที่เกี่ยวกับการหาความยาวสตริง ฟังก์ชั่นจะหยุดเมื่อเจอคำว่า'\0\' เป็นต้น ฉะนั้นถ้าเราต้องการใช้ฟังก์ชั่นที่อำนวยความสะดวก ในสตริงที่นอกเหนือจากการคัดลอกข้อมูลออกมา ควรกำหนดเป็นอาเรย์ชนิดไม่ระบุขนาด ******** ตัวอย่างการใช้ PROGMEM เพื่อบันทีกข้อมูลที่ต้องการเก็บลงแฟลช const unsigned char x12[20] PROGMEM ={ "01234567890122256789" }; const int int1_pgm PROGMEM = 0xF011; ตัวอย่างนี้เป็นข้อมูลตัวเลข2ไบท์ ในหลูปเมน ถ้าต้องการให้ไมโครคอนโทรลเลอร์โหลดด้วยคำสั่ง LPM เราอาจเขียนเป็น static unsigned int wwrd; wwrd = pgm_read_word(&int1_pgm); // ^ โหลดข้อมูล2ไบท์ของint1_pgmจากแฟลช มาเก็บใน wwrd // ฟังก์ชั่น pgm_read_word() เป็นแมคโครที่ใช้โหลดข้อมูลจากแฟลช // &int1_pgm หมายถึงที่อยู่16บิทที่เก็บข้อมูลของตัวแปรint1_pgm ซึ่ง ภาษาCจะสร้างตำแหน่งที่อยู่ให้เองโดยที่เราไม่รู้ว่าอยู่ที่ไหนแต่Cเข้าใจและ แทนค่าได้อย่างถูกต้องที่อยู่(พ้อยเตอร์)ของint1_pgm ได้อย่างถูกต้อง สมมติว่าCสร้างและเก็บเอาไว้ณ ที่อยู่0x0068บนแฟลช เก็บค่า 0x11 ที่อยู่0x0069บนแฟลช เก็บค่า 0xF0 LDI R30,0x68 LDI R31,0x00 ให้ค่าทีอยู่ 0x0068 LPM R24,Z+ โหลดมาเก็บใน R24 และเพิ่มค่าR30 (Z) LPM R25,Z โหลดมาเก็บใน R25 STS 0x0101,R25 STS 0x0100,R24 เก็บค่าไว้ในแรม 2 PGM_P ใช้ประกาศตัวแปรพ้อยเตอร์ที่ชี้ข้อมูลสตริงบนแฟลช แต่ไม่ได้บันทึกค่าพ้อยเตอร์ลงแฟลช ใช้ แค่ const char* ใส่ลงไปตอนประกาศก็ได้ มีความหมายเหมือนกัน #define PGM_P const char * ตัวอย่างต่อเนื่องจากข้อ1 การประกาศต้องมีคำ const หน้าตัวแปรพ้อยเตอร์ด้วย // const int int1_pgm PROGMEM = 0xF011; const char * const pint1_pgm PROGMEM = &int1_pgm; wwrd = pgm_read_word(pint1_pgm); ทำในหลูปเมน //const unsigned char x12[20] PROGMEM ={ "01234567890122256789" }; const char * const px12[2] PROGMEM ={&x12[0] , &x12[10]}; ในตัวอย่างของ GCC เรามีข้อมูลรวมๆดังนี้ เราสร้างพ้อยเตอร์ทับทีหลังก็ได้ แต่อาจไม่เหมาะกับการใช้งานบางฟังก์ชั่นประเภทที่อ่านข้อมูล'\0' หรือ NULL แล้วตัดสินใจกระทำการ ซึ่งเมื่อเป็นข้อมูลรวมๆจะมี'\0' หรือ NULL ที่ท้ายสุด ของข้อมูลstringเท่านั้นเมื่อไม่ได้กำหนดขนาดของอีลีเม้นในอาเรย์**** เช่น const char sa[] PROGMEM = {"String 1String 2String 3String 4String 5"}; // ^ ข้อมูลทั้งหมดจะบันทึกลงแฟลช 41 ไบท์ ซึ่งGCC จะปิดท้ายด้วย'\0' // ^ แต่ถ้ากำหนดค่าอีลีเม้นใน[]เป็น40 ก็จะมีแค่40ไบท์และไม่มีตัวปิดท้าย และข้อมูลชุดต่อไปก็จะต่อท้ายสตริง"5"ยาวต่อเนื่องกันไปเหมือนเป็นข้อ ความเดียวกัน const char * const string_table[] PROGMEM = {&sa[0] , &sa[8] , &sa[15] ,&sa[22],&sa[29],&sa[36]}; //**GCC แนะนำให้สร้างอย่างนี้ const char s1[] PROGMEM = "String 1"; //9ไบท์รวมคำว่า '\0' const char s2[] PROGMEM = "String 2"; const char s3[] PROGMEM = "String 3"; const char s4[] PROGMEM = "String 4"; const char s5[] PROGMEM = "String 5"; PGM_P const string_table[] PROGMEM ={s1,s2,s3,s4,s5}; 3 PGM_VOID_P ใช้ประกาศตัวแปรพ้อยเตอร์ทั่วไปที่ชี้ข้อมูลใดๆบนแฟลช แต่ไม่ได้บันทึก ค่าพ้อยเตอร์ของข้อมูลนั้นลงหน่วยความจำแฟลชแต่อย่างใด เข้าใจว่า เอาไว้ใช้ชี้กับข้อมูลชนิดอื่นๆซึ่งหัวข้อมูลในแฟลชของmcuใช้พ้อยเตอร์16บิท เหมือนกันหมดไม่ว่าหัวข้อมูลจะเป็นข้อมูลอะไรยาวกี่ไบท์ก็ตาม #define PGM_VOID_P const void * 4 PSTR() ???ถ้าอ่านในGCC จะไม่เข้าใจเลยว่ามันเอาไปใช้อย่างไรซึ่ง GCC นิยามว่า "Used to declare a static pointer to a string in program space." แปลว่าใช้ประกาศพ้อยเตอร์สแติกไปที่สตริงในแฟลช (แล้วมันต่างอะไรกับข้อ2-3?) #define PSTR(s) ((const PROGMEM char *)(s)) มีผู้อธิบายชื่อ Dean Camera อธิบายใน PDF และใน avrfreaks [avrfreaks :ติวเรื่อง [TUT] [C] GCC and the PROGMEM Attribute ] PSTR เป็นแมคโครที่อำนวยความสะดวกซึ่งใช้กับฟังก์ชั่นและใช้ในฟังก์ชั่นเท่านั้น ใช้เดี่ยวๆไม่ได้ ไม่สามารถประกาศแบบโกบอลหรือใช้ประกอบกับการให้ค่าพ้อยเตอร์ตอนประกาศได้ ทำหน้าที่คืนค่าพ้อยเตอร์ชี้ที่แฟลชซึ่งเป็นแบบชนิดที่ไม่ต้องสร้างชื่อตัวแปรพ้อยเตอร์ และทำการบันทึกข้อมูลในวงเล็บ(s)ลงบนแฟลชให้ โดยข้อความที่บันทึกหรือ ที่ใช้ในฟังก์ชั่นในที่นี้ส่วนใหญ่จะใช้ครั้งเดียวแล้วไม่ได้ใช้อีก เป็นลักษณะการบันทึกข้อมูล inline string บันทึกลงแฟลชแล้วก็จะไม่ได้ใช้อีก ถ้ามีการใช้เกิน1ครั้งให้ประกาศแบบโกบอลเหมือนในข้อ1จะดีกว่า เพราะ ถ้าใช้ซ้ำประโยคเดิมเปี๊ยบจะทำให้บันทึกข้อมูลลงแฟลชอีกรอบ PSTR(s) returnเป็นค่าพ้อยเตอร์ของข้อความsที่บันทึกลงแฟลช ตัวอย่างการใช้ PSTR กับ ใช้การประกาศแบบโกบอล ซึ่งมีผลเหมือนกัน เดิมมี func1(const char *data){nop();} func1(PSTR("write_in_FlasH1times")); //^ ถ้ามีการเรียกซ้ำก็บันทึกข้อมูล"write_in_FlasH1times"ลงแฟลชเพิ่มอีกเป็นเท่าตัว //^ ถ้าต้องการเรียกใช้ซ้ำเพื่ออ่านในภายหลังให้บันทึกพ้อยเตอร์PSTRลงบนตัวแปร พ้อยเตอร์บนแรม หรือตัวแปรพ้อยเตอร์โลคอล แล้วจึงใช้วิธีการโหลดค่าในภายหลังผ่านแมคโคร pgm_read_xx() ซึ่งวิธีนี้ทำให้โค๊ดอาจยาวขึ้นเนื่องจากไม่มีชื่อพ้อยเตอร์ของข้อความดังกล่าวGCCต้อง ผ่านตัวแปรไปมาจึงจะนำไปใช้ต่อได้ แต่ถ้าเป็นวิธีประกาศแบบโกบอลด้านล่าง GCCจะให้ค่าคงที่16บิทโดยตรงได้เลย โค๊ดจึงสั้นกว่าเช่น pgm_read_xx(&save1[i]); ซึ่งสมมูลกับโค๊ดดังนี้ const char save1[] PROGMEM = "write_in_FlasH1times" ; func1(save1); - กลุ่มแมคโครที่อ่านข้อมูลจากแฟลช เป็นการโหลดค่าข้อมูลจากแฟลชมาสำรองที่รีจิสเตอร์ รอการให้เก็บค่าลงตัวแปรแรมตามtypeต่างๆ ใน atmega328p เนื่องจากมีพื้นที่แฟลชเพียง 32K จึงใช้อ้างอิงแอดเดรสเพียง 2 ไบท์ จึงใช้ กลุ่มแมคโครประเภทที่ไม่ลงท้ายด้วยfarที่อ้างอิงที่อยู่ขนาดมากกว่าไบท์ คือ พวกที่ลงท้ายด้วย near และพวกที่ไม่ลงท้ายด้วยอะไรเลย ดังนี้ #define pgm_read_byte_near(address_short) __LPM((uint16_t)(address_short)) หมายถึงเมื่อมีคำเหล่านี้ในภาษาแมคโครจะถูกแทนค่าด้วยภาษาASM ประเภท LPM โดยจะโหลดค่าข้อมูลจากแฟลชที่มีจำนวนไบท์ต่างๆกันตามชนิดแมคโครโดยการ ผ่านค่า address_short คือต้องใส่ค่าที่อยู่16บิทลงไป และเมื่ออ่านค่าจากแฟลช แล้ว ข้อมูลจะถูกเก็บไว้ในรีจิสเตอร์ที่พร้อมที่จะส่งข้อมูลไปเก็บไว้ในแรมตามแต่ที่มีโค๊ด ต่อเนื่องกันไปคือเก็บไว้ในตัวแปรแรมเหมือนตัวอย่างที่ผ่านๆมา เป็นต้น 1 pgm_read_byte(address_short) อ่านค่าจากแฟลช 1 ไบท์ 2 pgm_read_word(address_short) อ่านค่าจากแฟลช 2 ไบท์ 3 pgm_read_dword(address_short) อ่านค่าจากแฟลช 4 ไบท์ 4 pgm_read_float(address_short) อ่านค่าจากแฟลช 4 ไบท์ ชนิดfloat 5 pgm_read_ptr(address_short) อ่านค่าจากแฟลช 2 ไบท์ เป็นค่าพ้อยเตอร์ - กลุ่มฟังก์ชั่นที่เกี่ยวกับการคัดลอกข้อมูล (แฟลชไปแรม) ซึ่งวิธีการใช้งานเหมือนกับกลุ่มคัดลอกของ string.h แต่ชื่อลงท้ายด้วย_p อาจมีข้อที่ต่างกันเล็กน้อย 1 - กลุ่มฟังก์ชั่นอื่นๆ scan ,compare ,find ,fillเติมค่าคงที่ ,Concatenateตัดแบ่ง , locateหาค่าที่อยู่ของค่าที่ต้องการในสตริง ,คำนวนความยาว ,Duplicateทำซ้ำสตริง ,ปรับเป็นอักษรตัวเล็ก-ใหญ่ ,กำหนดความยาวของสตริงโดยเติม'\0'ลงไป ,กลับการเรียงลำดับสตริงจากหน้าไปหลัง ,Parse a string into tokens. |
|
....
....
....
....
---
---
---
---
....
....
....
....
....
....
---
---
---
---
---
---
---
---
คลิ๊ก> หมวดหมู่ความรู้
Email :
. . pui108108@yahoo.com
Line ID :
. . pui108diy
โทร: .. 089-797-9411
blog word press :
. . . pui108diy.com/wp/
รูป Flickr :
. . . ./photos/pui108diy/
รูป Wiki commons :
. . ./wiki/User:Pui108108
Pinterest :
. . . ./pui108108/
Youtube :
. . . ./user/p108108
หน้าที่เข้าชม | 214,168 ครั้ง |
ผู้ชมทั้งหมด | 167,913 ครั้ง |
เปิดร้าน | 17 ก.ย. 2558 |
ร้านค้าอัพเดท | 15 ก.ย. 2568 |