[TH] Arduino: STM32F030F4P6

บทความนี้เป็นการใช้งานบอร์ดไมโครคอนโทรลเลอร์ 32 บิตภายใต้สถาปัตยกรรม RISC แบบ Cortex-M0 ที่มีราคาประหยัดแต่การทำงานนับว่าดีกว่าบอร์ดที่เป็นแบบ 8 บิตพอสมควร ประกอบกับมีวิธีการใช้งานให้ศึกษาอยู่บนอินเทอร์เน็ตมากมาย แต่อย่างไรก็ดีทางทีมพวกเราก็สร้างเป็นทางเลือกหนึ่งสำหรับผู้ที่สนใจได้อ่านกัน โดยบทความเริ่มจากเนื้อหาเกี่ยวกับคุณสมบัติของไมโครคอนโทรลเลอร์ การติดตั้งบอร์ดให้ Arduino IDE รู้จัก และตัวอย่างโปรแกรมการสั่งท็อกเกิ้ลหลอดแอลอีดี การรับส่งข้อมูลผ่านพอร์ตสื่อสาร USART และการทดสอบการหา Prime number เพื่อดูความเร็วในการประมวลผลในแบบการวนรอบซ้ำ ๆ กัน

ภาพที่ 1 บอร์ด STM32F030F4P6

STM32F030F4P6

ไมโครคอนโทรลเลอร์ STM32F030F4P6 มีคุณสมบัติดังนี้

  1. ใช้แกน (Core) แบบ 32 บิต รองรับความถี่สัญญาณนาฬิกาสูงสุด 48MHz (8MHz x 6)
  2. หน่วยความจำ Flash ROM 16KB
  3. หน่วยความจำ SRAM 4KB
  4. GPIO จำนวน 15 ขา
  5. ADC ความละเอียด 12 บิต
  6. ทำงานที่แรงดัน 2.4-3.6 VDC
  7. รองรับการสื่อสาร USART, I2C และ SPI
  8. มี WDT
  9. บนบอร์ดมี External XTAL ความถี่ 8MHz
  10. บอร์ดมีตัวแปลงแรงดัน 5VDC ให้เป็น 3V3
  11. บอร์ดมีการแยกขาสำหรับ RS232 (ทำ ISP) และ SWD (สำหรับ ST-Link)
  12. บอร์ดมีสวิตช์ NRST สำหรับรีเซ็ตการทำงาน
  13. บอร์ดมี Jumper สำหรับเลือกแรงดันของขา Boot0 ให้เป็น 3V3 หรือ GND สำหรับโหลดอัพโหลดกับรันโปรแกรม

การติดตั้ง

การติดตั้งบอร์ดให้ Arduino IDE รู้จักต้องใช้ JSON ของ Arduino Core STM32 ดังนี้ใน Preferences ของ Arduino IDE
https://github.com/stm32duino/BoardManagerFiles/raw/master/package_stmicroelectronics_index.json

หลังจากนั้นเข้า Board Manager เพื่อติดตั้ง STM32 MCU based boards จะได้รายการดังภาพที่ 2 ให้เลือกบอร์ดเป็น STM32 boards groups (board to be selected from Tools submenu “Board part number”) / Generic STM32F0 series ดังภาพที่ 2 และตั้งค่าต่าง ๆ ดังภาพที่ 3

ภาพที่ 2 รายการเลือก Generic STM32F0 series
ภาพที่ 3 การตั้งค่าของบอร์ด

อุปกรณ์ที่ต้องใช้

การอัพโหลดโปรแกรมเข้าบอร์ดต้องใช้โปรแกรม STM32CubeProgrammer ที่ต้องติดตั้งเอาไว้ก่อนเพื่อสั่งงานอุปกรณ์ ST-Link สำหรับเชื่อมผ่านพอร์ต SWD ดังภาพที่ 4 และ 5

ภาพที่ 4 ST-Link/V2
ภาพที่ 5 USB-Serial

ที่บอร์ดของ STM32F030F4P6 มีขั้ว ISP และ SWD ดังภาพที่ 6 เพื่อใช้ต่อกับ USB-Serial และ ST-Link/V2 ดังภาพที่ 7 และ 8

ภาพที่ 6 ขั้วต่อ ISP และ SWD
ภาพที่ 7 ต่อ USB2Serial เข้ากับ ISP
ภาพที่ 8 ต่อ ST-Link/V2 เข้ากับ SWD

โหมดอัพโหลด

