การพัฒนาเฟิร์มแวร์สำหรับ RP2040 ด้วยภาษา C/C++ (ตอน 1 blinking thread)

Supachai Vorapojpisut
4 min readApr 30, 2022

--

หน่วยประมวลผล RP2040 ที่ใช้ในบอร์ด Raspberry Pi Pico ถูกพัฒนาขึ้นโดย Raspberry Pi Foundation เพื่อเจาะตลาดไมโครคอนโทรลเลอร์ ซึ่งแตกต่างจากแนวทางเดิมที่เน้นออกแบบ SoC (System-on-Chip) สำหรับบอร์ด Raspberry Pi รุ่นต่างๆ หากพูดถึงสถาปัตยกรรมของตัว RP2040 เองก็ถือว่าน่าสนใจในหลายประเด็น เช่น การเป็น dual core ของ ARM Cortex-M0 การมีฮาร์ดแวร์พิเศษ PIO state machine ที่สามารถโปรแกรมให้จัดการ flow ของข้อมูลในระดับฮาร์ดแวร์ รวมทั้งการปล่อย toolchain ออกมาทั้งแบบ C/C++ SDK และ MicroPython

ผมให้ความสนใจตัว RP2040 จากข่าวการเปิดตัวช่องทางซื้อชิพแบบ reel ในราคาไม่ถึง $1/ตัว ซึ่งสวนทางชิพรุ่นอื่นๆที่ราคาขึ้นหลายเท่าตัวจากสถานการณ์หน่วยประมวลผลขาดตลาด แต่ข่าวของผลิตภัณฑ์ที่ใช้ชิพ RP2040 หรือแม้แต่การใช้งานบอร์ด Raspberry Pi Pico ในไทยกลับไม่ค่อยมากเท่าไร กลับเป็น Cytron ที่เป็น hardware startup ของมาเลเซียออกบอร์ดมาแล้ว 2 รุ่นคือ Maker Pi Pico และ Maker Pi RP2040 ในราคาที่แพงกว่าบอร์ด Arduino จีนไม่มากนัก และต้องยอมรับว่าออกแบบฮาร์ดแวร์บนบอร์ดได้เหมาะสำหรับใช้เรียนรู้ เช่น มี LED มาหลายดวง มี buzzer และมีหัวต่ออุปกรณ์มาเยอะจึงแทบไม่ต้องไปพึ่งพาเบรดบอร์ดเลย ผมเองก็ซื้อบอร์ดมาใส่โหลไว้ตอนลดราคา เลยถือโอกาสเอาบอร์ด Maker Pi RP2040 มาใช้ประกอบการเขียนบทความนี้

บอร์ด Maker Pi RP2040

เริ่มแบบง่ายด้วย CircuitPython

การพัฒนาซอฟต์แวร์ให้ RP2040 ง่ายที่สุดคงเป็น MicroPython ซึ่งเลือกได้ทั้งสาย original MicroPython และ branch ของ CircuitPython ที่สนับสนุนโดย Adafruit การเตรียมการแบ่งออกเป็น 3 ขั้น ได้แก่

  1. กดปุ่ม boot แล้วเสียบสาย/เปิดสวิทช์ เพื่อให้ bootloader สร้าง USB mass storage เชื่อมเข้ากับคอมพิวเตอร์
  2. ดาวน์โหลดเฟิร์มแวร์ (นามสกุล .uf2) แล้วลากไฟล์ลงในไดรฟ์จำลอง (RPI-RP2) เพื่อโปรแกรมลงบอร์ด
  3. ดาวน์โหลดโปรแกรม Thonny มาติดตั้งเป็น IDE

ผมเลือกใช้ CircuitPython ที่เมื่อติดตั้งเฟิร์มแวร์แล้วจะทำงานทั้งเป็นไดร์ฟจำลองชื่อ CIRCUITPY และเป็นพอร์ตอนุกรม การใช้งาน Thonny จะใช้กลไก REPL (Read–Eval–Print Loop) ผ่านทางพอร์ตอนุกรม โดยเลือกพอร์ตจากเมนู Run > Select intepreter จากนั้นจะสามารถทดลองโค้ดผ่านทาง Shell ในขณะที่การโปรแกรมบอร์ดให้ทำงานอัตโนมัติจะอาศัยไฟล์ code.py ซึ่งเป็นโค้ดที่จะถูกเรียกใช้ทันทีเมื่อ boot ตัวบอร์ด

โค้ด blink ผ่าน Shell บนโปรแกรม Thonny

การพัฒนาโค้ด Python ให้กับหน่วยประมวลผล RP2040 จัดเป็นแนวทางที่เหมาะสมสำหรับการเรียนรู้ แต่ก็เปิดช่องทางที่ไม่ ok นักสำหรับการนำมาใช้พัฒนาเป็นผลิตภัณฑ์ เช่น เปิดช่องทาง REPL ที่เข้ามา access ตัวหน่วยประมวลผลได้ การเขียนโค้ดด้วยภาษา C/C++ จึงเป็นช่องทางที่น่าจะเหมาะสมกว่าสำหรับการพัฒนาเฟิร์มแวร์สำหรับผลิตภัณฑ์

