ทดลอง u-boot porting แบบเบื้องต้น บน QEMU mini2440

By | December 2, 2016

จากประสบการณ์ในการทำงานด้านการออกแบบระบบสมองกลฝังตัวของแอดมินที่ผ่านมา เราอาจจะเจอกับงานที่มีการเปลี่ยนแปลงไปจากบอร์ดอ้างอิง (Reference design) เนื่องจากเหตุผลเรื่องราคา ความต้องการของลูกค้า อายุของผลิตภัณฑ์ในตลาด แนวโน้มของเทคโนโลยี่ หรือข้อจำกัดเรื่องกฎหมายสิ่งแวดล้อม โดยเฉพาะอุปกรณ์พวกหน่วยความจำต่างๆ เช่น nand flash หรือ SDRAM ซึ่งอาจจะทำให้เฟิร์มแวร์ ของบอร์ดอ้างอิงทำงานไม่ได้บนบอร์ดต้นแบบ ทำให้เราต้องมีการแก้ไขโค้ดต้นฉบับให้สามารถทำงานกับบอร์ดที่ออกแบบมาใหม่ได้
การแก้ไขนี้จะมากน้อยขนาดไหน อยู่ที่โชคของแต่ละคนนะครับ ในบางงานเราอาจจะได้เป็นนักพัฒนากลุ่มแรกในโลกที่ได้ทำงานกับอุปกรณ์นั้น ข้อดีคือ มันท้าทายมาก ข้อเสียคือมันใหม่จนเราไม่สามารถปรึกษาใครได้เลย แม้กระทั่งกับ FAE (Field Application Engineer)

ในบทความนี้ จึงอยากนำตัวอย่างของการทำ u-boot porting ยูบูตเวอร์ชั่นเก่าที่รองรับบอร์ด mini2440 อยู่แล้ว มาเป็นเวอร์ชั่น 2016.07 มาให้ดูเป็นแนวทางสำหรับคนที่สนใจ แต่ต้องทำความเข้าใจก่อนว่าในตัวอย่างนี้เป็นการทำเพื่อศึกษาเท่านั้น เพราะโดยปกติถ้าหากเรามีบูตโหลดเดอร์ที่ทำงานได้อยู่แล้ว และไม่มีความจำเป็นต้องใช้ฟีเจอร์ใหม่ๆของโค้ดเวอร์ชั่นที่ใหม่กว่า ก็ไม่มีความจำเป็นที่จะต้องพอร์ตโค้ดไปเป็นเวอร์ชั่นใหม่เลย

เลือกบอร์ดเป้าหมาย

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

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

mini2440-35

รายละเอียดสเปคของ mini2440 สามารถอ่านได้จาก http://www.friendlyarm.net/products/mini2440

เปรียบเทียบ u-boot 1.3.2 vs u-boot 2016.07

โค้ดยูบูตที่รองรับบอร์ด mini2440 เวอร์ชั่นล่าสุดที่สามารถหาได้จากอินเตอร์เน็ต คือเวอร์ชั่น u-boot 1.3.2 และตัวโปรเจคได้หยุดการอัพเดทตั้งแต่ปี 2012 แล้ว เพราะฉะนั้นการพอร์ตมาเป็นเวอร์ชั่น 2016.07 ก็ดูจะน่าสนใจอยู่บ้าง

เราเริ่มจากการสำรวจโครงสร้างไดเรคทอรี่ ของยูบูต 1.3.2 เทียบกับ เวอร์ชั่น 2016.07 จะพบว่ามีความแตกต่างกันอยู่หลายส่วน เนื่องจากในยูบูตเวอร์ชั่นตั้งแต่ 2014.10 ได้เปลี่ยนมาใช้ kbuild และ kconfig ในการกำหนดขั้นตอนในการสร้างส่วนประกอบต่างๆแทนที่จะรวมไว้ใน Makefile อย่างเดียว เนื่องจากตัวโปรเจคเริ่มมีขนาดใหญ่ขึ้นรวมถึงต้องการให้ยูบูตซอสโค้ดมีความสามารถในการปรับแต่งโมดูลต่างได้แบบเดียวกับ ลินุกซ์เคอร์เนล (make menuconfig)

ubootdircompare_bg

จากการเปรียบเทียบไดเรคทอรี่ชั้นแรกจะพบว่า ไดเรคทอรี่ส่วนที่เป็นขึ้นอยู่กับสถาปัตยกรรม (architecture specific) ได้ถูกย้ายและรวบรวมไว้ภายใต้ arch ทั้งหมด ซึ่งช่วยดูให้เข้าใจง่ายขึ้นมาก