การอัพโหลดต้องเปลี่ยนโหมดด้วยการย้าย jumper ขั้ว BOOT0 มาไว้ ณ ตำแหน่ง 3V3 แล้วกดปุ่ม NRST ดังภาพที่ 9 และ 10

ภาพที่ 9 ย้าย Jumper ของ boot0 มาที่ 3V3
ภาพที่ 10 กด Switch เพื่อ Reset เข้าโหมด Upload

กรณีที่อัพโหลดไม่สำเร็จเนื่องจากไม่พบ STM32CubeProgrammer จะรายงานดังภาพที่ 11

ภาพที่ 11 การรายงานความผิดพลาดเมื่อไม่พบ STM32CubeProgrammer

ในบางกรณีที่ตั้งค่าทุกอย่างถูกต้องแต่ลืมเปลี่ยนโหมดการทำงานของ boot mode เป็นการอัพโหลดโปรแกรมจะรายงานดังภาพที่ 12

ภาพที่ 12 ตัวอย่างการรายงานความผิดพลาดเมื่อไม่ได้เปลี่ยน boot mode

เมื่ออัพโหลดสำเร็จด้วยวิธีการอัพโหลดผ่าน RS232 จะรายงานผลดังภาพที่ 13 และกรณีที่ใช้ st-link จะรายงานดังภาพที่ 14

ภาพที่ 13 ผลลัพธ์เมื่ออัพโหลดด้วย USB2Serial
ภาพที่ 14 ผลลัพธ์เมื่ออัพโหลดด้วย st-link/v2

โหมดรันโปรแกรม

การรันทำโดยการย้ายตำแหน่งของ Jumper ขา Boot0 ไปไว้ที่ GND แล้วกดปุ่ม NRST ดังภาพที่ 15 และ 16

ภาพที่ 15 ย้าย Jumper ของ boot0 มาที่ GND
ภาพที่ 16 กดสวิตช์เพื่อเข้าโหมดรันโปรแกรม

ตัวอย่างโปรแกรม

การ Blink

ตัวอย่างการสลับการส่งค่าของขา PA0, PA1, PA2, PA3 และ PA4 ระหว่าง 0 กับ 1 ทุก 500 มิลลิวินาทีที่ต่อเชื่อมกับภาค LED ของบอร์ดทดลอง ET-TEST I/O (ใช้มานานมาก) หรือ ET-TEST 10P/OUT เป็นดังนี้ และได้ผลลัพธ์ดังภาพที่ 17 และ 18

// Blink-4pins
uint8_t pins[] = {PA0, PA1, PA2, PA3, PA4};

void setup() {
  for (int idx = 0; idx < 5; idx++) {
    pinMode( pins[idx], OUTPUT );
  }

}

void loop() {
  for (int idx = 0; idx < 5; idx++) {
    digitalWrite( pins[idx], !digitalRead( pins[idx] ));
  }
  delay(500);
}
ภาพที่ 17 ผลลัพธ์ของตัวอย่างโปรแกรม Blink 1/2
ภาพที่ 18 ผลลัพธ์ของตัวอย่างโปรแกรม Blink 2/2

การใช้งาน RS232

จากโค้ดของ Brian Lavery ที่เขียนเกี่ยวกับการใช้งานบอร์ด STM32F030F4P6 กับ Arduino ทำให้สามารถใช้งานคลาส Serial ได้ง่ายโดยไม่ต้องเขียนโค้ดแก้ไขการทำงานของไลบรารี ซึ่ง Brain Lavery ได้เพิ่มโค้ดโปรแกรม 3 ไฟล์เข้ามาเพื่อควบคุมการทำงานของการสื่อสารอนุกรมของบอร์ดดังนี้

  1. โค้ดของไฟล์ charQueue.h เป็นดังนี้
// Very basic class (char) array-as-queue    BL Nov 2018
#define BUF_SIZE 40
class Queue
{
  private:
    bool _isFull() {
      return (_size == BUF_SIZE);
    };
    bool _isEmpty() {
      return (_size == 0);
    };
    char _buffer[BUF_SIZE];
    int _head;
    int _tail;
    int _size;

  public:
    Queue(void) {
      _head = 0;
      _tail = BUF_SIZE - 1;
      _size = 0;
    };
    bool enqueue(char ch) {
      if (_isFull())
        return false;
      _tail = (_tail + 1) % BUF_SIZE;
      _buffer[_tail] = ch;
      _size ++;
      return true;
    };
    int dequeue() {
      if (_isEmpty())
        return -1;
      char ch = _buffer[_head];
      _head = (_head + 1) % BUF_SIZE;
      _size --;
      return int(ch);
    };
};

