การกำหนดฮีปและขนาดสแต็คสำหรับไมโครคอนโทรลเลอร์ ARM Cortex-M4


11

ฉันได้ทำงานในและนอกโครงการระบบฝังตัวขนาดเล็กในและนอก บางโครงการเหล่านี้ใช้หน่วยประมวลผลพื้นฐาน ARM Cortex-M4 ในโฟลเดอร์โครงการจะมีไฟล์startup.s ข้างในไฟล์นั้นฉันสังเกตบรรทัดคำสั่งสองบรรทัดต่อไปนี้

;******************************************************************************
;
; <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
;
;******************************************************************************
Stack   EQU     0x00000400

;******************************************************************************
;
; <o> Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
;
;******************************************************************************
Heap    EQU     0x00000000

เราจะกำหนดขนาดของฮีปและสแต็คสำหรับไมโครคอนโทรลเลอร์ได้อย่างไร? มีข้อมูลเฉพาะในแผ่นข้อมูลเพื่อเป็นแนวทางในการมาถึงมูลค่าที่ถูกต้องหรือไม่? ถ้าเป็นเช่นนั้นเราควรมองหาอะไรในแผ่นข้อมูล


อ้างอิง:

คำตอบ:


12

Stack และ heap เป็นแนวคิดซอฟต์แวร์ไม่ใช่แนวคิดฮาร์ดแวร์ สิ่งที่ฮาร์ดแวร์ให้คือหน่วยความจำ การกำหนดโซนของหน่วยความจำซึ่งหนึ่งในนั้นเรียกว่า "สแต็ค" และหนึ่งในนั้นเรียกว่า "ฮีป" เป็นตัวเลือกของโปรแกรมของคุณ

ฮาร์ดแวร์ช่วยด้วยสแต็ค สถาปัตยกรรมส่วนใหญ่มีการลงทะเบียนเฉพาะที่เรียกว่าตัวชี้สแต็ก การใช้งานตามวัตถุประสงค์คือเมื่อโปรแกรมทำการเรียกใช้ฟังก์ชั่นพารามิเตอร์ฟังก์ชั่นและที่อยู่ผู้ส่งจะถูกส่งไปยังสแต็ก การกดลงบนสแต็กหมายถึงการเขียนที่อยู่ที่กำหนดโดยตัวชี้สแต็กและลดตัวชี้สแต็กตาม (หรือการเพิ่มขึ้นตามทิศทางที่สแต็คเติบโต) Popping หมายถึงการเพิ่ม (หรือลด) ตัวชี้สแต็ก ที่อยู่ผู้ส่งจะอ่านจากที่อยู่ที่กำหนดโดยตัวชี้สแต็ก

สถาปัตยกรรมบางอย่าง (ไม่ใช่ ARM) มีคำสั่งเรียกรูทีนย่อยที่รวมการกระโดดด้วยการเขียนไปยังที่อยู่ที่กำหนดโดยตัวชี้สแต็กและคำสั่งรูทีนย่อยส่งคืนที่รวมการอ่านจากที่อยู่ที่กำหนดโดยตัวชี้สแต็ก บน ARM การบันทึกที่อยู่และการกู้คืนเสร็จสิ้นในการลงทะเบียน LR คำแนะนำในการโทรและส่งคืนจะไม่ใช้ตัวชี้สแต็ก อย่างไรก็ตามมีคำแนะนำเพื่อความสะดวกในการเขียนหรืออ่านการลงทะเบียนหลายครั้งไปยังที่อยู่ที่กำหนดโดยตัวชี้สแต็กเพื่อผลักดันและฟังก์ชั่นการโต้แย้งป๊อป

ในการเลือกฮีปและขนาดสแต็คข้อมูลที่เกี่ยวข้องจากฮาร์ดแวร์เท่านั้นคือจำนวนหน่วยความจำทั้งหมดที่คุณมี จากนั้นคุณเลือกตามความต้องการในการจัดเก็บในหน่วยความจำ (อนุญาตให้ใช้รหัสข้อมูลแบบคงที่และโปรแกรมอื่น ๆ )

