ข้ามไปยังเนื้อหา

Make และตัวแปรของการ build

เมื่อจบบทเรียนนี้ คุณจะ

  1. เปิดแคตตาล็อกตัวอย่างของ SDK ด้วย ENABLE_PAGE_EXAMPLES=1 และเลือกรันตัวอย่างฝั่ง CM33 ด้วย SDK_EXAMPLE_CM33 ได้
  2. อธิบายความต่างของการกำหนดค่าตัวแปร Make แบบ ?= กับ = และผลต่อการ build
  3. อธิบายว่าทำไมตัวอย่างที่ปิดไว้จึงไม่เพิ่มขนาดเฟิร์มแวร์เลย

ใช้เวลาประมาณ 70 นาที (แนวคิด 15 · ฝึก 25 · แล็บ 25 · เช็ก 5) ส่วนแนวคิดและแบบฝึกใช้แค่ GNU Make บนคอมพิวเตอร์ ไม่ต้องคอมไพล์อะไร

ทวนจากบทเรียน 2.1 สองข้อ

  1. ทำไมต้องรัน make getlibs ในทั้งสามโปรเจกต์ แทนที่จะรันครั้งเดียวที่โฟลเดอร์บนสุด
  2. บรรทัดไหนบน serial console ที่เป็นหลักฐานว่าบอร์ด mtb-only บูตสำเร็จ

เปิด examples/flags.mk ไฟล์นี้ไม่คอมไพล์อะไร มันแค่พิมพ์ว่า make เข้าใจตัวแปรแต่ละตัวอย่างไร ทายก่อนรัน ว่าบรรทัด SOURCES จะมีกี่ไฟล์ในแต่ละแบบ แล้วรันทีละบรรทัด

Terminal window
make -f examples/flags.mk
make -f examples/flags.mk ENABLE_PAGE_EXAMPLES=1 SDK_EXAMPLE_CM33=cm33/io/04_gpio_led_button
ENABLE_PAGE_EXAMPLES=1 make -f examples/flags.mk

แบบแรกได้ 2 ไฟล์ อีกสองแบบได้ 4 ไฟล์ และ make บอกด้วยว่าค่ามาจากไหน (file, command line, environment) สังเกตสองบรรทัดที่แปลกตา FROZEN ได้แค่ hello ทั้งที่ GREETING ได้ hello world และ TRAILING ที่ดูเหมือนเป็น 1 แต่ equals 1: no สามเรื่องนี้คือเรื่องทั้งหมดของบทเรียน

แม่แบบมี Makefile สองชั้น ชั้นบนสุด (Makefile) บอกแค่ว่าแอปพลิเคชันนี้มีโปรเจกต์อะไรบ้าง MTB_TYPE=APPLICATION กับ MTB_PROJECTS=proj_cm33_s proj_cm33_ns proj_cm55 แล้ว include ระบบ build ของ ModusToolbox ส่วน Makefile ของแต่ละโปรเจกต์ เช่น proj_cm33_ns/Makefile เรียงแบบนี้

ส่วน ตัวอย่างจากไฟล์ ทำอะไร
ค่าร่วม include ../common.mk variant, ที่อยู่ของ workspace
ชื่อและคอร์ APPNAME=proj_cm33_ns CORE=CM33 บอก ModusToolbox ว่า build ให้คอร์ไหน
component COMPONENTS+=FREERTOS RTOS_AWARE เปิดโฟลเดอร์ที่ชื่อขึ้นต้นด้วย COMPONENT_ ที่ตรงกัน
ตัวแปรของเรา ENABLE_PAGE_EXAMPLES ?= 0 ค่าเริ่มต้นที่ผู้ใช้เปลี่ยนได้
ส่งต่อให้ C DEFINES+=ENABLE_PAGE_EXAMPLES=$(ENABLE_PAGE_EXAMPLES) กลายเป็น -D ให้โค้ดใช้ #if ได้
ตัดโฟลเดอร์ออก CY_IGNORE += examples ไม่ให้ ModusToolbox หาซอร์สในโฟลเดอร์นั้น
ไลบรารี LDLIBS += .../libbento_hsm.a ส่งไฟล์ .a ให้ linker
ท้ายสุด include $(CY_TOOLS_DIR)/make/start.mk (บรรทัด 530) ระบบ build อ่านตัวแปรทั้งหมดข้างบนตรงนี้