ไฟล์ของ miniSerial.h เป็นดังนี้

// Serial BitBash for STM32F030F4P6

#pragma once

#include <Arduino.h>

#define BITDELAY  52
//  104 9600  baud  52 19200
//  not good above 19200

#define HEX 16
#define BIN 2
#define OCT 8

class MiniSerial
{
  public:
    MiniSerial(void);
    void begin(int baudrate = 19200, int txpin = PA9, int rxpin = PA10);
    int  read(void);
    void run(void);
    void write(unsigned char data);
    void print(double float_num, int prec = 2);	// uses lots of flash space! Avoid?
    void print(char* str);
    void print(int, int = DEC);
    void print(long, int = DEC);
    void println(double float_num, int prec = 2);
    void println(char* str = "");  // handles println() also
    void println(int, int = DEC);
    void println(long, int = DEC);

  private:
    int  _getChar(void);
    char _rx; // buffered chr
    char _rx1 = 0;
    int pinTx = -1; // -1 = not begin'd
    int pinRx ;
    unsigned long bitDelay = 52;  // //  104 9600  baud  52 19200

    enum { RXIDLE, READING, COMPLETE } ;
    enum { RXBUSY = -4, ERRNOTBEGIN = -3, ERRFRAME = -2,  RXNONE = -1 };
    int rxState = RXIDLE;
    unsigned long rx_chr = 0;
    unsigned long rx_reftime = 0;
    int rx_k;

};

extern MiniSerial mSerial;
#define Serial mSerial

ไฟล์ของ miniSerial.cpp เป็นดังนี้


// A bit-banged low-footprint serial transmit/receive
// primarily for low-memory STM32F030F4P6    BL Nov 2018
// By default uses PA9 as TX    PA10 as RX   -- regular 4-pin RX/TX end connector.
// 8 bit no parity.  19200 seems ideal (default)
// tx will block during character transfer (abt 0.6 mSec each chr at 19200)
// Your loop() must have free-running Serial.run(). No delay()s. Serial reception will suffer otherwise
// reception is buffered
// Transmit is not buffered, but does NOT stop reception processing.
// Functions in this "Serial" are an approximation to regular Serial calls.
// V 0.5.0

#include <miniSerial.h>
#include <charQueue.h>

Queue rxBuf;
//Queue txBuf;  // Buffered tx not currently implemented

MiniSerial::MiniSerial(void)
{
}

void MiniSerial::begin(int baud, int tx, int rx)
{
  pinRx = rx;
  pinTx = tx;
  bitDelay = (unsigned long) (1000000 / baud);
  pinMode(pinTx, OUTPUT);
  digitalWrite(pinTx, HIGH);
  pinMode(pinRx, INPUT);
}

int MiniSerial::read(void) // -1 =nothing/empty
{
  // fetch from buffer
  return rxBuf.dequeue();
}

void MiniSerial::run(void)  // MUST be called VERY frequently from your loop()
{
  int ch = _getChar();
  if (ch >= 0)
    rxBuf.enqueue((char) ch);
}

void MiniSerial::print(char* str)
{
  for (int i = 0; i < strlen(str); i++)
    write(str[i]);
}

void MiniSerial::println(char* str)
{
  print(str);
  print("\n");
}

void MiniSerial::print(int j, int base)
{
  char buffer[30];
  itoa(j, buffer, base);
  print(buffer);
}

void MiniSerial::print(long j, int base)  // but for stm32, both int and long are 32bit!
{
  print((int)j, base);
}

void MiniSerial::println(int j, int base)
{
  print(j, base);
  print("\n");
}

void MiniSerial::println(long j, int base)
{
  print((int)j, base); // long 32bit = int 32bit
  print("\n");
}

void MiniSerial::print(double float_num, int prec) {

  // precision - use 6 maximum
  int d = float_num; // get the integer part
  float f = float_num - d; // get the fractional part
  if (d == 0 && f < 0.0) {
    write('-');
    write('0');
    f *= -1;
  }
  else if (d < 0 && f < 0.0) {
    print(d);
    f *= -1;
  }
  else {
    print(d);
  }
  // only when fractional part > 0, we show decimal point
  if (f > 0.0) {
    write('.');
    int f_shift = 1;
    for (byte j = 0; j < prec; j++) {
      f_shift *= 10;
    }
    print((int)(f * f_shift));
  }
}