โดยทั่วไปแล้วโปรแกรมจะใช้ค่าคงที่เหล่านี้เพื่อเริ่มต้นข้อมูลบางอย่างในหน่วยความจำที่จะถูกใช้โดยรหัสที่เหลือเช่นที่อยู่ด้านบนสุดของสแต็กอาจเป็นค่าที่ใช้ในการตรวจสอบสแต็กโอเวอร์โฟลว์ ฯลฯ

ในรหัสที่คุณกำลังมองหาที่ที่Stack_Sizeคงที่จะใช้ในการสำรองบล็อกของหน่วยความจำในพื้นที่โค้ด (ผ่านSPACEคำสั่งใน ARM ประกอบ) ที่อยู่ด้านบนของบล็อกนี้จะได้รับฉลาก__initial_spและจะถูกเก็บไว้ในตารางเวกเตอร์ (โปรเซสเซอร์ใช้รายการนี้เพื่อตั้งค่า SP หลังจากรีเซ็ตซอฟต์แวร์) รวมทั้งส่งออกเพื่อใช้ในไฟล์ต้นฉบับอื่น Heap_Sizeคงถูกนำมาใช้ในทำนองเดียวกันเพื่อสำรองบล็อกของหน่วยความจำและป้ายกำกับที่จะขอบเขตของมัน ( __heap_baseและ__heap_limit) จะถูกส่งออกสำหรับการใช้งานในแฟ้มแหล่งข้อมูลอื่น ๆ

; Amount of memory (in bytes) allocated for Stack
; Tailor this value to your application needs
; <h> Stack Configuration
;   <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>

Stack_Size      EQU     0x00000400

                AREA    STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem       SPACE   Stack_Size
__initial_sp


; <h> Heap Configuration
;   <o>  Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>

Heap_Size       EQU     0x00000200

                AREA    HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem        SPACE   Heap_Size
__heap_limit

…
__Vectors       DCD     __initial_sp               ; Top of Stack
                DCD     Reset_Handler              ; Reset Handler
                DCD     NMI_Handler                ; NMI Handler

…

                 EXPORT  __initial_sp
                 EXPORT  __heap_base
                 EXPORT  __heap_limit

คุณรู้หรือไม่ว่าค่าเหล่านั้นมีการกำหนดค่า 0x00200 และ 0x000400
Mahendra Gunawardena

@MahendraGunawardena ขึ้นอยู่กับคุณที่จะพิจารณาพวกเขาตามสิ่งที่โปรแกรมของคุณต้องการ คำตอบของ Niall ให้คำแนะนำเล็กน้อย
Gilles 'หยุดชั่วร้าย'

7

ขนาดของสแต็กและฮีปถูกกำหนดโดยแอปพลิเคชันของคุณไม่ใช่ที่ใดก็ได้ในแผ่นข้อมูลของไมโครคอนโทรลเลอร์

สแต็ค

สแต็กถูกใช้เพื่อเก็บค่าของตัวแปรโลคัลภายในของฟังก์ชันค่าก่อนหน้าของรีจิสเตอร์ CPU ที่ใช้สำหรับตัวแปรโลคัล (เพื่อให้สามารถเรียกคืนเมื่อออกจากฟังก์ชัน) ที่อยู่ของโปรแกรมที่จะกลับไปเมื่อออกจากฟังก์ชันเหล่านั้นบวก ค่าใช้จ่ายบางอย่างสำหรับการจัดการสแต็กเอง

เมื่อพัฒนาระบบฝังตัวคุณประมาณความลึกการโทรสูงสุดที่คุณคาดว่าจะมีเพิ่มขนาดของตัวแปรโลคัลทั้งหมดในฟังก์ชันในลำดับชั้นนั้นจากนั้นเพิ่มการเติมช่องว่างบางส่วนเพื่อให้ได้ค่าใช้จ่ายที่กล่าวถึงข้างต้น การขัดจังหวะใด ๆ ที่อาจเกิดขึ้นระหว่างการทำงานของโปรแกรมของคุณ