ลำดับมีผลจริง ตารางแก้ปัญหาใน README ของแม่แบบบันทึกอาการ “Linker cannot find a function from lib/” ไว้ว่าเกิดจาก LDLIBS ถูกตั้งหลัง include start.mk “ModusToolbox reads it while including that file; anything later never reaches the linker.”

เขียน ความหมาย ขยายค่าเมื่อ
A = x recursive: เก็บข้อความไว้ ขยายทุกครั้งที่ถูกใช้ ตอนใช้ ตัวแปรที่นิยามทีหลังจึงมีผล
A := x simple: ขยายทันทีแล้วเก็บผล ตอนอ่านบรรทัดนั้น
A ?= x ให้ค่าเฉพาะเมื่อ A ยังไม่ถูกนิยาม ค่าจาก environment นับว่านิยามแล้ว
A += x ต่อท้ายค่าเดิม ตามชนิดเดิมของ A

ค่าจาก command line (make build X=1) ชนะการกำหนดค่าในไฟล์ทุกแบบ ส่วนค่าจาก environment แพ้ = และ := ในไฟล์ แต่ชนะ ?= นี่คือเหตุผลที่ SDK ประกาศ ENABLE_PAGE_EXAMPLES ?= 0 และ SDK_EXAMPLE_CM33 ?= (บรรทัด 321-322) ถ้าเขียน ENABLE_PAGE_EXAMPLES = 0 คำสั่ง make build ENABLE_PAGE_EXAMPLES=1 ก็ยังได้ผล แต่การตั้งค่าไว้ใน environment ของเครื่อง build จะถูกกลืนหายเงียบ ๆ

อีกเรื่องที่ต้องรู้คือ ifeq ถูกตัดสิน ตอน make อ่านบรรทัดนั้น คอมเมนต์ในไฟล์เดียวกันเตือนว่า ENABLE_PAGE_BENTO_BUDDY “must be assigned BEFORE the CY_IGNORE ifneq below evaluates it — otherwise the variable is empty at parse time” (บรรทัด 60-64) และ ifeq เทียบข้อความตรงตัว ค่า 1 ที่มีช่องว่างต่อท้ายไม่เท่ากับ 1 เอกสาร SDK บันทึกกับดักนี้เป็น Appendix X #23 ฟีเจอร์หายไปทั้งก้อนโดยไม่มี error ที่ไหนเลย ส่วน common.mk ป้องกันไว้ด้วยการ $(strip) ค่าของ BENTO_VARIANT แล้วจงใจให้ build ล้มด้วย $(error ...) ถ้าค่าไม่ใช่ mtb-mpy หรือ mtb-only (บรรทัด 80-89) เพราะ “a typo must not quietly select the wrong firmware”

3. ทำไมตัวอย่างที่ปิดไว้ไม่เพิ่มขนาดเฟิร์มแวร์เลย

หัวข้อที่มีชื่อว่า “3. ทำไมตัวอย่างที่ปิดไว้ไม่เพิ่มขนาดเฟิร์มแวร์เลย”

ModusToolbox ไม่ได้อ่านรายการซอร์สจาก Makefile มัน เดินทั้งโฟลเดอร์ของโปรเจกต์แล้วคอมไพล์ทุกไฟล์ที่เจอ คอมเมนต์ใน Makefile ของ CM33 อธิบายผลที่ตามมา

# Compiled ONLY with ENABLE_PAGE_EXAMPLES=1.
#
# MTB DISCOVERS SOURCES BY WALKING THE APP TREE. `SOURCES +=` adds; it does not
# subtract, and it cannot exclude a file the walk already found. Only CY_IGNORE
# removes one. Gating a subdirectory with `ifeq (...) SOURCES += wildcard`
# therefore does NOTHING — the files compile anyway, and the first symptom is a
# wall of "No such file or directory" for headers that subdirectory needs.
# Every exclusion below is a CY_IGNORE for that reason.
#
# sdk_examples_cm33_table.c is GENERATED by tools/gen_examples_table.py.
ifeq ($(ENABLE_PAGE_EXAMPLES),1)
INCLUDES += examples
# ... (บรรทัด 506-523: ยกเว้นบางกลุ่มแม้เปิดตัวอย่าง)
else
CY_IGNORE += examples
endif