เนื่องจาก CircuitPython เป็นแค่เฟิร์มแวร์ที่ติดตั้งลงในหน่วยความจำ การสลับไปใช้เครื่องมือพัฒนาอื่นๆ เช่น C/C++ SDK หรือ Arduino ก็ทำได้ง่ายๆด้วยการกดปุ่ม boot ก่อนเสียบสาย/เปิดสวิทช์ เพื่อกระตุ้นให้ bootloader ขึ้นมารอการติดตั้งเฟิร์มแวร์ในรูปแบบไฟล์ .uf2

การใช้ Arduino

การเขียนโค้ด C/C++ ผ่านทาง Arduino framework เป็นอีกตัวเลือกที่ง่ายสำหรับหน่วยประมวลผล RP2040 การใช้ Arduino framework สามารถทำได้ทั้งผ่าน Arduino IDE โดยเลือก Arduino Mbed OS Nano Boards หรือใช้ PlatformIO จากในโปรแกรม Visual Studio Code ผมเลือกใช้ VS Code + PlatformIO ซึ่งแค่เลือกบอร์ดเป็น Raspberry Pi Pico และ Arduino framework ก็จะดาวน์โหลดเครื่องมือพัฒนามาติดตั้งให้เอง จากนั้นกำหนดเงื่อนไขของการโปรแกรมเพิ่มเติมผ่านทางเมนู Project & Configuration หรือแก้ไขไฟล์ platformio.ini การโปรแกรมบอร์ดให้กดปุ่ม boot ก่อนเสียบสาย/เปิดสวิทช์เพื่อเข้าสู่ bootloader

upload_port = COM?
upload_protocol = picotool
การเขียนโค้ด C/C++ บน VS Code ผ่านทาง Platform.io

เฟรมเวิร์ค Arduino ถูกพัฒนาต่อยอดจาก mbed OS ทำให้สามารถเรียกใช้ฟังก์ชัน RTOS และ API อื่นๆ เช่น protocol stack ได้ด้วย สิ่งที่เปลี่ยนจากโค้ดสไตล์ Arduino คือ การเรียกใช้ mbed.h และสลับ namespace เป็น mbed จากนั้นจึงเรียกใช้ API ของ mbed OS ที่เป็น C++ ตามโค้ดตัวอย่าง

#include <mbed.h>
using namespace mbed;
DigitalOut led(p17);int main() {
for (;;) {
led = 1;
thread_sleep_for(500);
led = 0;
thread_sleep_for(700);
}
}

เมื่อเทียบกับโค้ด blink ของ Arduino โค้ดตัวอย่างเปลี่ยนจาก C API ในการเข้าถึง GPIO ได้แก่ pinMode() digitalRead() เป็นการใช้ C++ API ที่ประกาศ object ของคลาส DigitalOut ด้วยการ initialize ด้วยขาที่จะใช้ จากนั้นจึงเข้าถึงสถานะของขาด้วยการกำหนดค่า/อ่านค่าของ object ใครที่สนใจ API ของ mbed สามารถไปศึกษาได้จากเว็บของ mbed OS ซึ่งมีโครงสร้างที่ไม่ได้จบแค่ RTOS ยังรวมไปถึง middleware อีกหลายตัวโดยเฉพาะกลุ่ม protocol stack

สถาปัตยกรรมของ mbed OS

การเขียนโค้ดแบบ thread

เนื้อหาก่อนนี้เป็นตัวอย่าง blink แบบ single thread ซึ่งอาศัยการหน่วงเวลาในการเปลี่ยนสถานะของขา GPIO ที่ต่อกับ LED ทำให้เกิดการติด/ดับของ LED ด้วยอัตราที่ควบคุมได้จากช่วงเวลาที่หน่วง อย่างไรก็ตาม การเป็น single thread จะทำให้การเขียนโค้ดสำหรับงานที่ซับซ้อนขึ้นแม้จะไม่มาก เช่น การทำให้ LED มีอัตรา blink ที่ไม่เท่ากัน กลับเป็นงานที่ยากขึ้นมากเพราะต้องคิด logic ในการสลับงานเอง

ในเมื่อเครื่องมือพัฒนาของ Arduino ถูกพัฒนาต่อยอดจาก API ของ mbed OS การเขียนโค้ดที่ยังเรียกใช้เฉพาะ Arduino API ที่เป็น single thread ย่อมไม่ค่อยได้ประโยชน์นัก ทั้งนี้ API ของ mbed OS ที่น่าสนใจมีอยู่หลายส่วน ซึ่งจะเรียกใช้ได้ผ่านการกำหนด namespace ที่เกี่ยวข้อง ตัวอย่างเช่น กลไก task/thread ของ RTOS สำหรับเลือกสลับงานเข้าประมวลผลจะช่วยให้เขียนโค้ดได้ง่ายขึ้นมาก โค้ดตัวอย่างด้านล่างนี้สาธิตการสร้าง blink แบบ 2 thread ซึ่งมีอัตราการติด/ดับที่แตกต่างกัน (คาบ 1 และ 1.4 วินาที)