วิธีการประมาณค่าทางเลือกอื่น (โดยที่ RAM ไม่ได้ถูก จำกัด ) คือการจัดสรรพื้นที่สแต็กมากกว่าที่คุณต้องการเติมค่าสแต็กด้วยค่า Sentinel จากนั้นตรวจสอบจำนวนเงินที่คุณใช้จริงในระหว่างการดำเนินการ ฉันเคยเห็นรุ่นภาษา debug C debug ที่จะทำสิ่งนี้ให้คุณโดยอัตโนมัติ จากนั้นเมื่อคุณพัฒนาเสร็จแล้วคุณสามารถลดขนาดสแต็กได้ถ้าต้องการ

กอง

การคำนวณขนาดของกองที่คุณต้องการอาจเป็นเรื่องยาก กองถูกนำมาใช้สำหรับตัวแปรจัดสรรแบบไดนามิกดังนั้นถ้าคุณใช้malloc()และfree()ในโปรแกรมภาษา C หรือnewและdeleteใน C ++ ว่าที่ตัวแปรเหล่านั้นมีชีวิตอยู่

อย่างไรก็ตามใน C ++ โดยเฉพาะอาจมีการจัดสรรหน่วยความจำแบบไดนามิกซ่อนอยู่ ตัวอย่างเช่นถ้าคุณมีการจัดสรรวัตถุแบบคงที่ภาษาต้องการให้ตัวทำลายของมันถูกเรียกเมื่อโปรแกรมออก ฉันรู้ว่าอย่างน้อยหนึ่งรันไทม์ที่ที่อยู่ของ destructors ถูกเก็บไว้ในรายการเชื่อมโยงที่จัดสรรแบบไดนามิก

ดังนั้นในการประเมินขนาดของฮีปที่คุณต้องการดูที่การจัดสรรหน่วยความจำแบบไดนามิกทั้งหมดในแต่ละพา ธ ผ่านทรีสายของคุณคำนวณค่าสูงสุดและเพิ่มการเติมบางส่วน รันไทม์ภาษาอาจให้การวินิจฉัยที่คุณสามารถใช้เพื่อตรวจสอบการใช้ฮีปทั้งหมดการแตกแฟรกเมนต์ ฯลฯ


ขอบคุณสำหรับการตอบกลับฉันชอบวิธีกำหนดจำนวนเฉพาะเช่น 0x00400 และอื่น ๆ
Mahendra Gunawardena

5

นอกเหนือจากคำตอบอื่น ๆ ฉันต้องการเพิ่มเมื่อแกะสลัก RAM ระหว่างพื้นที่สแต็คและพื้นที่กองที่คุณต้องพิจารณาพื้นที่สำหรับข้อมูลคงที่ไม่คงที่ (เช่นไฟล์กลม, สถิติการทำงานและโปรแกรมทั้ง กลมจากมุมมอง C และอื่น ๆ อาจสำหรับ C ++)

การจัดสรรสแต็ค / ฮีปทำงานอย่างไร

เป็นที่น่าสังเกตว่าไฟล์ชุดประกอบเริ่มต้นเป็นวิธีหนึ่งในการกำหนดภูมิภาค Toolchain (ทั้งสภาพแวดล้อมการสร้างของคุณและสภาพแวดล้อมแบบรันไทม์) ส่วนใหญ่ใส่ใจเกี่ยวกับสัญลักษณ์ที่กำหนดจุดเริ่มต้นของสแต็กสเปซ (ใช้เพื่อเก็บตัวชี้สแต็กเริ่มต้นในตารางเวกเตอร์) และจุดเริ่มต้นและจุดสิ้นสุดของพื้นที่ฮีป ตัวจัดสรรหน่วยความจำซึ่งโดยทั่วไปแล้วจะใช้โดย libc ของคุณ)