เหตุผลที่ยกเรื่องโครงสร้างไดเรคทอรี่มาให้ดูด้านบน เพราะอยากให้เข้าใจว่าส่วนใดของโปรเจคที่เราต้องเข้าไปแก้ไข เนื่องจากเราต้องการทำให้โค้ดของโปรเจคนี้รองรับบอร์ด mini2440 ดังนั้นส่วนที่เป็น board specific ก็จะเป็นส่วนหลักที่เราจะเข้าไปทำงานในครั้งนี้ แต่ก็ไม่ได้หมายความว่าเราจะไม่ต้องไปแก้ส่วนอื่นเลย เพราะยังไงก็ยังมีส่วนที่เกี่ยวข้องกันอยู่ ยกตัวอย่างเช่น หากมีความต้องการเพิ่มไดร์เวอร์โค้ดของ NAND flash ที่ใช้อยู่บน mini2440  เราก็จำเป็นต้องเข้าไปแก้ในส่วน generic code ด้วย

เลือกวิธีกันก่อน

การพอร์ตโค้ดเพื่อให้สามารถทำงานบนบอร์ดเป้าหมายได้นั้น สามารถทำได้หลายวิธี ตั้งแต่เริ่มต้นเขียนโค้ดทั้งหมดขึ้นมาใหม่ หรือดัดแปลงจากบอร์ดที่ใกล้เคียงที่ โค้ดเวอร์ชั่นใหม่รองรับอยู่แล้ว ซึ่งในที่นี้เราจะเลือกวิธีหลัง เพราะจากการสำรวจ ไดเรคทอรี่ของโค้ด u-boot 2016.07 นั้น พบว่า ได้รองรับบอร์ด smdk2410 ของ Samsung ซึ่งใช้ซีพียู ในรุ่นเดียวกัน ทำให้เราสามารถใช้ทั้งโค้ด start.S , ไฟล์ ดีฟอล์ทคอนฟิก เป็นไฟล์ตั้งต้นได้ทันที

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

การแก้ไขระบบการสร้างให้รองรับบอร์ดใหม่ (Adding new board to build system)

ไฟล์ README.kconfig แนะนำให้ตั้งค่าของตัวแปรคอนฟิก CONFIG_SYS_CPU , CONFIG_SYS_SOC และ CONFIG_SYS_VENDOR ใน Kconfig เพื่อรองรับบอร์ดใหม่ ซึ่งเท่าที่ไล่ดูจากไฟล์คอนฟิกของ smdk2410 จะพบว่าตัวแปรเหล่านี้จะถูกตั้งค่าโดยอ้อมจากตัวแปร CONFIG_TARGET_{BOARDNAME} ของ configs/smdk2410_defconfig ซึ่งทำให้ ตัวแปร CONFIG_CPU_ARM920T ภายใน arch/arm/Kconfig ถูกสร้างแล้วทำให้ CONFIG_SYS_CPU ถูกกำหนดค่าเป็น “arm920t”  ตามลำดับอีกทีนึง

ตัวแปร CONFIG_TARGET_{BOARDNAME} ของ smdk2410 นั้นถูกกำหนดค่ามาจากไฟล์ configs/smdk2410_defconfig ซึ่งเราก็จะทำเหมือนกันสำหรับ mini2440 เพื่อคง “สไตล์” ของเดิมไว้ รวมถึงป้องกันปัญหาจากการแก้ไขที่แตกต่างไปจากแพลตฟอร์มอ้างอิง (ในที่นี้คือ smdk2410)

สร้าง default config

เราจะเพิ่มไฟล์ดีฟอล์ทคอนฟิก สำหรับบอร์ด mini2440 เข้าไปก่อน เพื่อที่จะทำให้สามารถใช้ คำสั่ง make mini2440_defconfig เหมือนกับบอร์ดอื่นๆที่ u-boot 2016.07 รองรับ โดยคัดลอกมาจากไฟล์ configs/smdk2410_defconfig มาเป็น configs/mini2440_defconfig แล้วเปลี่ยนแปลงค่าตัวแปรบางตัว ด้านล่างเปรียบเทียบไฟล์ทั้งสองแบบ บรรทัดต่อบรรทัด

compare_defconfig

คลิกเพื่อดูโค้ดใน diff format

เพิ่มส่วนรองรับบอร์ดใหม่แบบเฉพาะเจาะจง (Board specific support files)