void MiniSerial::println(double float_num, int prec)
{
  print(float_num, prec);
  print("\n");
}


/////////////////////////         HARDWARE IO:

int MiniSerial::_getChar(void) // bit-bang rx
// >=0 good rx char
{
  if (pinTx < 0) return ERRNOTBEGIN;

  switch (rxState) {
    case RXIDLE :
      if (digitalRead(pinRx))  // still idle
        return RXNONE;
      // ok, we start rx:
      rx_reftime = micros() - bitDelay / 2;
      rx_chr = 0;
      rxState = READING;
      rx_k = 0;
      return RXBUSY;

    case  READING :  // 10 bit-length passes
      if (micros() - rx_reftime < bitDelay)
        return RXBUSY;
      rx_chr |= (digitalRead(pinRx) << rx_k++);
      rx_reftime += bitDelay;
      if (rx_k > 9)
        rxState = COMPLETE;
      return RXBUSY;

    case COMPLETE :
      rxState = RXIDLE;
      if ((rx_chr & 0b01000000001) != 0b01000000000)  // start & stop bits correct?
        return ERRFRAME;
      return (int) (rx_chr & 0x1FE) >> 1;  // a good chr received

    default:
      break;
  }
}


void MiniSerial::write(unsigned char data)    // TX one byte
{
  if (pinTx < 0) return;

  int chr = (data << 1) | 0b011000000000 ;
  unsigned long starttime = micros();

  for (int i = 11; i > 0; i--)      // 1 start (0), 8 data bits, 2 stop (11)
  {
    digitalWrite(pinTx, chr & 1);
    chr = (chr >> 1);
    while (micros() - starttime < bitDelay) {
      run(); // keep processing incoming characters!!
    }
    starttime += bitDelay;
  }
}


MiniSerial mSerial;

ตัวอย่างการใช้งาน miniSerial พร้อมทั้งกะพริบหลอด LED ที่เชื่อมต่อกับ PA3

#include <miniSerial.h>
unsigned long t0;

void setup(void)
{
  Serial.begin(9600);
  // Serial.begin(19200, PA2, PA3);
  pinMode(LED_BUILTIN, OUTPUT);

  delay(500);  // IDE's serial terminal may take a bit of wakeup time. Don't lose first chrs.
  t0 = millis();   // used by loop()
  Serial.print("\nminiSerial RX/TX demo - software based, non native Serial port.\n");
  Serial.print("Transmit is unbuffered, Receive is buffered.\n");
  Serial.print("Type some input. Demo: Buffer is read each second.\n");

}

void loop()
{
  Serial.run();    // ESSENTIAL FOR RX BUFFERING SYSTEM. and no delay() allowed below.

  // can't use delay() so delay by a non-blocking method !!!
  unsigned long t1 = millis();
  if (t1 - t0 < 1000) // 1 sec
    return;
  t0 = t1;

  // here only every 1 sec:
  digitalWrite(LED_BUILTIN, 1 - digitalRead(LED_BUILTIN));
  int ch;
  while ((ch = Serial.read()) >= 0) // read any/all chrs from buffer
  {
    Serial.print("From buffer: ");
    Serial.println(ch);   // send it back out
  }
}

การหา Prime Number

จากตัวอย่างการใช้ RS232 เมื่อรวมกับการหา Prime Number จากบทความ LGT8F328P และ ET-BASE AVR EASY4809 เมื่อนำมาใช้กับ STM32F030F4P6 จะเป็นดังนี้ และผลของการทำงานเป็นดังภาพที่ 19

#include <miniSerial.h>
#include <math.h>

bool isPrimeNumber(uint16_t x) {
  uint16_t i;
  for (i = 2; i < x; i++) {
    if (x % i == 0) {
      return false;
    }
  }
  if (i == x)
    return true;
  return false;
}

int counter = 0;
uint32_t t0, t1;
void testPrimeNumber(uint16_t maxN) {
  t0 = millis();
  for (uint16_t n = 2; n < maxN; n++) {
    if (isPrimeNumber(n)) {
      counter++;
    }
  }
  t1 = millis();
}

void setup(void)
{
  Serial.begin(9600);
  testPrimeNumber(2000);
  Serial.print("Found ");
  Serial.print(counter, DEC);
  Serial.print(" in ");
  Serial.print(int(fabs(t1 - t0)), DEC);
  Serial.println(" milliseconds.");
}
void loop(void)
{
}
ภาพที่ 19 ผลลัพธ์จากการหา prime Number