ในตัวอย่างของ OP มีการกำหนดสัญลักษณ์เพียง 2 ตัวขนาดของสแต็กที่ 1kiB และขนาดของฮีปที่ 0B ค่าเหล่านี้จะถูกนำไปใช้ที่อื่นเพื่อสร้างพื้นที่สแต็กและฮีปจริง

ในตัวอย่าง @Gilles ขนาดจะถูกกำหนดและใช้ในไฟล์ชุดประกอบเพื่อตั้งค่าพื้นที่สแต็กเริ่มต้นทุกที่และขนาดที่ยั่งยืนโดยระบุสัญลักษณ์ Stack_Mem และตั้งป้ายกำกับ __initial_sp ที่ส่วนท้าย ในทำนองเดียวกันสำหรับฮีปซึ่งพื้นที่คือสัญลักษณ์ Heap_Mem (ขนาด 0.5kiB) แต่มีป้ายกำกับที่จุดเริ่มต้นและสิ้นสุด (__heap_base และ __heap_limit)

สิ่งเหล่านี้ได้รับการประมวลผลโดย linker ซึ่งจะไม่จัดสรรอะไรภายในพื้นที่สแต็กและพื้นที่ฮีปเนื่องจากหน่วยความจำนั้นถูกครอบครอง (โดยสัญลักษณ์ Stack_Mem และ Heap_Mem) แต่สามารถวางความทรงจำเหล่านั้นและกลมทั้งหมดได้ทุกที่ที่ต้องการ ป้ายกำกับท้ายสุดเป็นสัญลักษณ์ที่ไม่มีความยาวตามที่อยู่ที่ระบุ __initial_sp ใช้โดยตรงสำหรับตารางเวกเตอร์ในเวลาเชื่อมโยงและ __heap_base และ __heap_limit ด้วยรหัสรันไทม์ของคุณ ที่อยู่ที่แท้จริงของสัญลักษณ์ได้รับมอบหมายจาก linker ตามที่มันวางไว้

ดังที่ฉันได้กล่าวถึงข้างต้นสัญลักษณ์เหล่านี้ไม่จำเป็นต้องมาจากไฟล์ startup.s พวกเขาสามารถมาจากการกำหนดค่า linker ของคุณ (โหลดกระจายใน Keil, linkerscript ใน GNU) และในสิ่งที่คุณสามารถควบคุมตำแหน่งได้อย่างละเอียดยิ่งขึ้น ตัวอย่างเช่นคุณสามารถบังคับให้สแต็กอยู่ที่จุดเริ่มต้นหรือจุดจบของ RAM หรือทำให้กลมกลืนของคุณห่างจากกองหรือสิ่งที่คุณต้องการ คุณสามารถระบุได้ว่า HEAP หรือ STACK นั้นจะใช้งาน RAM ที่เหลืออยู่หลังจากวาง globals ไว้ หมายเหตุแม้ว่าคุณจะต้องระมัดระวังว่าการเพิ่มตัวแปรแบบคงที่เพิ่มเติมที่หน่วยความจำอื่นของคุณจะลดลง

อย่างไรก็ตามแต่ละ toolchain นั้นแตกต่างกันและวิธีเขียนไฟล์กำหนดค่าและสัญลักษณ์ใดที่ตัวจัดสรรหน่วยความจำแบบไดนามิกของคุณจะใช้จะต้องมาจากเอกสารประกอบของสภาพแวดล้อมเฉพาะของคุณ

การปรับขนาดสแต็ก