โค้ดสำหรับรองรับบอร์ดใหม่ โดยทั่วๆไป ควรวางไว้ภายใต้ board/<vendor>/<board>/ โดยที่ <vendor> จะถูกกำหนดค่ามาจากตัวแปร CONFIG_SYS_VENDOR และ <board> จะถูกกำหนดค่ามาจากตัวแปร CONFIG_SYS_BOARD

สำหรับ mini2440 ในที่นี้ เราจะกำหนดให้ vendor = “friendlyarm” และ board = “mini2440” สำหรับการประยุกต์ไปใช้กับบอร์ดอื่นๆ สามารถกำหนดได้ตามใจชอบนะครับ

ไฟล์ที่อยู่ภายใต้ board/friendlyarm/mini2440 มีจำนวน 4 ไฟล์ดังนี้

board
└── friendlyarm
└── mini2440
├── Kconfig
├── lowlevel_init.S
├── Makefile
└── mini2440.c

Kconfig และ Makefile สามารถ copy มาจาก board/samsung/smdk2410/ แล้วแก้ไขให้รองรับบอร์ด mini2440 ได้ Kconfig ของบอร์ดไฟล์นี้ ทำหน้าที่กำหนดค่าของ ตัวแปร CONFIG_SYS_BOARD, CONFIG_SYS_VENDOR, CONFIG_SYS_SOC และ CONFIG_SYS_CONFIG_NAME

compare_boardkconfig

คลิกเพื่อดูโค้ดใน diff format

lowlevel_init.S และ mini2440.c เราจะใช้ไฟล์จาก u-boot 1.3.2 แล้วแก้ไขบางส่วนให้ทำงานกับ u-boot 2016.07 ได้ รายละเอียดการแก้ไข จะอธิบายในส่วนหลังนะครับ

เพิ่มเฮดเดอร์ไฟล์ของบอร์ดเป้าหมาย

เฮดเดอร์ไฟล์สำหรับบอร์ดใหม่จะต้องเพิ่มเข้าไปใน include/configs/<target>.h เพื่อใช้กำหนดค่าคงที่ต่างๆ เช่น ค่าตำแหน่ง,ค่าเริ่มต้น ของรีจิสเตอร์ หรือตัวหารพรีสเกลเลอร์ของสัญญาณนาฬิกา รวมถึงเปิดใช้งานความสามารถต่างๆ ในขั้นตอนพรีโปรเซสเซอร์ ขณะคอมไพล์

เราจะคัดลอก include/configs/smdk2410.h มาเป็น include/configs/mini2440.h เนื่องจากใช้ชุดรีจิสเตอร์เดียวกันและแอดมินตั้งใจจะใช้ความสามารถพื้นฐานเดียวกับ smdk2410 ก่อน เพื่อให้แน่ใจว่าโค้ดสามารถทำงานได้ แล้วจึงจะเพิ่มความสามารถอื่นๆ เข้าไปภายหลัง

เพิ่มบอร์ดใหม่ในเมนู (Add a new entry to the board select menu)

