ARM Mbed OS for Raspberry Pi Pico RP2040: Code Examples (Part 1)#

เนื้อหาในส่วนนี้สาธิตการเขียนโค้ดโดยใช้ Mbed OS และ Arduino IDE สำหรับบอร์ดไมโครคอนโทรลเลอร์ที่ใช้ชิป RP2040 เช่น Raspberry Pi Pico โดยนำเสนอเป็น ตอนที่ 1

แนะนำให้ศึกษาจากบทความเริ่มต้นเกี่ยวกับ Mbed OS และบทความแนะนำไมโครคอนโทรลเลอร์ RP2040 สำหรับเรียนรู้ด้านระบบสมองกลฝังตัว

Keywords: Mbed OS, Raspberry Pi Pico, RP2040, Mbed-enabled Platforms, RTOS Programming


โค้ดนี้สาธิตการใช้คลาส DigitalOut ของ Mbed Drivers เพื่อใช้สำหรับขา GPIO ที่เป็นขาดิจิทัลในทิศทางเอาต์พุต เช่น ใช้กับขา GP25 ของ LED

ในตัวอย่างนี้มีการใช้คลาส DigitalOut ภายใต้ชื่อ mbed ซึ่งเป็น C++ namespace และสร้างอ็อบเจกต์ใหม่ชื่อ led สำหรับใช้งานขา GP25 เป็นเอาต์พุต

#include "mbed.h"
#define LED_PIN (p25) // use on-board LED (GP25 pin)

mbed::DigitalOut led( LED_PIN ); // use onboard LED

void setup() {
   SerialUSB.begin( 115200 );
}

String sbuf; // string buffer object

void loop() {
   led = !led; // toggle and update the LED output
   sbuf = "LED state: ";
   sbuf += led.read(); // read LED status
   SerialUSB.println( sbuf.c_str() ); // send a string to SerialUSB 
   rtos::ThisThread::sleep_for( 
      std::chrono::milliseconds(100) 
   );
}

หรือจะสร้างไฟล์และเขียนโค้ดใน main.cpp แทนการเขียนโค้ดในไฟล์ .ino (ให้เป็นไฟล์ว่างเปล่า) ตามรูปแบบต่อไปนี้ ก็ทำได้เช่นกัน และสังเกตว่า ไม่มีการสร้างฟังก์ชัน setup(){...} และ loop(){...}

#include "mbed.h"
#include "USB/PluggableUSBSerial.h"
#define LED_PIN (p25) // use on-board LED (GP25 pin)

mbed::DigitalOut led( LED_PIN ); // use onboard LED
String sbuf; // String buffer object

int main() {
   PluggableUSBD().begin();
   SerialUSB.begin( 115200 );
   while(1) {
     led = !led; // toggle and update the LED output
     sbuf = "LED state: ";
     sbuf += led.read();
     SerialUSB.println( sbuf.c_str() ); // send a string 
     rtos::ThisThread::sleep_for( 
        std::chrono::milliseconds(100) 
     );
   }
}

 

การใช้คำสั่ง sleep_for(...) ของ rtos::ThisThread ภายใต้ C++ namespace ที่มีชื่อว่า rtos ให้ผลเหมือนคำสั่ง delay(...) ของ Arduino Core API

คำสั่งนี้จะทำให้ Main Thread หรือ 'เธรดหลัก' ที่เกี่ยวข้องกับการทำงานของฟังก์ชัน main(){...} หยุดทำงานชั่วคราวตามระยะที่กำหนดไว้ แล้วจึงทำงานต่อ

อีกคำสั่งหนึ่งที่ให้ผลเหมือนกันคือ thread_sleep_for(...) ซึ่งเป็นคำสั่งที่ใช้กับเธรด โดยจะต้องระบุค่าตัวเลขเป็นจำนวนเต็มบวก (หน่วยเป็นมิลลิวินาที)

ข้อสังเกต: ในตัวอย่างนี้ มีการใช้คำสั่งของ Arduino Core for Mbed เช่น PluggableUSBD() และ SerialUSB (ถูกสร้างมาจากคลาส USBSerial) สำหรับส่งข้อความผ่านทาง USB ไปยังคอมพิวเตอร์

 


ตัวอย่างนี้สาธิตการเขียนโค้ด LED Blink โดยไม่ใช้คำสั่ง delay(...) ของ Arduino API แต่ใช้วิธีอ่านค่าเวลาของระบบในหน่วยเป็นมิลลิวินาที โดยใช้คำสั่ง millis() เพื่อนำมาใช้กำหนดช่วงเวลาในการเปลี่ยนสถานะของ LED ในครั้งถัดไป