ที่มา: proj_cm33_ns/Makefile บรรทัด 494-527 (Apache-2.0, tesaiot-pse84-devkit-sdk) ตัดบรรทัด 506-523 ออกเพื่อความสั้น

เมื่อธงเป็น 0 โฟลเดอร์ examples ถูก CY_IGNORE ทั้งโฟลเดอร์ ไฟล์ในนั้นไม่ถูกคอมไพล์ จึงไม่มี object ไม่มีอะไรให้ linker ใส่ลงใน image อีกชั้นหนึ่งอยู่ในโค้ด C: main.c เรียก sdk_examples_cm33_start() เฉพาะใน #if ENABLE_PAGE_EXAMPLES (บรรทัด 384-388) ถ้าปิดธงแล้วยังมีการเรียกหลงอยู่ build จะล้มที่ขั้น link ซึ่งดีกว่าการได้ image ที่มีของครึ่ง ๆ กลาง ๆ README ของแคตตาล็อกสรุปว่าเมื่อปิดธง “not one byte of this tree reaches the firmware image”

examples/flags.mk ทำงานเป็นสี่ท่า

  • ท่าที่ 1 ?= ให้ค่าเริ่มต้น แล้วพิมพ์ $(origin ...) บอกว่าค่ามาจากไฟล์ command line หรือ environment
  • ท่าที่ 2 GREETING = hello $(WHO) กับ FROZEN := hello $(WHO) ประกาศก่อน WHO ตัวแรกขยายตอนใช้จึงได้ hello world ตัวหลังขยายทันทีจึงได้ hello
  • ท่าที่ 3 รายการ SOURCES เปลี่ยนตาม ENABLE_PAGE_EXAMPLES และ DEFINES ส่งทั้งสองธงต่อให้โค้ด C
  • ท่าที่ 4 ค่า TRAILING มีช่องว่างต่อท้ายเพราะคอมเมนต์ท้ายบรรทัด ifeq จึงไม่เท่ากับ 1 จนกว่าจะ $(strip)

ลองแก้แล้วทายก่อนรัน

  1. ย้ายบรรทัด WHO = world ขึ้นไปไว้ก่อน FROZEN := ... ผลของ FROZEN เปลี่ยนเป็นอะไร
  2. เปลี่ยน ENABLE_PAGE_EXAMPLES ?= 0 เป็น ENABLE_PAGE_EXAMPLES = 0 แล้วรันทั้งสามแบบอีกครั้ง แบบไหนเปลี่ยนผล
  3. รัน make -f examples/flags.mk 'ENABLE_PAGE_EXAMPLES=1 ' (มีช่องว่างในเครื่องหมายคำพูด) ไฟล์นี้รับได้เพราะ $(strip) แล้ว ifeq ($(ENABLE_PAGE_EXAMPLES),1) ในบรรทัด 504 ของ Makefile จริงของ SDK ซึ่งไม่ได้ strip จะทำอะไรกับค่าเดียวกัน

เปิด practice/feature.mk โจทย์คือทำให้ feature/feature.c ถูกคอมไพล์เฉพาะเมื่อ ENABLE_FEATURE เป็น 1 โดยค่าเริ่มต้นปิด เปิดได้ทั้งจาก command line และ environment และทนช่องว่างต่อท้ายได้ มีช่องให้เติม 2 จุด ตรวจงานด้วย

Terminal window
make -f practice/feature.mk check

ก่อนแก้คุณจะเห็น FAIL สองบรรทัด และ make จบด้วย error เพราะ check คืนค่าที่ไม่ใช่ 0 เมื่อมี FAIL แม้บรรทัดเดียว (การตรวจที่พิมพ์ FAIL แต่คืน 0 จะเขียวเสมอเมื่อนำไปรันใน CI เรื่องนี้กลับมาในบทเรียน 6.2) ลองแก้ทีละ TODO แล้วรัน check ทุกครั้ง จะเห็นว่าแต่ละ TODO ทำให้บรรทัดไหนเปลี่ยนจาก FAIL เป็น PASS