สำหรับวิธีการกำหนดขนาดของกองซ้อนชุดเครื่องมือจำนวนมากสามารถให้ความลึกของสแต็กได้สูงสุดโดยการวิเคราะห์แผนผังการเรียกใช้ฟังก์ชันของโปรแกรมของคุณหากคุณไม่ใช้การเรียกซ้ำหรือตัวชี้ฟังก์ชัน หากคุณใช้สิ่งเหล่านั้นให้ประเมินขนาดสแต็กและเติมค่าด้วยคาร์ดินาลล่วงหน้า (อาจจะผ่านฟังก์ชั่นการป้อนข้อมูลก่อนหน้าหลัก) จากนั้นตรวจสอบหลังจากโปรแกรมของคุณทำงานในขณะที่มีความลึกสูงสุด จบ) หากคุณออกกำลังกายโปรแกรมอย่างเต็มที่จนถึงขีด จำกัด คุณจะรู้ได้อย่างถูกต้องว่าคุณสามารถย่อขนาดสแต็กหรือถ้าโปรแกรมของคุณขัดข้องหรือไม่มีค่าสำคัญที่เหลืออยู่คุณต้องเพิ่มสแต็กและลองอีกครั้ง

การปรับขนาดกอง

การกำหนดขนาดของฮีพนั้นขึ้นอยู่กับแอพพลิเคชั่นอีกเล็กน้อย หากคุณทำการจัดสรรแบบไดนามิกเฉพาะในระหว่างการเริ่มต้นคุณสามารถเพิ่มพื้นที่ที่ต้องการในรหัสเริ่มต้นของคุณ (รวมทั้งค่าใช้จ่ายสำหรับการจัดการหน่วยความจำ) หากคุณสามารถเข้าถึงแหล่งที่มาของตัวจัดการหน่วยความจำของคุณคุณสามารถรู้ได้อย่างชัดเจนว่าค่าใช้จ่ายคืออะไรและอาจเขียนโค้ดเพื่อเดินหน่วยความจำเพื่อให้ข้อมูลการใช้งานแก่คุณ สำหรับแอปพลิเคชันที่ต้องการหน่วยความจำแบบรันไทม์แบบไดนามิก (เช่นการจัดสรรบัฟเฟอร์สำหรับเฟรมอีเธอร์เน็ตขาเข้า) ที่ดีที่สุดที่ฉันสามารถแนะนำคือการฝึกกองขนาดเล็กของคุณอย่างระมัดระวังและให้กองทุกอย่างที่เหลือหลังจากกองซ้อนและสถิต

หมายเหตุสุดท้าย (RTOS)

คำถามของ OP ถูกติดแท็กสำหรับโลหะเปลือย แต่ฉันต้องการเพิ่มบันทึกสำหรับ RTOS บ่อยครั้ง (เสมอ?) แต่ละงาน / กระบวนการ / เธรด (ฉันเพิ่งจะเขียนงานที่นี่เพื่อความเรียบง่าย) จะได้รับการกำหนดขนาดสแต็คเมื่อมีการสร้างงานนอกเหนือไปจากสแต็คงานจะมีระบบปฏิบัติการขนาดเล็ก กอง (ใช้สำหรับขัดจังหวะและเช่น)

โครงสร้างบัญชีงานและสแต็กต้องได้รับการจัดสรรจากที่ไหนสักแห่งและสิ่งนี้มักจะมาจากพื้นที่โดยรวมของฮีปของแอพพลิเคชันของคุณ ในกรณีเหล่านี้ขนาดสแต็กเริ่มต้นของคุณมักจะไม่สำคัญเนื่องจากระบบปฏิบัติการจะใช้ในระหว่างการเริ่มต้นเท่านั้น ยกตัวอย่างเช่นฉันได้เห็นการระบุพื้นที่ที่เหลือทั้งหมดในระหว่างการเชื่อมโยงถูกจัดสรรไปที่ HEAP และวางตัวชี้สแต็กเริ่มต้นที่ท้ายฮีปให้เติบโตเป็นฮีปโดยที่รู้ว่าระบบปฏิบัติการจะจัดสรรเริ่มต้นตั้งแต่ฮีปและ จะจัดสรรสแต็ก OS ก่อนที่จะละทิ้ง initial_sp สแต็ก จากนั้นจะใช้พื้นที่ทั้งหมดสำหรับการจัดสรรสแต็กงานและหน่วยความจำที่จัดสรรแบบไดนามิกอื่น ๆ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.