#define LED_PIN (p25) // use on-board LED (GP25 pin)

uint32_t start_time_ms, saved_time_ms = 0;

void setup() {
   SerialUSB.begin( 115200 );
   pinMode( LED_PIN, OUTPUT );
   saved_time_ms = start_time_ms = millis();
}

String sbuf;

void loop() {
  uint32_t now = millis();
  if ( now - saved_time_ms >= 100 ) {
    saved_time_ms = now;
    digitalWrite( LED_PIN, !digitalRead( LED_PIN ) );
    sbuf = "LED state: ";
    sbuf += digitalRead( LED_PIN );
    sbuf += ", time: ";
    sbuf += (now - start_time_ms);
    SerialUSB.println( sbuf.c_str() ); 
  }
}

หากลองเปลี่ยนมาใช้คำสั่งตามรูปบบของ Mbed OS API ดูบ้าง เช่น ในโค้ดต่อไปนี้ มีการสร้างฟังก์ชัน get_time_msec(...) มาใช้แทนคำสั่ง millis(...) ของ Arduino

#include "mbed.h"
using namespace std::chrono;
#define LED_PIN (p25) // use on-board LED (GP25 pin)

mbed::DigitalOut led( LED_PIN ); // onboard LED

uint32_t get_time_msec() {
  auto now = rtos::Kernel::Clock::now();
  auto tp  = time_point_cast<microseconds>(now); 
  return (uint32_t) tp.time_since_epoch().count()/1000;
}

uint32_t start_time_ms, saved_time_ms = 0;

void setup() {
  SerialUSB.begin( 115200 );
  saved_time_ms = start_time_ms = get_time_msec();
}

String sbuf;

void loop() {
  uint32_t now = _millis();
  if ( now - saved_time_ms >= 100 ) {
    saved_time_ms = now;
    led = !led; // toggle and update the LED output
    sbuf = "LED state: ";
    sbuf += led.read();
    sbuf += ", time: ";
    sbuf += (now - start_time_ms);
    sbuf += " ms";
    SerialUSB.println( sbuf.c_str() ); 
  }
}

หรือจะใช้คำสั่ง thread_sleep_until(...) ของ Mbed OS เพื่อหยุดการทำงานชั่วคราวของเธรดที่กำลังทำงานในขณะนั้น จนถึงเวลาครั้งถัดไปในอนาคตตามที่กำหนดไว้

#include "mbed.h"
using namespace mbed; // for DigitalOut
using namespace rtos; // for ThisThread
using namespace std::chrono; // for milliseconds()

const PinName LED_PIN = p25;
DigitalOut led( LED_PIN ); // onboard LED

uint64_t next_time_ms, start_time_ms; // timestamps

void setup() {  
  SerialUSB.begin( 115200 );
  while(!SerialUSB){} // wait until the Serial port is open.
  start_time_ms = next_time_ms = Kernel::get_ms_count();
}

String sbuf;

void loop() {
  uint32_t ts;
  led = !led; // toggle the LED state
  sbuf = "LED state: ";
  sbuf += led.read();
  sbuf += ", time: ";
  ts = Kernel::get_ms_count() - start_time_ms;
  sbuf += ts;
  sbuf += " ms";
  SerialUSB.println( sbuf.c_str() ); 
  next_time_ms += 100;
  thread_sleep_until( next_time_ms );
}

 


ตัวอย่างนี้สาธิตการเขียนโค้ดเพื่อสร้าง 'เธรด' (Thread) จากเธรดหลัก และให้ทำหน้าที่สลับสถานะลอจิกของ LED เว้นช่วงเวลาตามที่กำหนดไว้ เช่น 100 msec เมื่อเธรดหลักทำให้เธรดใหม่ (LED Thread) เริ่มทำงาน ก็จะถูกหยุดรอแบบไม่มีกำหนด โดยใช้คำสั่ง osDelay( osWaitForever ) ซึ่งเป็นคำสั่งตามรูปแบบของ CMSIS RTOS

#include "mbed.h"
using namespace mbed; // for mbed::DigitalOut
using namespace rtos; // for rtos::Thread

#define LED_PIN (p25) // use on-board LED (GP25 pin)

DigitalOut  led( LED_PIN ); // on-board led pin
Thread      thread; // the led-blink thread