I2C Client

จากบทความการสั่งงาน esp8266/esp32 ที่ใช้ MicroPython แล้วสั่งงานควบคุม Arduino Uno ผ่านทางบัส I2C นั้น เมื่อทางทีมเราได้ทดลองกับ stm32f030f4p6 โดยโค้ดของ Micropython เป็นดังนี้

#code-02 โค้ดของ ESP8266
from machine import Pin, I2C
import time

sclPin = Pin(5)
sdaPin = Pin(4)
devAddr = const(0x17)
devLedAddr = const(0x00)
devSpkAddr = const(0x01)
devBuffer = bytearray(2)

i2c = I2C(sda = sdaPin, scl = sclPin )
time.sleep_ms(250)
print("Begin of Program")
print(i2c.scan())
for i in range(10): # blink
    devBuffer[0] = devLedAddr
    devBuffer[1] = 0
    i2c.writeto(devAddr, devBuffer)
    time.sleep_ms(100)
    devBuffer[1] = 1
    i2c.writeto(devAddr, devBuffer)
    time.sleep_ms(100)
print("End of Program")

และโค้ดการทำงานฝั่ง stm32 เพื่อรับคำสั่งจาก I2C สำหรับเปิดและปิด LED ที่ต่ออยู่กับขา PA4 และการเชื่อมต่อกับบัส I2C กระทำโดยใช้ขา PA10 กับขา PA9

////////////////////////////////////////////////////////////////////////
// i2c client
// STM32F030F4P6
// Address 0x17
// Input
//     Byte 1
//        0 Blink LED : PA4
// (C) 2021, JarutEx
////////////////////////////////////////////////////////////////////////
#include <Wire.h>
int unoAddr = 0x17;
#define ledPin PA4

void i2cReceive( int bytes ) {
  uint8_t dInput = Wire.read(); // command
  uint8_t dValue = Wire.read(); // argument
  if (dInput == 0) { // Flash
    if (dValue == 0) {
      digitalWrite(ledPin, HIGH);
    } else if (dValue == 1) {
      digitalWrite(ledPin, LOW);
    }
  }
}
void i2cRequest() {
  Wire.write(0x00);
}
void setup() {
  pinMode( ledPin, OUTPUT );
  digitalWrite( ledPin, HIGH); // off LED
   Wire.begin(unoAddr);
  Wire.onReceive(i2cReceive);
  Wire.onRequest(i2cRequest);
}
void loop() {
}

สรุป

จากบทความนี้จะพบว่าการใช้งาน STM32F030F4P6 นั้นมีขั้นตอนที่ต้องกระทำอย่างเป็นนิจคือ เลือกโหมดว่ากำลังจะอัพโหลดโปรแกรมหรือรันโปรแกรม ต้องเตรียมสาย Serial เพื่อทำ ISP หรือใช้ ST-Link ผ่าน SWD ต้องเลือกโหมดทำงานของ STM32CubeProgrammer ว่าเป็น Serial หรือ SWD ให้ตรงกัน ที่สำคัญต้องเพิ่มโค้ดของ Brain Lavery เพื่อสื่อสาร RS232 ได้ถูกต้อง จะเห็นว่ามีขั้นตอนที่ละเอียดกว่าบอร์ด Arduino รุ่นอื่น ๆ และเมื่อเทียบกับบอร์ด STM32 ที่ทาง ETT ออกแบบให้สามารถเลือกโหมดด้วยการกดสวิตช์และใช้สาย Serial แบบ RS232 ในการอัพโหลดนับว่าสะดวกกว่า แต่อย่างไรก็ดี ด้วยราคาและประสิทธิภาพที่นับว่าดีกว่า Arduino แบบ 8 บิต จึงนับได้ว่าเป็นทางเลือกที่ดีทางหนึ่งสำหรับการเลือกเส้นทางการพัฒนางานสมองกลฝังตัวด้วย Cortex-M0 สุดท้ายนี้ขอให้สนุกกับการเขียนโปรแกรมครับ

หากท่านอยากพูดคุยหรือแลกเปลี่ยนกันสามารถคอมเมนท์ไว้ได้เลยครับ

แหล่งอ้างอิง

  1. Brian Lavery
  2. Brian Lavery miniSerial
  3. Arduino CORE STM32

(C) 2020-2021, โดย อ.ดนัย เจษฎาฐิติกุล/อ.จารุต บุศราทิจ
ปรับปรุงเมื่อ 2021-06-29, 2021-07-03, 2021-10-06