ลองเองก่อนอย่างน้อย 15 นาที แล้วเปิด solution/feature.mk TODO 1 คือเปลี่ยน := เป็น ?= และ TODO 2 คือเริ่ม SOURCES โดยไม่มี feature/feature.c แล้วเพิ่มด้วย += ใน ifeq ($(strip $(ENABLE_FEATURE)),1) ให้สังเกตว่าบรรทัด “command line turns it on” ผ่านตั้งแต่ก่อนแก้ เพราะค่าจาก command line ชนะทั้ง := และ ?= test ที่ผ่านตั้งแต่แรกไม่ได้พิสูจน์ว่าโค้ดของคุณถูก มันพิสูจน์แค่ว่ากรณีนั้นไม่ได้แยกคำตอบถูกออกจากคำตอบผิด เรื่องนี้กลับมาในบทเรียน 6.1

ตอบคำถาม 5 ข้อใน quiz.yaml (บนเว็บไซต์อยู่ท้ายหน้านี้) ครอบคลุมเป้าหมายทั้งสามข้อ ตอบถูกตั้งแต่ 4 ข้อขึ้นไปถือว่าจบบทเรียน

งาน: เปิดแคตตาล็อกตัวอย่าง เลือกรันตัวอย่างฝั่ง CM33 หนึ่งตัว และวัดว่าการเปิดตัวอย่างเปลี่ยนขนาดเฟิร์มแวร์เท่าไร

  1. build แบบปิดตัวอย่างก่อน (ค่าเริ่มต้น) แล้วหาไฟล์ ELF ของ CM33_NS และวัดขนาดของแต่ละ section
    Terminal window
    make build -j
    find proj_cm33_ns/build -name "*.elf"
    arm-none-eabi-size <ไฟล์ .elf ที่หาเจอ>
    จดคอลัมน์ text data bss
  2. build ใหม่แบบเปิดตัวอย่าง และไม่เลือกตัวไหน แล้ววัดอีกครั้ง
    Terminal window
    make build -j ENABLE_PAGE_EXAMPLES=1
    flash ด้วย make program ถอดสายเสียบใหม่ แล้วอ่าน serial console ตัวรันจะพิมพ์รายการ === TESAIoT SDK examples on CM33_NS (...) === และบอกวิธีรันด้วย SDK_EXAMPLE_CM33=<id> จดรูปแบบของ id ที่มันพิมพ์ (เช่น cm33/io/04_gpio_led_button)
  3. build อีกครั้งโดยเลือกตัวอย่างหนึ่งตัว เช่น SDK_EXAMPLE_CM33=cm33/sensors/01_i2c_bus_scan แล้วดูบรรทัด [sdk-example] running ... และบรรทัดผลลัพธ์ [sdk-example] ... -> 0 (ok) หรือรหัสอื่น
  4. ลองชื่อที่ไม่มีอยู่จริงหนึ่งครั้ง เช่นตัวอย่างในหัวข้อ “Turning them on” ของ README ของแคตตาล็อก ที่ใช้ tesaiot_hsm/01_acquire_chip ตัวรันตอบว่าอะไร แล้วเทียบกับรายการที่ตัวรันพิมพ์เอง ข้อไหนคือความจริงของ commit นี้
  5. เทียบขนาดจากข้อ 1 กับข้อ 2 ส่วนต่างอยู่ใน section ไหน แล้วอธิบายด้วยแนวคิดข้อ 3 ว่าทำไมตอนปิดธงจึงไม่มีส่วนต่างนั้น

หลักฐานที่เก็บไว้ใน portfolio: ผลของ arm-none-eabi-size ทั้งสองครั้ง log ของตัวรันตัวอย่าง (รายการ ผลของตัวที่เลือก และผลของชื่อที่ไม่มีอยู่) และคำอธิบายข้อ 5 สั้น ๆ

  • ./bento.sh menus ของแม่แบบถาม make ทีละธงแทนการอ่าน Makefile ด้วยตา README ของแม่แบบบอกเหตุผลว่าบางธงถูกกำหนดสองครั้ง ในเงื่อนไขของบอร์ดกับใน else อ่านข้อความแล้วได้คำตอบผิด ลองหาธงแบบนั้นหนึ่งตัวใน proj_cm55/Makefile ด้วย grep -n "ENABLE_PAGE_" proj_cm55/Makefile
  • เอกสาร GNU Make หัวข้อ “The Two Flavors of Variables” และ “Overriding Variables” อธิบายเรื่องในแนวคิดข้อ 2 แบบเต็ม