void toggle( DigitalOut *pin ) { // thread entry function
  auto sleep_ms = std::chrono::milliseconds( 100 );
  SerialUSB.println("LED-blink thread started...");
  while(1) {
    *pin = !*pin; // toggle the pin
    ThisThread::sleep_for( sleep_ms );
  }
}

void setup() {
  SerialUSB.begin( 115200 );
  while(!SerialUSB){} // wait until the USB serial port is open.
  thread.start( callback(toggle,&led) ); // start the thread
  osDelay( osWaitForever ); // the main thread will wait forever
}

void loop() {} // not executed

 

แต่ถ้าลองเปลี่ยนมาสร้างเธรด เช่น threadA และ threadB ที่มีระดับความสำคัญ หรือ ค่า Priority Level เท่ากัน (osPriorityNormal) และให้ทำหน้าที่กำหนดสถานะของ LED ที่แตกต่างกัน คือ ลอจิก 0 และ 1 ตามลำดับ แต่สลับช่วงเวลากัน ในกรณีนี้เราสามารถใช้ Mutex (Mutual Exclusion) ของ Mbed OS เพื่อป้องกันและให้มีเพียงเธรดเดียวในแต่ละช่วงเวลาที่เข้าถึงและกำหนดสถานะของ LED ได้

#include "mbed.h"
using namespace mbed;
using namespace rtos;

#define LED_PIN (p25) // use on-board LED (GP25 pin)

DigitalOut  led( LED_PIN );
Thread threadA(osPriorityNormal), threadB(osPriorityNormal);
Mutex  mutex;

typedef struct _thread_arg {
  DigitalOut *pin;
  uint32_t   id;
  uint32_t   sleep_ms;
} thread_arg_t;

// used to pass thread's arguments
 thread_arg_t thread_args[] = {
  { .pin = &led, .id = 0, .sleep_ms = 100 },
  { .pin = &led, .id = 1, .sleep_ms = 100 },
};

void led_update( thread_arg_t *args ) {
  String s;
  auto sleep_ms = std::chrono::milliseconds( args->sleep_ms );
  while(1) {
    mutex.lock();
    args->pin->write( args->id );
    s = "thread id ";
    s += args->id;
    SerialUSB.println( s.c_str() );
    ThisThread::sleep_for( sleep_ms );
    mutex.unlock();
  }
}

void setup() {
  SerialUSB.begin( 115200 );
  // start threads A and B
  threadA.start( callback(led_update, &thread_args[0]) );
  threadB.start( callback(led_update, &thread_args[1]) );
  osDelay( osWaitForever ); // the main thread will wait forever
}

void loop() {} // not executed

 


การทำให้ LED กระพริบด้วยอัตราคงที่นั้น เป็นการทำงานแบบที่มีคาบ (Periodic Task) ในกรณีนี้เราสามารถใช้คลาส Ticker ของ Mbed Drivers ตามตัวอย่างต่อไปนี้

ขา GP25 จะถูกใช้เป็นเอาต์พุตสำหรับวงจร LED และสร้างอ็อบเจกต์จากคลาส Ticker เพื่อคอยทำคำสั่งของฟังก์ชันที่เป็น Callback และเกิดขึ้นซ้ำตามระยะเวลาที่กำหนดไว้ เช่น ให้เรียกฟังก์ชัน callback(){...} ทุก ๆ 100 มิลลิวินาที

#include "mbed.h"
using namespace mbed;
using namespace rtos;

#define LED_PIN (p25) // use on-board LED (GP25 pin)
DigitalOut  led( LED_PIN );
Ticker      ticker;

const auto interval = std::chrono::microseconds(100*1000);

void toggle( DigitalOut *pin ) { // ISR callback function
  *pin = !*pin;  // toggle LED pin
}

void setup() {
  SerialUSB.begin( 115200 );
  while(!SerialUSB){} // wait for serial to open
  SerialUSB.println( "Ticker demo..." );
  ticker.attach( callback(toggle, &led), interval );
  osDelay( osWaitForever ); // block the main thread
}

void loop() {} // not executed

 

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

#include "mbed.h"
using namespace mbed;

#define LED_PIN (p25) // use on-board LED (GP25 pin)
DigitalOut  led( LED_PIN );
Timeout     timeout;

const auto interval = std::chrono::microseconds(100*1000);

void toggle( DigitalOut *pin ) {
  *pin = !*pin;  // toggle LED pin
  timeout.attach( callback(toggle,pin), interval );
}