#include <mbed.h>
using namespace mbed;
using namespace rtos;
DigitalOut led0(p0);
DigitalOut led1(p1);
Thread thread;
// thread code
void led1_blink() {
while (true) {
led1 = !led1;
thread_sleep_for(500);
}
}
// main thread
int main() {
thread.start(callback(led1_blink));
for (;;) {
led0 = !led0;
thread_sleep_for(700);
}
}

โค้ดตัวอย่างข้างต้นมี 2 thread ทำงานร่วมกันโดยส่วนของ main() จะถือเป็น 1 thread และมีอีก 1 thread ที่ไปผูกกับฟังก์ชัน led1_blink() ในฐานะ callback การเขียนโค้ดแบบ multithreading จะเริ่มด้วยการกำหนด namespace rtos แล้วเรียกใช้ API ในการสร้าง (ประกาศ object ของคลาส Thread) และกระตุ้น thread (ผูกฟังก์ชัน callback ด้วยเมธอด Thread::start()) ให้ทำงานเสมือนพร้อมๆกันได้ เมื่อลงเฟิร์มแวร์เข้าบอร์ดจะเห็นการกระพริบของ LED ที่ต่อกับขา P0 และ P1 ด้วยคาบเวลาที่ไม่เท่ากัน

โค้ดตัวอย่างแบบ thread blink นี้แม้จะเรียบง่ายแต่แสดงให้เห็นถึงข้อได้เปรียบเมื่อเทียบกับการเขียนโค้ดแบบ single thread ซึ่งจะต้องปรับโค้ดให้ทำงานทุก 0.1 วินาทีเพื่อมาตรวจสอบเงื่อนไขในการติด/ดับของ LED 2 ดวง เมื่อเทียบกับโค้ดแบบ thread ที่เมื่อดูในลำดับการประมวลผลแล้วเป็นเพียงโค้ด blink ธรรมดาเท่านั้น ทั้งนี้ การเขียนโค้ดที่ต้องเชื่อมโยงกับฮาร์ดแวร์ที่มีความแตกต่างกันทั้งในแง่ลำดับและช่วงเวลาในการเข้าถึงข้อมูล เช่น การคุม duty cycle ของสัญญาณ PWM ที่ไปขับเคลื่อนมอเตอร์ควบคู่กับการรับคำสั่ง/รายงานสถานะผ่านโพรโทคอล MODBUS จะได้ประโยชน์อย่างมากด้วยการสร้าง thread แยกแล้วสื่อสาร/เข้าจังหวะระหว่างกันด้วย API ในกลุ่ม synchronization และ/หรือ message transfer

終わりに (ในตอนท้าย)

ผมเขียนบทความนี้ด้วยหลายสาเหตุเริ่มจากอยากควักบอร์ดในโหลดองออกมากระพริบ LED สักหน่อย เพื่อให้มั่นใจว่ายังจำขั้นตอนการ build ได้ ส่วนตัวเลือกที่เป็น RP2040 เพราะเพิ่งสั่งบอร์ด LILYGO T-PicoC3 (RP2040+ESP32C3) มาเพิ่มในโหล ซึ่งน่าจะมีอะไรมาเล่าได้อีกจากการเป็น dual processor สาเหตุถัดมาคือ อยากกระตุ้นให้หันมามอง RP2040 หน่อยในระหว่างหาชิพหน่วยประมวลผลที่น่าจะยังขาดตลาดไปอีกสักพัก และมีทิ้งท้ายอีก 1 ประเด็นที่อยากให้เอะใจเรื่อง mbed OS ที่น่าจะได้ประโยชน์จากงานที่อยากได้ RTOS มาใช้ โดยเฉพาะพวกมือใหม่หัดเขียนโค้ดที่ copy โค้ดมาแปะรวมกันแล้วไม่ทำงาน (ไม่ต้องแปลกใจหากจะเจอ delay() หลายรอบใน loop()) เนื้อหารอบหน้าว่าจะปรับไปเป็น blink แบบ dual core ก่อนจะเป็น blink แบบ dual processor … ไม่ต้องทำมาก แค่ไฟกระพริบก็พอ

รูปบอร์ดของ LILYGO ที่สั่งเมื่อต้นเดือนนี้
แต่น่าจะได้ก่อนบอร์ดนี้ที่สมทบทุนตั้งแต่ปีก่อน

--

--

Supachai Vorapojpisut
Supachai Vorapojpisut

Written by Supachai Vorapojpisut

Assistant Professor at Thammasat University

No responses yet