บทถัดไป: บทเรียน 2.3 Git สำหรับงานเฟิร์มแวร์

  • ถ้าเพื่อนบอกว่า “ผมเปิด ENABLE_PAGE_EXAMPLES แล้วแต่ไม่เห็นอะไรเปลี่ยน” คุณจะถามอะไรเขาสามข้อแรก
  • ธงของ build แบบไหนที่ควรทำให้ build ล้มเมื่อค่าผิด และแบบไหนที่ใช้ค่าเริ่มต้นเงียบ ๆ ได้

คำถามทบทวน

ลองตอบเองก่อน แล้วค่อยเปิดดูเฉลย

  1. ต้องการรันตัวอย่างอ่านปุ่มและ LED ฝั่ง CM33 ตอนบูต คำสั่งใดถูกต้องสำหรับ commit ที่หลักสูตรนี้ใช้ (เป้าหมายข้อ 1)

    1. make build ENABLE_PAGE_EXAMPLES=1 SDK_EXAMPLE_CM33=cm33/io/04_gpio_led_button
    2. make build SDK_EXAMPLE_CM33=cm33/io/04_gpio_led_button (ไม่ต้องเปิดธงอื่น)
    3. make build ENABLE_PAGE_EXAMPLES=1 SDK_EXAMPLE_CM33=04_gpio_led_button.c
    4. make build ENABLE_PAGE_EXAMPLES=0 SDK_EXAMPLE_CM33=cm33/io/04_gpio_led_button
    ดูเฉลย

    คำตอบ: A. make build ENABLE_PAGE_EXAMPLES=1 SDK_EXAMPLE_CM33=cm33/io/04_gpio_led_button

    ต้องเปิดธงหลัก ENABLE_PAGE_EXAMPLES=1 ก่อน ไม่อย่างนั้นโฟลเดอร์ examples ถูก CY_IGNORE และไม่มีตัวรัน ส่วน id ต้องตรงกับรายการใน sdk_examples_cm33_table.c ซึ่งมีรูปแบบ cm33/<กลุ่ม>/<ชื่อ> ตัวรันเทียบด้วย strcmp ชื่อไฟล์จึงใช้ไม่ได้

  2. build ด้วย ENABLE_PAGE_EXAMPLES=1 แต่ไม่ได้ตั้ง SDK_EXAMPLE_CM33 จะเกิดอะไรบน serial console (เป้าหมายข้อ 1)

    1. ตัวรันรันตัวอย่างทุกตัวเรียงกัน
    2. ไม่มีอะไรเลย เพราะไม่ได้เลือกตัวอย่าง
    3. ตัวรันพิมพ์รายการตัวอย่างทั้งหมดพร้อมวิธีรัน แล้วไม่รันอะไร
    4. build ล้มเพราะ SDK_EXAMPLE_CM33 ว่าง
    ดูเฉลย

    คำตอบ: C. ตัวรันพิมพ์รายการตัวอย่างทั้งหมดพร้อมวิธีรัน แล้วไม่รันอะไร

    Makefile เขียนไว้ว่าค่าว่างคือ list them and run nothing เพราะคำถามแรกของนักพัฒนาคือมีอะไรให้เรียกบ้าง และการไม่รันอะไรโดยปริยายกันไม่ให้การเปิดธงเดียวไปเปิดวิทยุหรือแตะ secure element โดยไม่ตั้งใจ

  3. Makefile มีบรรทัด FLAG = 0 แล้วผู้ใช้รัน FLAG=1 make build (ตั้งใน environment) ค่าของ FLAG ระหว่าง build คืออะไร และถ้าเปลี่ยนเป็น FLAG ?= 0 จะเป็นอะไร (เป้าหมายข้อ 2)

    1. เป็น 1 ทั้งสองแบบ
    2. เป็น 0 กับ = และเป็น 1 กับ ?=
    3. เป็น 1 กับ = และเป็น 0 กับ ?=
    4. เป็น 0 ทั้งสองแบบ
    ดูเฉลย

    คำตอบ: B. เป็น 0 กับ = และเป็น 1 กับ ?=

    ค่าจาก environment แพ้การกำหนดค่าในไฟล์ด้วย = หรือ := แต่ ?= ให้ค่าเฉพาะเมื่อตัวแปรยังไม่ถูกนิยาม และค่าจาก environment นับว่านิยามแล้ว ถ้าส่งเป็น make build FLAG=1 (command line) จะเป็น 1 ทั้งสองแบบ

  4. ใน Makefile มี A = x$(B) แล้วบรรทัดถัดไป C := x$(B) และบรรทัดสุดท้าย B = y เมื่อใช้งาน A กับ C มีค่าอะไร (เป้าหมายข้อ 2)

    1. A = xy และ C = x
    2. A = x และ C = xy
    3. ทั้งคู่เป็น xy
    4. ทั้งคู่เป็น x
    ดูเฉลย

    คำตอบ: A. A = xy และ C = x

    = เก็บข้อความไว้และขยายตอนใช้ ซึ่งตอนนั้น B เป็น y แล้ว ส่วน := ขยายทันทีตอนอ่านบรรทัด ขณะนั้น B ยังว่าง

  5. ข้อใดอธิบายได้ถูกว่าทำไมเมื่อ ENABLE_PAGE_EXAMPLES=0 ตัวอย่างของ SDK ไม่เพิ่มขนาดเฟิร์มแวร์ (เลือกได้หลายข้อ) (เป้าหมายข้อ 3)

    1. โฟลเดอร์ examples ถูก CY_IGNORE ทั้งโฟลเดอร์ จึงไม่มีไฟล์ใดถูกคอมไพล์
    2. main.c เรียกตัวรันตัวอย่างเฉพาะใน #if ENABLE_PAGE_EXAMPLES จึงไม่มีการอ้างถึงโค้ดตัวอย่าง
    3. ตัวอย่างถูกคอมไพล์แล้วบีบอัดใน flash จึงไม่กินที่
    4. ใช้ ifeq ครอบ SOURCES += ของโฟลเดอร์ examples ก็ตัดไฟล์ออกได้เหมือนกัน
    ดูเฉลย

    คำตอบ: A. โฟลเดอร์ examples ถูก CY_IGNORE ทั้งโฟลเดอร์ จึงไม่มีไฟล์ใดถูกคอมไพล์ · B. main.c เรียกตัวรันตัวอย่างเฉพาะใน #if ENABLE_PAGE_EXAMPLES จึงไม่มีการอ้างถึงโค้ดตัวอย่าง

    ModusToolbox หาซอร์สจากการเดินโฟลเดอร์ SOURCES += เพิ่มได้แต่ลบไม่ได้ Makefile ของ SDK จึงบอกว่าการครอบ SOURCES += ด้วย ifeq does NOTHING มีแต่ CY_IGNORE ที่ตัดออก และไม่มีการบีบอัดโค้ดอะไรแบบนั้น