ส่วนกำหนด board select menu อยู่ใน arch/<arch>/Kconfig หรือ arch/<arch>/*/Kconfig ในตัวอย่างของเราคือ arch/arm/Kconfig ซึ่งเราต้องเพิ่มให้ไฟล์นี้รวม board/friendlyarm/mini2440/Kconfig ที่เราเพิ่มเข้าไปใหม่ด้วย diff code ด้านล่างแสดงส่วนที่ต้องแก้ไข

compare_arch_kconfig

คลิกเพื่อดูโค้ดใน diff format

ตอนนี้ระบบการสร้างของยูบูตก็รองรับบอร์ด mini2440 ตามที่เราต้องการแล้ว เราสามารถใช้คำสั่ง make mini2440_defconfig เพิ่อตั้งค่าพื้นฐาน รวมถึงคำสั่ง make menuconfig เพื่อปรับแต่งความสามารถอย่างอื่นเพิ่มเติมได้

การแก้ไขโค้ดให้ทำงานได้

include/configs/mini2440.h

ไฟล์นี้แอดมินคัดลอกมาจาก include/configs/smdk2410.h และต้องแก้ไขบางจุด

compare_board_header

คลิกเพื่อดูโค้ดใน diff format

บรรทัดที่ 25 เนื่องจาก mini2440 QEMU ใช้ entry point เป็นตำแหน่ง 0x33F80000 (SDRAM) แทนตำแหน่ง 0x00000000 (NAND flash) ทำให้เราจำเป็นต้องแก้ค่า CONFIG_SYS_TEXT_BASE เพื่อให้โปรแกรม QEMU โหลดโปรแกรมยูบูตที่ได้จากการคอมไพล์ของเรา สอดคล้องกับ entry point ของ QEMU

อธิบายเพิ่มเติม เนื่องจากบอร์ด mini2440 ของจริงนั้นสามารถบูตได้ทั้งจาก NOR flash หรือ NAND flash โดยเลือกจากสวิตซ์ S2 บนบอร์ด และใช้ตำแหน่ง 0x00000000 เป็นค่าตำแหน่งเริ่มต้น ทำให้โปรแกรมยูบูตเริ่มทำงานที่ ROM mode ก่อนแล้วจึงทำการย้ายตำแหน่งไปทำงานบน RAM mode ทีหลัง
แต่สำหรับตัวโปรแกรม QEMU เลือกที่จะใช้ตำแหน่ง 0x33F80000 เป็นค่าตำแหน่งเริ่มต้น และยังใช้วิธีโหลดยูบูต (ELF format) ไปยังเมมโมรี่ตามที่ระบุไว้ในเฮดเดอร์ของไฟล์ ELF เราจึงจำเป็นต้องแก้ค่า CONFIG_SYS_TEXT_BASE ให้เป็น 0x33F80000 เพื่อที่จะได้ตรงกับตำแหน่งเริ่มต้นของ QEMU เนื่องจากขั้นตอนลิ้งเกอร์ จะใช้ค่านี้ในการระบุตำแหน่งในไฟล์ ELF

board/friendlyarm/mini2440/mini2440.c

ไฟล์นี้แอดมินเอามาจากยูบูต 1.3.2 แล้วแก้ไขให้ทำงานได้บนยูบูต 2016.07 ด้านล่างแสดงความต่างของโค้ดก่อนและหลังแก้ไข

compare_board_file

คลิกเพื่อดูโค้ดใน diff format

จะเห็นว่าส่วนที่ต้องแก้ไขนั้นมีหลายบรรทัดเหมือนกัน แต่ยังถือว่าไม่มากเท่าไรนัก
บรรทัด 32 แสดงการแก้ไข path ของเฮดเดอร์ไฟล์ให้ตรงกับโครงสร้างของยูบูต 2016.07
บรรทัด 33 เราต้องรวม asm/arch/s3c24x0.h เข้ามาเพิ่มด้วย สำหรับรีจิสเตอร์ที่ต้องอ้างถึง
บรรทัด 59 ขั้นตอนการทำงานของยูบูต 2016.07 แบ่ง board initialize เป็นสองช่วงตามที่เคยอธิบายไว้ในบทความเรื่องขั้นตอนการทำงานของยูบูต แต่ในยูบูต 1.3.2 จะมีแค่ช่วงเดียว เราจึงต้องแยกฟังก์ชั่นนี้ออกเป็นสองส่วนและเปลี่ยนชื่อฟังก์ชั่นส่วนแรกเป็น board_early_init_f()
บรรทัด 60-176 ส่วนใหญ่เกือบทั้งหมดจะเป็นการแก้ชื่อรีจิสเตอร์จากตัวพิมพ์ใหญ่ไปเป็นตัวพิมพ์เล็กตามรูปแบบที่ยูบูต 2016.07 ใช้อ้างถึง
บรรทัด 184 board initialize ส่วนที่สอง
บรรทัด 273-282 ยูบูต 2016.07 นั้นต้องการฟังก์ชั่น board_flash_get_legacy() อันนี้แอดมินไปลอกมาจาก smdk2410.c เกือบทั้งดุ้น แต่ที่จริงเราก็ยังไม่ได้ใช้งานอะไร

board/friendlyarm/mini2440/lowlevel_init.S

ไฟล์นี้แอดมินเอามาจากยูบูต 1.3.2 แล้วแก้ไขบรรทัด 134 แค่ค่า TEXT_BASE ให้เป็น CONFIG_SYS_TEXT_BASE ตามรูปแบบของยูบูต 2016.07

compare_mini2440_lowlevelinit

คลิกเพื่อดูโค้ดใน diff format

common/board_f.c

ไฟล์นี้เป็นโค้ดส่วน generic ส่วนที่ต้องแก้ไขแสดงด้านล่าง

compare_board_f

คลิกเพื่อดูโค้ดใน diff format

บรรทัดที่ 360-368 การแก้ไขส่วนนี้ จริงๆ แล้วหากโค้ดนี้นำไปใช้กับบอร์ดจริงก็คงไม่จำเป็นอะไร แต่เนื่องจากเราใช้ mini2440 QEMU ในการทดสอบยูบูตที่ได้จากการคอมไพล์ แล้ว QEMU มันดันโหลดยูบูตของเราไปบนตำแหน่ง 0x33F80000 ตั้งแต่ตอนเริ่มต้น ทำให้เกิดปัญหาในขั้นตอนย้ายยูบูตไบนารี่ (relocation) เนื่องจากมันเป็นตำแหน่งเดียวกันเป๊ะ จึงเกิดการทำลายตัวเองขึ้น
ทางแก้ของแอดมินคือขยับตำแหน่งการย้ายของยูบูตลงมา 0x1000000 ไบต์ เพื่อไม่ให้เกิดการเขียนทับตัวเอง โค้ดส่วนนี้ยังสามารถทำงานได้บนบอร์ดจริงด้วย

สำหรับคนที่เคยใช้ mini2440 QEMU กับ ยูบูต 1.3.2 คงมีคำถามว่าแล้วยูบูต 1.3.2 แก้ปัญหานี้อย่างไร แอดมินต้องบอกว่าตัวบูตโค้ดของ 1.3.2 (cpu/arm920t/start.S) มันเทพกว่าตัว 2016.07 คือมันมีส่วนการเช็คว่าตัวโค้ดถูกสตาร์ทมาจากตำแหน่ง 0x00000000 หรือไม่ ซึ่งถ้าใช่ มันจะข้ามขั้นตอนการย้ายตำแหน่งไปเลย จึงไม่เกิดปัญหานี้

ผลการทดสอบ

ผลการรัน u-boot.bin ที่ได้จากการแก้ไขโค้ด u-boot 2016.07 แสดงอยู่ในตารางด้านล่าง โดยเบื้องต้นแค่ทำให้มันสามารถทำงานไปจนถึง คอมมานด์พร้อมนี่ก็เล่นเอาหืดขึ้นคอเหมือนกัน แต่ก็สนุกดี ลองใช้คำสั่ง bdinfo ให้แสดงค่าตำแหน่งของเมมโมรี่ สำหรับค่าต่างๆ ก็ดูปกติ แต่จากเอาพุตนี้จะยังเห็น NAND flash แสดงค่า 0 ไบต์ เนื่องจากเรายังไม่ได้แก้ไข NAND driver

./../arm-softmmu/qemu-system-arm -M mini2440 -serial stdio -mtdblock ./mini2440_nand128.bin -show-cursor -usb -usbdevice keyboard -usbdevice mouse -monitor telnet::5555,server,nowait
mini2440_init: Boot mode: NAND
S3C: CLK=240 HCLK=240 PCLK=240 UCLK=57
QEMU: ee24c08_init
DM9000: INIT QEMU MAC : 52:54:00:12:34:56
QEMU mini2440_reset: loaded default u-boot from NAND
QEMU mini2440_reset: loaded override u-boot (size 6c400)
S3C: CLK=240 HCLK=120 PCLK=60 UCLK=57
S3C: CLK=240 HCLK=60 PCLK=30 UCLK=57
S3C: CLK=240 HCLK=60 PCLK=30 UCLK=48
S3C: CLK=405 HCLK=101 PCLK=50 UCLK=48


U-Boot 2016.07-g27bb79a (Dec 02 2016 - 20:49:25 +0700)

CPUID: 32440001
FCLK: 202.500 MHz
HCLK: 202.500 MHz
PCLK: 101.250 MHz
DRAM: 64 MiB
WARNING: Caches not enabled
Flash: 0 Bytes
NAND: 0 MiB
*** Warning - bad CRC, using default environment

In: serial
Out: serial
Err: serial
Net: Net Initialization Skipped
No ethernet found.
MINI2440 # printenv
baudrate=115200
bootdelay=5
ipaddr=10.0.0.110
netmask=255.255.255.0
serverip=10.0.0.1
stderr=serial
stdin=serial
stdout=serial

Environment size: 144/65532 bytes
MINI2440 # bdinfo
arch_number = 0x000007CF
boot_params = 0x30000100
DRAM bank = 0x00000000
-> start = 0x30000000
-> size = 0x04000000
current eth = unknown
ip_addr = 10.0.0.110
baudrate = 115200 bps
TLB addr = 0x32FF0000
relocaddr = 0x32F81000
reloc off = 0xFF001000
irq_sp = 0x32B70EF0
sp start = 0x32B70EE0
MINI2440 # reset
resetting ...

สำหรับคนที่สนใจอยากลองศึกษาโค้ดตัวอย่างเพิ่มเติมสามารถ pull ได้จาก github ตาม URL ด้านล่างนี้นะครับ

https://github.com/embedded-maker/u-boot-2016.07_mini2440.git

แล้วเจอกันใหม่ในโปรเจคต่อไปครับ