void setup() {
  SerialUSB.begin( 115200 );
  while (!SerialUSB){} // wait for serial to open
  SerialUSB.println( "Timeout demo..." );
  timeout.attach( callback(toggle, &led), interval );
  osDelay( osWaitForever ); // block the main thread
}

void loop() {} // not executed

 


LED Fading โดยการสร้างสัญญาณ PWM#

ตัวอย่างนี้สาธิตการเขียนโค้ดโดยใช้ PwmOut ของ Mbed Drivers เพื่อสร้างสัญญาณแบบ PWM (Pulse Width Modulation) ตั้งค่าความถี่ไว้เท่ากับ 500 Hz หรือมีคาบเท่ากับ 2000 usec (2 msec)

LED ที่ขา GP25 จะสว่างขึ้นและดับลง วนซ้ำสลับกันไปเรื่อย ๆ โดยการปรับค่า Duty Cycle ของ PWM ในช่วง 0 ถึง 100% (อัตราส่วนของความกว้างของพัลส์ High เทียบกับความกว้างของหนึ่งคาบ) ในทิศทางเพิ่มและลดลง

#include "mbed.h"
using namespace mbed;
#define LED_PIN (p25) // use on-board LED (GP25 pin)
#define STEPS   (10)  // number of LED brightness steps

const uint32_t PWM_PERIOD_US = 2000; // frequency 500Hz
PwmOut led_pwm( LED_PIN ); // use onboard LED

void setup() {
   SerialUSB.begin( 115200 );
   led_pwm.period_us( PWM_PERIOD_US ); 
   led_pwm.pulsewidth_us( 0 );
}

char sbuf[32];

void loop() {
   uint32_t t, pw;
   for (uint32_t i=0; i < 2*STEPS; i++) {
      t = (i*PWM_PERIOD_US/STEPS);
      pw = (t <= PWM_PERIOD_US) ? t : 2*PWM_PERIOD_US-t;   
      led_pwm.pulsewidth_us( pw );
      sprintf( sbuf, "step %2lu) %lu", i, pw );
      SerialUSB.println( sbuf );
      delay(100);
   } 
}

 

หรือจะเปลี่ยนไปใช้คำสั่ง analogWrite() ของ Arduino API สำหรับสร้างสัญญาณ PWM (ความถี่ 500 Hz เป็นค่า default) ก็เป็นไปตามตัวอย่างโค้ดต่อไปนี้

#define LED_PIN   (p25) // use on-board LED (GP25 pin)
#define STEPS     (10)  // number of LED brightness steps
#define MAX_VALUE (1023)

void setup() {
   SerialUSB.begin( 115200 );
   pinMode( LED_PIN, OUTPUT );
   analogWriteResolution( 10 ); // use 10-bit value
   analogWrite( LED_PIN, 0 ); 
}

char sbuf[32];

void loop() {
  uint32_t t, pw;
   for ( uint32_t i=0; i < 2*STEPS; i++ ) {
      t = (i*MAX_VALUE/STEPS);
      pw = (t <= MAX_VALUE) ? t : 2*MAX_VALUE-t;
      analogWrite( LED_PIN, pw );
      sprintf( sbuf, "step %2lu) %lu", i, pw );
      SerialUSB.println( sbuf );
      delay( 100 );
   } 
}

 


การจำลองการทำงานด้วย WokWi Simulator#

WokWi รองรับการเขียนโปรแกรมและจำลองการทำงานเสมือนจริงสำหรับบอร์ด Raspberry Pico (RP2040) ผู้ใช้สามารถเลือกรูปแบบการเขียนโค้ดได้หลายแบบ ในกรณีที่ต้องการลองใช้คำสั่งของ Mbed OS ให้สร้างโปรเจกต์ใหม่ ซึ่งปรกติแล้วจะเขียนโค้ดด้วย Arduino Sketch (.ino)

แต่แนะนำให้สร้างไฟล์ main.cpp ในโปรเจกต์และเขียนโค้ดในไฟล์ดังกล่าว แทนการเขียนโค้ดในไฟล์ .ino ดังนั้นจึงไม่มีการสร้างฟังก์ชัน setup() และ loop() แต่ให้สร้างฟังก์ชัน main() ตามโค้ดในรูปตัวอย่างต่อไปนี้

รูป: ตัวอย่างการจำลองการทำงานด้วย WokWi Simulator

 


This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Created: 2021-04-16 | Last Updated: 2023-03-04