อ้างอิงบทเรียนนี้

ถ้านำบทเรียนนี้ไปสอน ทำสไลด์ หรือทำเอกสารต่อ ให้อ้างอิงด้วยข้อความนี้ ถ้าดัดแปลงเนื้อหา ให้เติม (ดัดแปลง)ต่อท้ายชื่อบทเรียน

"Make และตัวแปรของการ build" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0

ข้อความอ้างอิงภาษาอังกฤษ: "Make and build flags" from TESA Open Knowledge by the Thai Embedded Systems Association (TESA), https://github.com/tesaiot/tesa-qualification-program, licensed under CC BY-NC 4.0

ลิงก์บทเรียน: https://tesaiot.github.io/tesa-qualification-program/courses/embedded-c-foundations/m02-build-and-version/l02-make-and-build-flags/

บทเรียนนี้ดัดแปลงจากต้นฉบับด้านล่าง เมื่ออ้างอิงให้คงเครดิตต้นฉบับไว้ด้วย
https://github.com/tesaiot/tesaiot-pse84-devkit-sdk/tree/ef72c1b658178eee8c38b1e47d28b006f80a59b5 · SDK examples and docs are linked at this commit, not copied into this course. Lessons quote short excerpts (at most 25 lines) with a link to the file at this commit and the credit (Apache-2.0, tesaiot-pse84-devkit-sdk).

วิธีอ้างอิง TESA ฉบับเต็ม

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

เนื้อหาเผยแพร่ภายใต้ CC BY-NC 4.0 นำไปใช้ต่อในงานที่ไม่ใช่เพื่อการค้าได้ โปรดอ้างอิงสมาคมสมองกลฝังตัวไทย (TESA) ทุกครั้ง · วิธีอ้างอิง TESA