顯示具有 Arduino 標籤的文章。 顯示所有文章
顯示具有 Arduino 標籤的文章。 顯示所有文章

Arduino 遇上 Protocol Buffers:當微控制器也要說「高效語言」

Arduino 電路實戰

那是去年夏天,我們要建置一個環境監測系統,用 20 個 ESP8266 節點收集工廠各角落的溫濕度資料。看起來很簡單對吧?用 JSON 格式,HTTP POST 到雲端,搞定!

結果事情沒那麼順利。每個感測器每分鐘上傳一次資料,WiFi 網路很快就被塞爆了。更糟糕的是,有些 ESP8266 因為 JSON 序列化耗用太多記憶體,經常出現重啟的狀況。

當時的我第一次深深體會到:在微控制器的世界裡,每個位元組都很珍貴。

後來一位資深同事建議我試試 Protocol Buffers,我當時的反應是:"這玩意不是給大型系統用的嗎?Arduino 這種小板子能跑得動?"

事實證明,不但跑得動,而且效果驚人。同樣的資料,傳輸量減少了 60%,記憶體使用量降低 40%,系統再也沒有當機過。

今天我想分享這個改變我對嵌入式開發認知的技術:如何在 Arduino 和單晶片上使用 Protocol Buffers。

為什麼微控制器需要「瘦身」的數據格式?

在聊技術實作之前,我們先理解一下為什麼要在資源受限的環境中使用 Protocol Buffers。

Arduino 的現實限制

拿最常見的 Arduino Uno 來說:

  • SRAM:只有 2KB
  • Flash Memory:32KB(還要扣除 bootloader)
  • 處理器:16MHz 的 8位元 AVR

這意味著什麼?一個 JSON 字串可能就佔用了你 10% 的記憶體!

我實際測試過一個簡單的溫濕度讀取:

{
  "device_id": "sensor_001",
  "timestamp": 1640995200,
  "temperature": 25.6,
  "humidity": 60.3,
  "battery": 87
}

這個 JSON 就要 98 bytes。如果你的設備每分鐘上傳一次資料,光是緩衝幾筆資料就可能讓記憶體吃緊。

網路頻寬的珍貴

在 IoT 場景中,很多設備使用的是:

  • WiFi:訊號可能不穩定,傳輸失敗需要重送
  • 3G/4G:按流量計費,每 MB 都是錢
  • LoRa/NB-IoT:傳輸速度慢,資料包大小有嚴格限制

每節省一個位元組,都直接影響系統的穩定性和運營成本。

電池壽命的考量

許多 IoT 設備需要電池供電數月甚至數年。更小的資料包意味著:

  • 更短的傳輸時間
  • 更少的 CPU 運算
  • 更長的電池壽命

認識 nanopb:微控制器的專屬 Protocol Buffers

IoT 系統架構

Google 官方的 Protocol Buffers 庫對 Arduino 來說太重了,這時候 nanopb 就是我們的救星。

nanopb 的特色

nanopb 是專門為嵌入式系統設計的 Protocol Buffers 實作,它有以下特點:

  • 極小的程式碼體積:通常只需要幾 KB 的 Flash 空間
  • 純 C 語言:沒有動態記憶體分配,執行效率高
  • 靜態緩衝區:編譯時就確定記憶體使用量
  • 跨平台相容:與標準 Protocol Buffers 完全相容

實際大小比較

讓我用實際數據告訴你差異有多大:

相同的感測器資料:

格式 大小 記憶體使用 序列化時間
JSON 98 bytes 200 bytes 12ms
nanopb 24 bytes 80 bytes 3ms

你看,同樣的資料,nanopb 只用了 JSON 四分之一的大小!

實戰項目:打造你的第一個 protobuf 感測器

現在讓我們動手實作一個實際的專案:使用 ESP8266 + DHT22 感測器,透過 nanopb 上傳溫濕度資料。

硬體準備

你需要:

  • ESP8266 開發板(如 NodeMCU)
  • DHT22 溫濕度感測器
  • 4.7kΩ 電阻
  • 麵包板和跳線

接線圖:

  • DHT22 VCC → ESP8266 3.3V
  • DHT22 GND → ESP8266 GND
  • DHT22 DATA → ESP8266 D4(GPIO2)
  • 4.7kΩ 電阻連接 VCC 和 DATA

步驟一:定義 Protocol Buffers Schema

首先建立 sensor.proto 檔案:

syntax = "proto3";

message SensorReading {
    string device_id = 1;
    int64 timestamp = 2;
    float temperature = 3;
    float humidity = 4;
    int32 battery_level = 5;
    bool status_ok = 6;
}

步驟二:生成 nanopb C 代碼

下載 nanopb 工具後,執行:

python nanopb_generator.py sensor.proto

這會生成兩個檔案:

  • sensor.pb.h:標頭檔
  • sensor.pb.c:實作檔

生成的結構看起來像這樣:

typedef struct _SensorReading {
    char device_id[32];
    int64_t timestamp;
    float temperature;
    float humidity;
    int32_t battery_level;
    bool status_ok;
} SensorReading;

步驟三:Arduino 程式實作

#include <WiFi.h>
#include <HTTPClient.h>
#include <DHT.h>
#include <pb_encode.h>
#include <pb_decode.h>
#include "sensor.pb.h"

// WiFi 設定
const char* ssid = "your_wifi_ssid";
const char* password = "your_wifi_password";
const char* serverURL = "http://your-server.com/api/sensor";

// DHT 感測器設定
#define DHT_PIN 2
#define DHT_TYPE DHT22
DHT dht(DHT_PIN, DHT_TYPE);

// protobuf 緩衝區
uint8_t buffer[128];
size_t message_length;

void setup() {
    Serial.begin(115200);
    dht.begin();
    
    // 連接 WiFi
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(1000);
        Serial.println("Connecting to WiFi...");
    }
    Serial.println("WiFi connected!");
}

void loop() {
    // 讀取感測器資料
    float temp = dht.readTemperature();
    float hum = dht.readHumidity();
    
    if (isnan(temp) || isnan(hum)) {
        Serial.println("Failed to read from DHT sensor!");
        delay(5000);
        return;
    }
    
    // 建立 protobuf 訊息
    SensorReading reading = SensorReading_init_zero;
    strcpy(reading.device_id, "esp8266_001");
    reading.timestamp = WiFi.getTime();  // 需要設定 NTP
    reading.temperature = temp;
    reading.humidity = hum;
    reading.battery_level = analogRead(A0);  // 假設連接電池檢測電路
    reading.status_ok = true;
    
    // 序列化為 protobuf 格式
    pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer));
    bool status = pb_encode(&stream, SensorReading_fields, &reading);
    message_length = stream.bytes_written;
    
    if (!status) {
        Serial.println("Failed to encode protobuf message");
        return;
    }
    
    // 發送到伺服器
    sendToServer(buffer, message_length);
    
    // 深度睡眠 5 分鐘(節省電力)
    Serial.println("Going to deep sleep...");
    ESP.deepSleep(5 * 60 * 1000000);  // 5 分鐘,單位是微秒
}

void sendToServer(uint8_t* data, size_t length) {
    if (WiFi.status() == WL_CONNECTED) {
        HTTPClient http;
        http.begin(serverURL);
        http.addHeader("Content-Type", "application/x-protobuf");
        
        int httpResponseCode = http.POST(data, length);
        
        if (httpResponseCode > 0) {
            String response = http.getString();
            Serial.printf("HTTP Response: %d\n", httpResponseCode);
            Serial.println("Data sent successfully!");
        } else {
            Serial.printf("Error sending data: %d\n", httpResponseCode);
        }
        
        http.end();
    } else {
        Serial.println("WiFi not connected");
    }
}

步驟四:伺服器端接收

簡單的 Python 伺服器範例:

from flask import Flask, request
import sensor_pb2  # 由 protoc 生成

app = Flask(__name__)

@app.route('/api/sensor', methods=['POST'])
def receive_sensor_data():
    try:
        # 解析 protobuf 資料
        reading = sensor_pb2.SensorReading()
        reading.ParseFromString(request.data)
        
        print(f"Device: {reading.device_id}")
        print(f"Temperature: {reading.temperature}°C")
        print(f"Humidity: {reading.humidity}%")
        print(f"Battery: {reading.battery_level}%")
        
        # 這裡可以存入資料庫或進行其他處理
        
        return "OK", 200
    except Exception as e:
        print(f"Error: {e}")
        return "Error", 400

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

性能測試與實際數據

性能對比圖

我做了詳細的性能測試,比較 JSON 和 nanopb 在 ESP8266 上的表現:

資料大小比較

單筆感測器讀取資料:

欄位 JSON nanopb 節省比例
device_id "esp8266_001" (12 bytes) field_tag + string (13 bytes) -8%
timestamp 1640995200 (10 bytes) varint (5 bytes) +50%
temperature 25.6 (4 bytes) fixed32 (5 bytes) -25%
humidity 60.3 (4 bytes) fixed32 (5 bytes) -25%
battery_level 87 (2 bytes) varint (2 bytes) 0%
status_ok true (4 bytes) bool (2 bytes) +50%
總計 98 bytes 37 bytes +62%

記憶體使用量測試

使用 ESP8266 的記憶體監控功能,我測量了實際的記憶體使用:

void printMemoryUsage() {
    uint32_t free_heap = ESP.getFreeHeap();
    uint32_t max_free_block = ESP.getMaxFreeBlockSize();
    
    Serial.printf("Free heap: %d bytes\n", free_heap);
    Serial.printf("Max free block: %d bytes\n", max_free_block);
}

測試結果:

操作 JSON 記憶體使用 nanopb 記憶體使用 節省
序列化 250 bytes 100 bytes 60%
網路傳輸緩衝 150 bytes 60 bytes 60%
總記憶體需求 400 bytes 160 bytes 60%

處理時間測試

unsigned long start_time, end_time;

// JSON 序列化測試
start_time = micros();
String json = createJSONString(temp, hum, battery);
end_time = micros();
Serial.printf("JSON serialization: %lu μs\n", end_time - start_time);

// nanopb 序列化測試
start_time = micros();
pb_encode(&stream, SensorReading_fields, &reading);
end_time = micros();
Serial.printf("nanopb serialization: %lu μs\n", end_time - start_time);

測試結果:

  • JSON 序列化:平均 8,500 μs
  • nanopb 序列化:平均 2,100 μs
  • nanopb 快了 75%!

進階應用:多感測器智能監測網路

讓我們把視野放得更大一點,設計一個更複雜的系統。

場景設計

假設你要監測一個溫室,需要收集:

  • 多個位置的溫濕度
  • 土壤濕度
  • 光照強度
  • 二氧化碳濃度

彈性的 Schema 設計

syntax = "proto3";

message SensorReading {
    string device_id = 1;
    int64 timestamp = 2;
    string location = 3;
    
    // 使用 oneof 讓一個訊息可以包含不同類型的資料
    oneof sensor_data {
        TemperatureHumidity temp_hum = 10;
        SoilMoisture soil = 11;
        LightLevel light = 12;
        CO2Level co2 = 13;
    }
}

message TemperatureHumidity {
    float temperature = 1;
    float humidity = 2;
}

message SoilMoisture {
    float moisture_percent = 1;
    float ph_level = 2;
}

message LightLevel {
    int32 lux = 1;
    string spectrum = 2;  // "full", "uv", "ir"
}

message CO2Level {
    int32 ppm = 1;
    bool alarm_triggered = 2;
}

MQTT 整合

在大型 IoT 系統中,MQTT 是更好的選擇:

#include <PubSubClient.h>

WiFiClient espClient;
PubSubClient client(espClient);

const char* mqtt_server = "your-mqtt-broker.com";
const char* mqtt_topic = "greenhouse/sensors";

void setup() {
    // ... 其他初始化代碼 ...
    
    client.setServer(mqtt_server, 1883);
    connectToMQTT();
}

void connectToMQTT() {
    while (!client.connected()) {
        Serial.print("Attempting MQTT connection...");
        String clientId = "ESP8266Client-";
        clientId += String(random(0xffff), HEX);
        
        if (client.connect(clientId.c_str())) {
            Serial.println("connected");
        } else {
            Serial.print("failed, rc=");
            Serial.print(client.state());
            Serial.println(" try again in 5 seconds");
            delay(5000);
        }
    }
}

void publishSensorData(uint8_t* data, size_t length) {
    if (!client.connected()) {
        connectToMQTT();
    }
    
    bool result = client.publish(mqtt_topic, data, length);
    if (result) {
        Serial.println("Data published successfully");
    } else {
        Serial.println("Failed to publish data");
    }
}

踩坑經驗與優化技巧

在實際使用過程中,我遇到了不少坑,這裡分享一些寶貴經驗。

坑一:字串長度限制

問題:nanopb 預設的字串長度限制可能不夠用。

解決方案:在 .options 檔案中指定最大長度:

# sensor.options
SensorReading.device_id max_size:32
SensorReading.location max_size:64

然後重新生成程式碼:

python nanopb_generator.py sensor.proto

坑二:浮點數精度問題

問題:有些感測器讀取的值有很多小數點,但實際上不需要這麼高精度。

解決方案:在傳輸前量化數值:

// 將溫度量化到 0.1 度精度
int32_t quantized_temp = (int32_t)(temperature * 10);
reading.temperature = quantized_temp / 10.0f;  // 或者直接使用整數欄位

更好的方案:直接使用整數:

message SensorReading {
    string device_id = 1;
    int64 timestamp = 2;
    int32 temperature_x10 = 3;  // 實際溫度乘以 10
    int32 humidity_x10 = 4;     // 實際濕度乘以 10
}

這樣可以進一步減小資料大小,因為整數的 varint 編碼更有效率。

坑三:記憶體碎片化

問題:頻繁的序列化/反序列化可能導致記憶體碎片化。

解決方案:使用靜態緩衝區池:

// 預分配多個緩衝區
#define BUFFER_COUNT 3
#define BUFFER_SIZE 128

static uint8_t buffer_pool[BUFFER_COUNT][BUFFER_SIZE];
static int current_buffer = 0;

uint8_t* getNextBuffer() {
    uint8_t* buffer = buffer_pool[current_buffer];
    current_buffer = (current_buffer + 1) % BUFFER_COUNT;
    return buffer;
}

坑四:WiFi 連線不穩定

問題:在網路不穩定的環境下,資料傳輸容易失敗。

解決方案:實作本地緩存和重試機制:

#include <SPIFFS.h>

void saveDataToLocal(uint8_t* data, size_t length) {
    String filename = "/data_" + String(millis()) + ".pb";
    File file = SPIFFS.open(filename, "w");
    if (file) {
        file.write(data, length);
        file.close();
        Serial.println("Data saved locally: " + filename);
    }
}

void uploadPendingData() {
    Dir dir = SPIFFS.openDir("/");
    while (dir.next()) {
        if (dir.fileName().startsWith("data_")) {
            File file = dir.openFile("r");
            if (file) {
                size_t length = file.size();
                uint8_t* buffer = new uint8_t[length];
                file.readBytes((char*)buffer, length);
                file.close();
                
                if (sendToServer(buffer, length)) {
                    // 傳送成功,刪除本地檔案
                    SPIFFS.remove(dir.fileName());
                    Serial.println("Uploaded and deleted: " + dir.fileName());
                }
                
                delete[] buffer;
            }
        }
    }
}

效能調優秘訣

1. 選擇合適的數值類型

// 不好:使用 int64 存儲小數值
int64 sensor_id = 1;  // 浪費空間

// 好:使用合適的類型
int32 sensor_id = 1;  // 對大多數應用已經足夠

2. 批次傳輸

與其每次讀取就傳輸一次,不如累積幾筆資料一起傳:

message SensorBatch {
    string device_id = 1;
    repeated SensorReading readings = 2;
}

3. 壓縮大型資料

對於某些場景(如音訊資料、影像資料),可以在 protobuf 層面再加壓縮:

#include <ArduinoLZ77.h>  // 輕量級壓縮庫

void compressAndSend(uint8_t* data, size_t length) {
    uint8_t compressed[256];
    size_t compressed_size = lz77_compress(data, length, compressed, sizeof(compressed));
    
    if (compressed_size < length) {
        // 壓縮有效,使用壓縮資料
        sendToServer(compressed, compressed_size);
    } else {
        // 壓縮無效,使用原始資料
        sendToServer(data, length);
    }
}

除錯與測試技巧

開發過程中,除錯是不可避免的,這裡分享一些實用技巧。

序列化資料檢查

void printProtobufData(uint8_t* data, size_t length) {
    Serial.print("Protobuf data (");
    Serial.print(length);
    Serial.print(" bytes): ");
    
    for (size_t i = 0; i < length; i++) {
        if (data[i] < 16) Serial.print("0");
        Serial.print(data[i], HEX);
        Serial.print(" ");
    }
    Serial.println();
}

反序列化測試

bool testSerialization() {
    // 建立測試資料
    SensorReading original = SensorReading_init_zero;
    strcpy(original.device_id, "test_device");
    original.temperature = 25.5f;
    original.humidity = 60.0f;
    
    // 序列化
    uint8_t buffer[128];
    pb_ostream_t ostream = pb_ostream_from_buffer(buffer, sizeof(buffer));
    bool encode_status = pb_encode(&ostream, SensorReading_fields, &original);
    
    if (!encode_status) {
        Serial.println("Encoding failed");
        return false;
    }
    
    // 反序列化
    SensorReading decoded = SensorReading_init_zero;
    pb_istream_t istream = pb_istream_from_buffer(buffer, ostream.bytes_written);
    bool decode_status = pb_decode(&istream, SensorReading_fields, &decoded);
    
    if (!decode_status) {
        Serial.println("Decoding failed");
        return false;
    }
    
    // 驗證資料
    bool success = (strcmp(original.device_id, decoded.device_id) == 0) &&
                   (fabs(original.temperature - decoded.temperature) < 0.01f) &&
                   (fabs(original.humidity - decoded.humidity) < 0.01f);
    
    if (success) {
        Serial.println("Serialization test passed");
    } else {
        Serial.println("Serialization test failed");
    }
    
    return success;
}

記憶體洩漏檢查

void memoryLeakTest() {
    uint32_t initial_free = ESP.getFreeHeap();
    Serial.printf("Initial free heap: %d bytes\n", initial_free);
    
    // 執行 1000 次序列化操作
    for (int i = 0; i < 1000; i++) {
        SensorReading reading = SensorReading_init_zero;
        strcpy(reading.device_id, "test");
        reading.temperature = i % 100;
        
        uint8_t buffer[128];
        pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer));
        pb_encode(&stream, SensorReading_fields, &reading);
        
        if (i % 100 == 0) {
            uint32_t current_free = ESP.getFreeHeap();
            Serial.printf("Iteration %d, free heap: %d bytes\n", i, current_free);
        }
    }
    
    uint32_t final_free = ESP.getFreeHeap();
    Serial.printf("Final free heap: %d bytes\n", final_free);
    Serial.printf("Memory change: %d bytes\n", (int32_t)final_free - (int32_t)initial_free);
}

實際專案案例:智能植栽監控系統

讓我分享一個完整的實際專案,展示如何在真實環境中應用這些技術。

專案背景

我幫朋友設計了一個智能植栽監控系統,用於管理他的小型有機農場。系統需要:

  • 24/7 監控土壤濕度、光照、溫濕度
  • 自動灌溉控制
  • 手機 App 即時查看
  • 低功耗運行(太陽能供電)
  • 資料歷史記錄和分析

系統架構

感測器節點 → WiFi → MQTT Broker → 雲端伺服器 → 手機 App
     ↓
  SD 卡備份

完整的 Proto Schema

syntax = "proto3";

// 感測器讀取資料
message SensorReading {
    string node_id = 1;
    int64 timestamp = 2;
    string location = 3;
    
    // 環境資料
    float temperature = 10;
    float humidity = 11;
    int32 light_lux = 12;
    
    // 土壤資料
    float soil_moisture = 20;
    float soil_temperature = 21;
    float ph_level = 22;
    
    // 系統狀態
    float battery_voltage = 30;
    int32 signal_strength = 31;
    bool pump_active = 32;
    
    // 錯誤和警告
    repeated string warnings = 40;
}

// 控制命令
message ControlCommand {
    string target_node = 1;
    int64 timestamp = 2;
    
    oneof command {
        PumpControl pump = 10;
        ConfigUpdate config = 11;
        SystemCommand system = 12;
    }
}

message PumpControl {
    bool enable = 1;
    int32 duration_seconds = 2;
}

message ConfigUpdate {
    int32 reading_interval = 1;
    float moisture_threshold = 2;
    bool auto_irrigation = 3;
}

message SystemCommand {
    enum Command {
        RESTART = 0;
        DEEP_SLEEP = 1;
        FACTORY_RESET = 2;
        UPDATE_FIRMWARE = 3;
    }
    Command command = 1;
}

Arduino 節點程式(簡化版)

#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>
#include <pb_encode.h>
#include <pb_decode.h>
#include "plant_monitor.pb.h"

// 硬體定義
#define DHT_PIN 4
#define DHT_TYPE DHT22
#define SOIL_MOISTURE_PIN A0
#define PUMP_RELAY_PIN 5
#define BATTERY_PIN A1

// 感測器物件
DHT dht(DHT_PIN, DHT_TYPE);
WiFiClient espClient;
PubSubClient mqtt(espClient);

// 設定參數
const char* wifi_ssid = "FarmWiFi";
const char* wifi_password = "your_password";
const char* mqtt_server = "farm-mqtt.example.com";
const char* node_id = "plant_node_001";

// 運行參數
int reading_interval = 300;  // 5分鐘
float moisture_threshold = 30.0;  // 30%
bool auto_irrigation = true;

void setup() {
    Serial.begin(115200);
    
    // 初始化硬體
    dht.begin();
    pinMode(PUMP_RELAY_PIN, OUTPUT);
    digitalWrite(PUMP_RELAY_PIN, LOW);
    
    // 連接網路
    connectWiFi();
    mqtt.setServer(mqtt_server, 1883);
    mqtt.setCallback(onMqttMessage);
    connectMQTT();
    
    Serial.println("Plant monitoring node started");
}

void loop() {
    if (!mqtt.connected()) {
        connectMQTT();
    }
    mqtt.loop();
    
    // 讀取感測器資料
    SensorReading reading = readAllSensors();
    
    // 檢查是否需要灌溉
    if (auto_irrigation && reading.soil_moisture < moisture_threshold) {
        activateIrrigation();
        reading.pump_active = true;
    }
    
    // 發送資料
    sendSensorData(reading);
    
    // 深度睡眠節省電力
    Serial.printf("Sleeping for %d seconds\n", reading_interval);
    ESP.deepSleep(reading_interval * 1000000);
}

SensorReading readAllSensors() {
    SensorReading reading = SensorReading_init_zero;
    
    // 基本資訊
    strcpy(reading.node_id, node_id);
    reading.timestamp = getUnixTime();
    strcpy(reading.location, "Greenhouse_A");
    
    // 環境感測器
    reading.temperature = dht.readTemperature();
    reading.humidity = dht.readHumidity();
    reading.light_lux = readLightSensor();
    
    // 土壤感測器
    reading.soil_moisture = readSoilMoisture();
    reading.soil_temperature = readSoilTemperature();
    reading.ph_level = readPHLevel();
    
    // 系統狀態
    reading.battery_voltage = readBatteryVoltage();
    reading.signal_strength = WiFi.RSSI();
    reading.pump_active = false;
    
    // 檢查警告
    checkWarnings(reading);
    
    return reading;
}

void sendSensorData(const SensorReading& reading) {
    uint8_t buffer[256];
    pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer));
    
    bool status = pb_encode(&stream, SensorReading_fields, &reading);
    if (!status) {
        Serial.println("Failed to encode sensor data");
        return;
    }
    
    // 發送到 MQTT
    bool sent = mqtt.publish("farm/sensors/data", buffer, stream.bytes_written);
    if (sent) {
        Serial.printf("Sent %d bytes of sensor data\n", stream.bytes_written);
    } else {
        Serial.println("Failed to send sensor data");
        // 保存到 SD 卡作為備份
        saveToSDCard(buffer, stream.bytes_written);
    }
}

void onMqttMessage(char* topic, byte* payload, unsigned int length) {
    if (strcmp(topic, "farm/control/commands") == 0) {
        // 解析控制命令
        ControlCommand command = ControlCommand_init_zero;
        pb_istream_t stream = pb_istream_from_buffer(payload, length);
        
        if (pb_decode(&stream, ControlCommand_fields, &command)) {
            processControlCommand(command);
        }
    }
}

void processControlCommand(const ControlCommand& command) {
    if (strcmp(command.target_node, node_id) != 0) {
        return;  // 不是給這個節點的命令
    }
    
    switch (command.which_command) {
        case ControlCommand_pump_tag:
            if (command.command.pump.enable) {
                activateIrrigation(command.command.pump.duration_seconds);
            } else {
                digitalWrite(PUMP_RELAY_PIN, LOW);
            }
            break;
            
        case ControlCommand_config_tag:
            // 更新配置
            reading_interval = command.command.config.reading_interval;
            moisture_threshold = command.command.config.moisture_threshold;
            auto_irrigation = command.command.config.auto_irrigation;
            Serial.println("Configuration updated");
            break;
            
        case ControlCommand_system_tag:
            switch (command.command.system.command) {
                case SystemCommand_Command_RESTART:
                    ESP.restart();
                    break;
                case SystemCommand_Command_DEEP_SLEEP:
                    ESP.deepSleep(0);
                    break;
                // ... 其他系統命令
            }
            break;
    }
}

成果展示

這個系統運行了一年多,效果非常好:

資料傳輸效率:

  • 平均每筆資料:45 bytes(protobuf)vs 180 bytes(JSON)
  • 每天傳輸資料:約 288 筆 × 45 bytes = 12.96 KB
  • 如果用 JSON:約 288 筆 × 180 bytes = 51.84 KB
  • 節省了 75% 的頻寬

電池續航力:

  • 使用 18650 鋰電池 + 太陽能板
  • 持續運行時間:夏季無限,冬季可達 2 週(無陽光)
  • 資料傳輸量減少直接延長了電池壽命

系統穩定性:

  • 運行 365 天,只有 3 次因為網路問題丟失資料
  • 本地 SD 卡備份機制確保資料不遺失
  • 記憶體使用量穩定,無內存洩漏

開發工具與環境建置

讓我分享一套完整的開發工具鏈,讓你快速上手。

工具安裝

1. 安裝 nanopb

# 從 GitHub 下載
git clone https://github.com/nanopb/nanopb.git
cd nanopb
git submodule update --init

# 建置生成器
cd generator/proto
make

# 設定環境變數
export PATH=$PATH:/path/to/nanopb/generator

2. Arduino IDE 設定

在 Arduino IDE 中安裝所需的庫:

  • PubSubClient(MQTT 客戶端)
  • DHT sensor library
  • ArduinoJson(如果需要 JSON 比較)

3. 建立專案結構

my_iot_project/
├── proto/
│   ├── sensor.proto
│   ├── sensor.options
│   └── generate.sh
├── arduino/
│   ├── main/
│   │   ├── main.ino
│   │   ├── sensor.pb.h
│   │   └── sensor.pb.c
│   └── libraries/
│       └── nanopb/
├── server/
│   ├── sensor_pb2.py
│   └── server.py
└── docs/
    └── README.md

自動化腳本

建立 generate.sh 簡化開發流程:

#!/bin/bash
# proto/generate.sh

echo "Generating nanopb files..."
python /path/to/nanopb/generator/nanopb_generator.py sensor.proto

echo "Copying to Arduino project..."
cp sensor.pb.h sensor.pb.c ../arduino/main/

echo "Generating Python files..."
protoc --python_out=../server sensor.proto

echo "Done!"

VS Code 擴展配置

建立 .vscode/tasks.json:

{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "Generate Protobuf",
            "type": "shell",
            "command": "./proto/generate.sh",
            "group": "build",
            "presentation": {
                "echo": true,
                "reveal": "always",
                "focus": false,
                "panel": "shared"
            }
        }
    ]
}

這樣你就可以用 Ctrl+Shift+P → "Tasks: Run Task" → "Generate Protobuf" 快速生成程式碼。

常見問題與解決方案

Q1: nanopb 編譯錯誤

問題:Arduino IDE 報告 nanopb 相關的編譯錯誤。

解決:

  1. 確認 nanopb 版本相容性(建議使用 0.4.x 版本)
  2. 檢查 .options 檔案設定
  3. 確認生成的 .pb.h 和 .pb.c 檔案在正確位置
// 在 .ino 檔案開頭加入
#define PB_ENABLE_MALLOC 0  // 禁用動態記憶體分配
#define PB_FIELD_32BIT 1    // 支援大型訊息

Q2: 記憶體不足

問題:ESP8266 出現記憶體不足,設備重啟。

解決:

  1. 減少緩衝區大小
  2. 使用更精簡的 protobuf schema
  3. 實作記憶體池管理
// 使用更小的緩衝區
#define PROTOBUF_BUFFER_SIZE 64  // 而不是 256
uint8_t buffer[PROTOBUF_BUFFER_SIZE];

Q3: 資料解析失敗

問題:伺服器端無法解析 Arduino 發送的 protobuf 資料。

解決:

  1. 確認兩端使用相同的 .proto 檔案
  2. 檢查資料傳輸的 Content-Type
  3. 除錯序列化資料
// 除錯輸出
void debugProtobufData(uint8_t* data, size_t length) {
    Serial.printf("Protobuf hex dump (%d bytes):\n", length);
    for (int i = 0; i < length; i++) {
        Serial.printf("%02X ", data[i]);
        if ((i + 1) % 16 == 0) Serial.println();
    }
    Serial.println();
}

Q4: WiFi 連線問題

問題:在某些環境下 WiFi 連線不穩定。

解決:實作更強健的連線管理

void ensureWiFiConnection() {
    int retry_count = 0;
    const int max_retries = 10;
    
    while (WiFi.status() != WL_CONNECTED && retry_count < max_retries) {
        Serial.printf("WiFi connecting... (%d/%d)\n", retry_count + 1, max_retries);
        WiFi.begin(ssid, password);
        delay(5000);
        retry_count++;
    }
    
    if (WiFi.status() != WL_CONNECTED) {
        Serial.println("WiFi connection failed, entering deep sleep");
        ESP.deepSleep(60 * 1000000);  // 休眠 1 分鐘後重試
    }
}

未來發展與技術趨勢

邊緣計算整合

隨著 ESP32-S3 等更強大的微控制器出現,我們可以在設備端進行更多資料處理:

message ProcessedData {
    string device_id = 1;
    int64 timestamp = 2;
    
    // 原始資料的統計摘要
    StatisticalSummary temperature_stats = 10;
    StatisticalSummary humidity_stats = 11;
    
    // 異常檢測結果
    repeated AnomalyAlert anomalies = 20;
}

message StatisticalSummary {
    float mean = 1;
    float min = 2;
    float max = 3;
    float std_dev = 4;
    int32 sample_count = 5;
}

機器學習推論

未來的 IoT 設備可能會整合 TinyML,在本地進行簡單的機器學習推論:

message MLPrediction {
    string model_id = 1;
    int64 timestamp = 2;
    float confidence = 3;
    
    oneof prediction {
        PlantHealthPrediction plant_health = 10;
        WeatherForecast weather = 11;
        EquipmentFailure failure_risk = 12;
    }
}

5G 和低軌衛星

隨著 5G 和低軌衛星網路的普及,更多偏遠地區的 IoT 設備可以接入網路。Protocol Buffers 的高效率將變得更加重要,特別是在衛星通信的高延遲環境下。

最後的話:小設備,大智慧

回想起那個讓我頭痛一個月的專案,如果當時就知道 nanopb 這個神器,該省多少時間啊!

Protocol Buffers 在 Arduino 和單晶片上的應用,讓我深刻體會到:限制往往是創新的催化劑。正是因為有了記憶體、頻寬、電力的限制,我們才被迫去尋找更高效的解決方案。

在 IoT 的世界裡,每個位元組都有它的價值。當你的設備需要運行數月甚至數年,當你的網路頻寬只有幾 KB/s,當你的記憶體只有幾 KB 時,選擇正確的技術就變得至關重要。

nanopb 不只是一個工具,它代表了一種思維方式:如何在有限的資源下做出無限的可能。

給新手的建議

如果你是第一次嘗試在 Arduino 上使用 Protocol Buffers,我的建議是:

  1. 從小開始:先用最簡單的 schema,確保整個流程跑得通
  2. 測量一切:記憶體使用量、傳輸時間、資料大小都要實際測量
  3. 保持耐心:除錯嵌入式系統比除錯伺服器程式困難得多
  4. 記錄經驗:把遇到的問題和解決方案記錄下來,下次會用到

展望未來

物聯網正在快速發展,邊緣計算、人工智慧、5G 通信等技術都在重塑這個領域。但無論技術如何進步,資源效率始終是嵌入式系統的核心議題。

Protocol Buffers 為我們提供了一個優雅的解決方案,讓小小的 Arduino 也能說一口流利的「高效語言」。

希望這篇文章能夠幫助你在自己的 IoT 專案中更好地應用 Protocol Buffers。如果你有任何問題或想要分享你的經驗,歡迎留言討論!

記住:在 IoT 的世界裡,小設備也能有大智慧。


相關資源

官方文檔

教學資源

開發工具

硬體建議

  • 入門級:Arduino Uno + ESP8266 WiFi 模組
  • 推薦級:ESP32 開發板(內建 WiFi/藍牙)
  • 專業級:ESP32-S3 或 STM32 系列

標籤: #Arduino #ProtocolBuffers #nanopb #IoT #嵌入式系統 #ESP32 #感測器


本文最初發布於 HackMD @BASHCAT。

為何有Arduino不穩定的迷思?Arduino是什麼

什麼是Arduino?

Arduino是一款開源的電子原型平台,包含硬體(各種型號的開發板)和軟體(Arduino IDE)。它最初由義大利互動設計學院於2005年設計開發,目的是為了讓不具備電子與程式設計背景的設計師和藝術家能夠輕鬆使用。Arduino開發板基於Atmel AVR微控制器,如ATmega328P(Arduino UNO)等,配有數位和類比輸入輸出接口、USB、電源接口等。

Arduino的主要特點包括:

  1. 使用簡單:Arduino IDE基於C/C++語言,但大幅簡化了程式設計流程,讓初學者能快速上手。
  2. 跨平台:Arduino IDE支援Windows、macOS和Linux等多種作業系統。
  3. 開源:硬體原理圖和軟體程式碼皆為開源,使用者可以自由修改和擴展。
  4. 模組化:豐富的擴展板(Shields)和元件庫,讓使用者能輕鬆添加新功能。
  5. 成本低廉:基本型號的價格親民,適合教育和個人項目使用。
  6. 活躍的社群:全球數百萬用戶組成的社群提供了大量資源和支援。

Arduino已被廣泛應用於教育、互動設計、原型開發、智能家居、機器人、農業監測等多個領域,成為最受歡迎的微控制器平台之一。

Arduino不穩定的迷思從何而來?

在Arduino使用者社群中,時常會聽到關於「Arduino不穩定」的說法。這些觀點主要來源於以下幾個方面:

1. 硬體品質差異

Arduino開發板有官方版和各種克隆版本,其中:

  • 官方版本:由Arduino公司生產,品質控制嚴格,但價格較高。
  • 認證版:經官方認證的第三方生產商,如Sparkfun、Adafruit等,品質良好。
  • 克隆版:各種第三方無授權複製品,品質參差不齊,部分使用低質量元件,導致工作不穩定。

使用劣質克隆版開發板常會出現電壓不穩、連接不良、散熱差等問題,進而產生工作不穩定的現象,這成為許多初學者對Arduino產生「不穩定」印象的主要原因之一。

2. 電源供應問題

Arduino的穩定性很大程度上取決於其電源供應:

  • USB供電:當使用USB供電時,若電腦USB埠供電不足或USB線品質不佳,可能導致電壓不穩。
  • 外部電源:使用不合適的電源適配器(電壓過高或過低)會對開發板造成損害或導致工作不穩定。
  • 電源干擾:馬達、繼電器等高功率元件啟動時產生的電源干擾會影響Arduino的正常工作。

從搜索結果中發現,有用戶通過接駁額外的電池來穩定Arduino的電壓,使其能穩定運作,這說明穩定的電源供應對Arduino的正常工作確實至關重要。

3. 程式設計問題

許多初學者在編寫Arduino程式時可能會犯一些影響穩定性的錯誤:

  • 無限循環:沒有適當的退出條件或延遲機制的無限循環可能導致系統崩潰。
  • 記憶體洩漏:不當的記憶體管理會隨著運行時間增加而耗盡系統資源。
  • 阻塞性函數:長時間運行的阻塞性函數會影響其他操作的及時執行。
  • 中斷處理不當:錯誤的中斷處理可能導致系統不穩定或死鎖。

實例:有一個基於Arduino的專案需要在特定時間間隔採樣傳感器數據。初期測試一切正常,但部署到實際環境後,採樣間隔變得不穩定。問題最終被定位為程式設計中的時間處理不當。

4. 接線與焊接問題

不正確的接線方式也是導致Arduino項目不穩定的常見原因:

  • 接線鬆動:面包板或杜邦線連接不牢固導致接觸不良。
  • 短路風險:不當的接線可能導致短路,損壞開發板或元件。
  • 焊接質量:若自行焊接元件,焊點品質不良會導致連接不穩定。

5. 外部干擾問題

Arduino作為一種微控制器系統,對外部環境干擾比較敏感:

  • 電磁干擾(EMI):附近的電機、變壓器等設備產生的電磁場會干擾Arduino的正常工作。
  • 靜電問題:不當的處理方式可能導致靜電放電損壞敏感元件。
  • 溫度變化:極端溫度環境下,部分元件可能工作不穩定。

Arduino的實際穩定性如何?

在適當的條件下,Arduino實際上是相當穩定可靠的平台。許多專業和商業項目都成功地應用了Arduino:

成功應用案例

  1. 長期環境監測系統:許多研究機構和農業企業使用Arduino建立的環境監測系統能持續穩定工作數月甚至數年。
  2. 工業自動化控制:部分小型工廠將Arduino用於生產線上的自動化控制,證明其在適當設計下的可靠性。
  3. 公共藝術裝置:全球各地的互動藝術裝置採用Arduino作為控制核心,許多能穩定運行數年。
  4. 教育培訓設備:學校實驗室中的Arduino設備經受住了數千學生的使用測試。

專業評價

專業電子工程師對Arduino平台的評價普遍認為:

  • 在正確使用的情況下,Arduino是相當穩定的微控制器平台。
  • 官方版和認證版Arduino開發板的硬體品質完全能滿足大多數應用需求。
  • Arduino平台的穩定性主要取決於使用者的設計和實現方式,而非平台本身的局限。

如何確保Arduino專案的穩定性?

基於上述分析,我們可以歸納出以下確保Arduino專案穩定性的最佳實踐:

1. 選擇優質的硬體

  • 盡量選擇官方或認證的Arduino開發板。
  • 使用品質可靠的元件和配件(如傳感器、執行器等)。
  • 確保連接線材(如USB線、杜邦線)品質良好。

2. 提供穩定的電源

  • 使用符合規格的電源適配器,確保電壓穩定。
  • 對於需要驅動大功率元件的項目,考慮使用獨立電源。
  • 添加濾波電容以減少電源干擾。
  • 考慮使用外部電池作為備用電源,確保系統持續運作。

3. 合理的電路設計

  • 遵循良好的電路設計原則,避免短路和過載風險。
  • 為敏感元件添加適當的保護電路。
  • 使用光耦或繼電器等方式隔離高低壓電路。
  • 注意信號線的佈局,避免干擾。

4. 良好的程式設計實踐

  • 避免長時間的阻塞性操作,考慮使用非阻塞編程技術。
  • 合理使用中斷和定時器功能,但避免過度使用。
  • 定期重置watchdog計時器,防止系統卡死。
  • 實現錯誤檢測和恢復機制。
  • 優化記憶體使用,避免記憶體洩漏。

5. 正確的安裝和維護

  • 確保所有連接牢固,避免松動和接觸不良。
  • 將Arduino安裝在適當的外殼中,防止灰塵和濕氣。
  • 注意散熱,避免過熱。
  • 定期檢查系統運行狀態,及時發現和解決問題。

Arduino與其他微控制器平台的比較

為了全面理解Arduino的穩定性,我們可以將其與其他流行的微控制器平台進行比較:

Arduino vs. ESP32/ESP8266

  • 處理能力:ESP32/8266的處理能力和記憶體更強大,適合更複雜的任務。
  • 穩定性:Arduino在簡單應用中穩定性較好,ESP32在WiFi應用方面有時會出現連接不穩定的情況。
  • 功耗:Arduino在持續運行的低功耗應用中表現更穩定。

Arduino vs. Raspberry Pi

  • 系統複雜度:Raspberry Pi運行完整的作業系統,複雜度高於Arduino,可能面臨更多的系統級不穩定因素。
  • 可靠性:Arduino的簡單架構在特定應用中反而提供了更高的可靠性,不容易受到軟體崩潰的影響。
  • 啟動時間:Arduino幾乎瞬間啟動,而Raspberry Pi需要較長的啟動時間。

Arduino vs. STM32

  • 效能:STM32通常提供更高的性能和更豐富的外設。
  • 開發難度:Arduino的簡易性使得初學者較少遇到穩定性問題,而STM32對初學者有較高的學習門檻。
  • 穩定性:在適當編程的情況下,兩者都能提供非常穩定的性能。

結論

Arduino「不穩定」的迷思主要源於使用者在硬體選擇、電源供應、程式設計、接線方式和環境條件等方面的不當處理,而非平台本身的固有問題。

事實上,Arduino作為一個開源電子原型平台,在其設計範圍內具有相當的穩定性和可靠性。通過選擇優質的硬體、提供穩定的電源、採用合理的電路設計、遵循良好的程式設計實踐以及確保正確的安裝和維護,Arduino完全能夠勝任從簡單的家庭項目到專業的工業應用等各種場景。

Arduino的真正價值在於其簡單易用、社群支持強大且成本低廉,這使得它成為電子愛好者、設計師、教育工作者和專業開發者的理想選擇。理解Arduino的特性和限制,在適當的場景中正確使用,它將是一個極其可靠的工具。

進一步學習資源


本文發布於2025年4月23日,Arduino平台可能會有後續更新和變化。


本文最初發布於 HackMD @BASHCAT。

Arduino IDE -Sulime Text 2

除了Arduino原廠IDE以外,現在還有另外一個選擇Sulime Text 2
 

1.下載Sulime Text
Sulime Text 官方載點
Sulime Text 2
http://www.sublimetext.com/2
Sulime Text 3
http://www.sublimetext.com/3

2.安裝package control
Sulime Text 有相當多的package可以使用,而package control就可以方便的管理這些package。

https://packagecontrol.io/installation
開啟Sulime Text 後 使用 Ctrl+` 在下方命令列入

Sulime Text 2輸入下串
import urllib2,os,hashlib; h = 'eb2297e1a458f27d836c04bb0cbaf282' + 'd0e7a3098092775ccb37ca9d6b2e4b7d'; pf = 'Package Control.sublime-package'; ipp = sublime.installed_packages_path(); os.makedirs( ipp ) if not os.path.exists(ipp) else None; urllib2.install_opener( urllib2.build_opener( urllib2.ProxyHandler()) ); by = urllib2.urlopen( 'http://packagecontrol.io/' + pf.replace(' ', '%20')).read(); dh = hashlib.sha256(by).hexdigest(); open( os.path.join( ipp, pf), 'wb' ).write(by) if dh == h else None; print('Error validating download (got %s instead of %s), please try manual install' % (dh, h) if dh != h else 'Please restart Sublime Text to finish installation') 

Sulime Text 3輸入下串 
import urllib.request,os,hashlib; h = 'eb2297e1a458f27d836c04bb0cbaf282' + 'd0e7a3098092775ccb37ca9d6b2e4b7d'; pf = 'Package Control.sublime-package'; ipp = sublime.installed_packages_path(); urllib.request.install_opener( urllib.request.build_opener( urllib.request.ProxyHandler()) ); by = urllib.request.urlopen( 'http://packagecontrol.io/' + pf.replace(' ', '%20')).read(); dh = hashlib.sha256(by).hexdigest(); print('Error validating download (got %s instead of %s), please try manual install' % (dh, h)) if dh != h else open(os.path.join( ipp, pf), 'wb' ).write(by) 

完成後 重新開啟Sulime Text 即完成安裝package control

3.安裝Arduino like-IDE
 a.開啟Sulime Text 按下組鍵Ctrl+Shift+P
 b.輸入install package 並稍做等待
 c.輸入Arduino like-IDE 並點選安裝


4.指定 Application 
安裝好Arduino like IDE 後
在工作列最左方會出現 Arduino 的選項
選擇 Perferences->Selest Arduino Application Folder
選擇Arduion 官方 IDE 安裝位置

接下來就可以正常使用拉~~

ATTINY85

 

 

 

轉http://www.instructables.com/id/Digispark-DIY-The-smallest-USB-Arduino/

Digispark is an ATtiny85 based microcontroller development board come with USB interface. Coding is similar to Arduino, and it use the familiar Arduino IDE for development.

Digispark is copyrighted by Digistump LLC (digistump.com) and the full license is here: http://digistump.com/wiki/digispark/policy

Specification:
Support for the Arduino IDE 1.0+ (OSX/Win/Linux)
Power via USB or External Source - 5v or 7-35v (automatic selection)
On-board 500ma 5V Regulator
Built-in USB (and serial debugging)
6 I/O Pins (2 are used for USB only if your program actively communicates over USB, otherwise you can use all 6 even if you are programming via USB)
8k Flash Memory (about 6k after bootloader)
I2C and SPI (vis USI)
PWM on 3 pins (more possible with Software PWM)
ADC on 4 pins
Power LED and Test/Status LED (on Pin0)

 

Step 1: Prerequisite

AVRISP MKII In-System Programmer
ATTINY85 Microcontroller
2 x 3.6V zener diode
2 x 68 ohm resistor
1 x 1.5K resistor
USB cable (get from broken mouse or keyboard)
Some wires



Step 2: Burning bootloader to ATTINY85

Like Arduino, Digispark require a bootloader to be running on ATTINY85. The bootloader will occupied 2KB flash memory.

Download bootloader
1. Download Micronucleus bootloader for ATTINY85
2. Extract the file (micronucleus-t85-master.zip) to any folder
3. You can find the bootloader file at micronucleus-t85-master\firmware\releases folder
4. Use micronucleus-1.06.hex for the bootloader

Burning bootloader to ATTINY85
You must use the correct fuses bit for the bootloader

Extended: 0xFE
High: 0xDD
Low: 0xE1

Note: The above fuse bit will not enable reset as I/O, so you can have only 5 I/O instead of 6 I/O. I'm still try to figure out on how to set it to 6 I/O

I'm using AVRISP MKII In System Programmer and AVR Studio software for burning bootloader.


Step 3: Installing Digispark USB driver

Digispark use USB to communicate with computer, so your computer must install Digispark USB driver
1. Download Arduino for Digispark which come with USB driver
2. Extract the file (DigisparkArduino-Win32-1.0.4-March29.zip)  to any folder
3. Execute DigisparkArduino-Win32\DigisparkWindowsDriver\InstallDriver.exe to start installing the USB driver

Arduino DC motor - DIY自走車(1)前進後退



從網路上購入自走車車架,

使用壓克力板組成,
搭上3VDC的直流馬達與減速模組構成,
MotorShield 模組相關資料,
http://playground.arduino.cc/Main/AdafruitMotorShield
可同時推動2組步進馬達 或 四組DC馬達正反轉,
Arduino DC motor - DIY自走車(1)前進後退(圖 1)
亦可推動,兩組伺服馬達,伺服馬達與步進馬達似乎沒辦法同時運作,
在這模組板上 ,
下連結為 模組板library,
https://github.com/adafruit/Adafruit-Motor-Shield-library

Arduino DC motor - DIY自走車(1)前進後退(圖 2) 

Arduino DC motor - DIY自走車(1)前進後退(圖 3)
組裝完成正面
Arduino DC motor - DIY自走車(1)前進後退(圖 4)
組裝完成背面
 以下為簡單程式碼
動作為前進1000ms,後退1000ms,然後停止1000ms,
然後持續循環,
有遭遇到電流不夠帶動3VDC馬達,因原先使用1.5V充電電池*4,
雖然Arduino可以運作,但在帶動馬達時發出滋滋的聲響,
後改用電源供應器推動,馬達才有動作,馬達的瞬間電流大約落在1A。



Arduino DC motor - DIY自走車(1)前進後退(圖 5)



後續還需要整合伺服馬達與超音波模組,
完成障礙辨識,
再掛上通訊模組,目前考慮使用wifi或是bluetooth模組,
但如果使用wifi模組,電池電量將是一個問題。
完成通訊之外還要加上cmd才能使用手機或者是電腦去控制自走車,
並請自走車回報狀況。

後續再將進度補上。

燒錄 Arduino mini Pro 燒錄

    Arduino Mini Pro 算是Uno的縮小板,使用上也沒太大變化,可以注意的是,市面上可能分為有幾個版本16Mhz、8Mhz就連電壓也會有5V、3.3V,但我這邊使用的是5V-16Mhz,工作於3.3V的時候也是可以正常工作,
 燒錄 Arduino mini Pro 燒錄(圖 1)燒錄 Arduino mini Pro 燒錄(圖 2)
 該有的腳位也都拉出來了,這塊mini上面還有A4~A7,網路上有些比較精簡的版本就沒有這4pin。
 燒錄 Arduino mini Pro 燒錄(圖 3) 

接下來介紹燒錄方法,需要準備一組USB to TTL轉換,有RX/TX就可以了, 將
mini RX->TTL TX
mini TX->TTL RX
mini VCC->TTL VCC
mini GND->TTL GND
燒錄 Arduino mini Pro 燒錄(圖 4)
接妥後,開啟自己的CODE,這邊用範例Blink測試,按下上傳後,等出現Uploading...這時候,立刻馬上,按下mini的 Reset,成功後就會將CODE Upload上去了。

燒錄 Arduino mini Pro 燒錄(圖 5)
 手腳太慢就會出現下面這畫面
燒錄 Arduino mini Pro 燒錄(圖 6)
 網路上還有領外一個使用Uno對燒的方式,也是相同原理,只是Reset是接Uno板子上的Reset這樣就不用手去按他了。






趴趴走 RV-3 DIY 改造

最近入手一台掃地機器人,因為很便宜的關係,使用起來機器人也非常癡呆,沒關係,有他的軀體就好,他的靈魂就自己用Arduino來改。
電路主板部份,也只有這部份了,整體看來不複雜,加上機器人的模式判斷與走路的癡呆樣子,MCU沒做太多的判斷選擇。

電路板背面。

趴趴走 RV-3 DIY 改造(圖 3)趴趴走 RV-3 DIY 改造(圖 4)
整台主機,只有用一個碰裝Sensor,元件是使用光遮斷器做判斷,應該是為了耐久度,並且用彈簧帶開光遮斷。

趴趴走 RV-3 DIY 改造(圖 5)
 需要加強的地方:
 1.走路方式需要急救
 2.吸力不足,需改馬達與葉片

總共使用三個馬達:
1.側面的掃把
2.吸灰塵用
3.走路用
前兩個基本上不用在做判斷,所以我們要控制的只有走路用的。

預計使用arduino mini 搭配超音波 + 馬達控制板就可以完成了。
趴趴走 RV-3 DIY 改造(圖 6)

Arduino 課本可能沒教的事(1)

  相信有用過Arduino的人都會知道,在一個開發板上的IO腳,有分為"數位腳"、"類比輸入",也就是板子上場看到的數字0~13與A0~A5,在比較常見的教材中,往往很少提到A0~A5腳位的使用方式,正常都是說明,如何在上面接上一個可變電阻,在由可變電阻調整電壓進入類比輸入端,再有數位腳輸出對應的亮度,重點來了,因為這樣可能有些人認為A0~A5"只能"做類比的事情,但這些類比腳,亦可拿來當數位腳使用,可以從下圖看到,A0~A5於粉紅色區塊標示為14~19,代表其實該腳位要用於數位時,它的編號,而MEGA版本也是相同道理,數位腳由53之後轉為,A0=54、A1=55…以此類推,總而言之,這樣UNO版本腳位其十就很多,足以供一般專案使用。
Arduino 課本可能沒教的事(1)(圖 1)

SI4432 搭配Arduino

        前陣子從購入兩片si4432模組,拿來搭配arduino使用,傳輸效果算是相當不錯,訊號從1樓可以打到5F,由於已經作成模組化了,使用上也不會太困難,但需要同時接收/發射,所提供的範例是不夠用的。

        Arduino&si4432連接方法,由於Si4432工作電壓用的是3.3V,所以建議以CD4050做電位轉換, 但我測試的時候,是使用arduino mini pro ,工作電壓直接接上3.3V所以沒有在做轉換,要使用5V的MCU則需要加,如果不怕燒掉的話也是可以直接上。

SI4432 搭配Arduino(圖 1)



SI4432 搭配Arduino(圖 2)
arduino mini pro
SI4432 搭配Arduino(圖 3)
si4432


library則是使用RF22
http://www.airspayce.com/mikem/arduino/RF22/
檔案中包含了基本的範例,可以直接使用。

SI4432 搭配Arduino(圖 4)





Arduino 交流電功率調整

最近在測試110V功率調整,透過外部觸發與內部65微秒觸發中斷來切割弦波的波型,使其達到功率調整的方式。


 參考連結:

http://forum.arduino.cc/index.php?PHPSESSID=1mlmloei1vpish99327r5gfol0&topic=22512.15



















震動開關推動LED


當一端低於水準位置傾斜,開關尋通,類比口電壓值為5V左右(數位二進位表示為1023),點亮led燈。將A0所讀到數值由0~1023轉為0~255,用意是為了提供給LED推動實縮需要的PWM,另外這不只可以使用在震動開關上面,只要是有類比變化的接可接在Arduino類比端輸入,一樣能推動。

震動開關推動LED(圖 1)




const int analogInPin = A0;  // Analog input pin that the potentiometer is attached to
const int analogOutPin = 9; // Analog output pin that the LED is attached to

int sensorValue = 0;        // value read from the pot
int outputValue = 0;        // value output to the PWM (analog out)

void setup() {

  Serial.begin(9600);
}

void loop() {

  sensorValue = analogRead(analogInPin);          

  outputValue = map(sensorValue, 0, 1023, 0, 255);

  analogWrite(analogOutPin, outputValue);        

  Serial.print("sensor = " );                    
  Serial.print(sensorValue);    
  Serial.print("\t output = ");    
  Serial.println(outputValue);

  delay(2);                  
}

執行結果如下
震動開關推動LED(圖 2)

Arduino 整串封包接收處理

由於Serial所Read 的值都只有一個byte,

要讀取整筆封包還要再自己寫,

所以稍微寫了一下,將"#"當作起始字,"$"當作結尾字,

並且將之間的資料存於Databuf中,

有需要的時候再Send "!" 會將資料丟出來。


Arduino 整串封包接收處理(圖 1)

Arduino 整串封包接收處理(圖 2)



以下是code:


1:  char Databuf[100];     // 用來儲存收進來的 data byte  
2:  int addr=1;  
3:  char buf;  
4:  int RW=0;  
5:  void setup() {  
6:   // 開啟 Serial port, 通訊速率為 9600 bps  
7:   Serial.begin(9600);  
8:   Serial.println("Hello Oliver"); Serial.println("http://arduino-oliver.blogspot.com/");  
9:  }  
10:  void loop() {   
11:   // 檢查是否有資料可供讀取#@  
12:   if (Serial.available() > 0) {  
13:    buf = Serial.read();  
14:    switch (buf)   
15:    {  
16:    case '#':  
17:     Serial.println("#");  
18:     RW=1;  
19:     break;  
20:    case '!':  
21:     Serial.println("!");  
22:     while(addr>0)  
23:     {  
24:     addr--;  
25:     Serial.print(Databuf[addr],HEX);  
26:     }  
27:     break;  
28:    case '$':  
29:     Serial.println("$");  
30:     RW=0;  
31:     break;  
32:    default:  
33:    if(RW=1)  
34:    {    
35:    Databuf[addr]=buf;  
36:    Serial.print(Databuf[addr],HEX);  
37:    addr++;  
38:    }  
39:   }  
40:   }  
41:  }  

ReadAnalogVoltage 類比輸入 example




簡單的範例,做輸入電壓的偵測


void setup() {

 //設定serial
  Serial.begin(9600);
}

void loop()
 {

 // 讀取A0的電壓
  int sensorValue = analogRead(A0);
  float voltage = sensorValue * (5.0 / 1023.0);
 //透過監看COM數值傳出
  Serial.print("voltage : ");
  Serial.println(voltage);
 //一秒執行一次
  delay(1000);
}

  voltage將會收到0~1023的數值,A0接口可以接上VR,就可以測試這項功能正不正常了。


  int sensorValue = analogRead(AX)
  將取到的值先丟到 Value

執行畫面如下:

ReadAnalogVoltage 類比輸入 example(圖 1)

第一次如何使用Arduino

Arduino在台灣已經有一陣子了,對於有再接觸程式設計的人應該都不陌生,然而對於不是程式設計,或者是不是電子電機相關科目的,也不會太難以上手。

由於Arduino本身官方由提供許多librarie,與網路上有豐富的資源,使其上手的門檻相對的降低許多,新手要取得硬體設備也不是什麼難事,在各大網拍、較大的電子零件通路都能看到它的身影。至於出版物台灣也有相當多的譯本,但是小弟覺得是不用買那些書,在網路上也能找到相當多的sample code。

現在就開始進入本篇主題:

1.下載Arduino IDE

Arduino官方網站下載點 http://arduino.cc/en/Main/Software

第一次如何使用Arduino(圖 1)
可以看到上圖有兩個方框,如果你手上的開發板是屬於Yun或者是Due請下載1.5.6版本,才有支援這兩款板子的燒錄喔,而其中差別在往後再跟大家介紹。

選擇Windows Installer就可以了ZIP file是屬於免安裝版本,但使用上可能會較為不方便,所以這邊建議Windows Installer安裝就好了。

下載好後執行,也沒有太多需要注意的,Next...Next...Next就好了。

2.安裝Arduino驅動程式

如果是照著上一步,安裝Arduino,在開發板接上時,於電腦設備管理中會看到無法辨識的裝置,選擇它
第一次如何使用Arduino(圖 2)
點選瀏覽電腦上的驅動程式軟體

第一次如何使用Arduino(圖 3)

如果Arduino使用預設安裝可以在安裝路進下的Drivers中找到驅動程式

C:\Program Files (x86)\Arduino\drivers

第一次如何使用Arduino(圖 4)

安裝好了,本文是使用Mega 2560。

第一次如何使用Arduino(圖 5)


3.開啟桌面上Arduino

就可以看到下圖這個畫面了,工作列因為我使用1.5.4它有支援繁體中文。
而工作列底下的圖案分別為驗證、下載、開新檔案、開啟、存檔與最左邊的監看。

第一次如何使用Arduino(圖 6) 

 4.如何燒錄程式

我們先來點一個簡單的範例,選取 檔案>範例>Basics>Blink


會出現以下的Code

/*
  Blink
  Turns on an LED on for one second, then off for one second, repeatedly.

  This example code is in the public domain.
 */


// Pin 13 has an LED connected on most Arduino boards.
// give it a name:

int led = 13;

// the setup routine runs once when you press reset:

void setup() {               
  // initialize the digital pin as an output.
  pinMode(led, OUTPUT);    
}

// the loop routine runs over and over again forever:
void loop() {
  digitalWrite(led, HIGH);   // turn the LED on (HIGH is the voltage level)
  delay(1000);               // wait for a second
  digitalWrite(led, LOW);    // turn the LED off by making the voltage LOW
  delay(1000);               // wait for a second
}

接著按下驗證

會出線編譯完成的訊息,與總共使用了多少空間和最大的空間。
接著來選擇"工具",里面可以找到板子與Port這兩個選項,就對應你所使用的板子型號,與於系統中所使用的Port位置。


按下箭頭的圖案上傳

第一次如何使用Arduino(圖 10)

到這邊,就正式的把第一個程式上傳到開發板子上了,這時候板子不用接上任何的線,
可以看到板子上有LED在不停的閃爍。
第一次如何使用Arduino(圖 12)
 

這樣就完成了第一次燒入Arduino的完整動做了。
本篇是針對整個燒入流程做介紹,Code的部分往後再做說明。


如果使用上有遇到其他問題,也歡迎在這邊提問唷,對於本篇文章有錯誤的地方也請大家指教,小弟會立刻更正。

Arduino 介紹 [Wiki]

關於

Arduino 介紹 [Wiki](圖 1)
Arduino 介紹 [Wiki](圖 2)
Arduino
Arduino可以使用 Arduino 語言與 Macromedia Flash、Processing、Max/MSP、Pure Data和SuperCollider等軟體,結合電子元件,例如開關或感測器或其他控制器件、LED、步進馬達或其他輸出裝置,作出互動作品。 Arduino也可以獨立運作成為一個可以跟軟體溝通的介面。

特色

  • 基於創用CC開放原始碼的電路圖設計。
  • 免費下載,也可依需求自己修改,但需遵照姓名標示。您必須按照作者或授權人所指定的方式,表彰其姓名。
  • 依相同方式分享,若您改變或轉變著作,當散布該衍生著作時,您需採用與本著作相同或類似的授權條款。
  • Arduino 可使用 ICSP 線上燒入器,將 Bootloader 燒入新的 IC 晶片。
  • 可依據Arduino官方網站,取得硬體的設計檔,加以調整電路板及元件,以符合自己實際設計的需求。
  • 可簡單地與感測器,各式各樣的電子元件連接,如 紅外線、超音波、熱敏電阻、光敏電阻、伺服馬達…等。
  • 支援多樣的互動程式,如 Adobe Flash, Max/MSP, VVVV, Pure Data, C, Processing… 等。
  • 使用低價格的微處理控制器 (Atmel AVR) (ATMEGA 8,168,328等)。
  • USB 介面,不需外接電源。另外有提供直流(DC)電源輸入。

核心開發團隊

Arduino的核心開發團隊成員包括:Massimo Banzi,David Cuartielles,Tom Igoe,Gianluca Martino,David Mellis 和 Nicholas Zambetti。
據說 Massimo Banzi 之前是義大利 Ivrea 一家高科技設計學校的老師。他的學生們經常抱怨找不到便宜好用的微控制器。2005年冬天, Massimo Banzi 跟David Cuartielles 討論了這個問題。David Cuartielles 是一個西班牙籍晶片工程師,當時在這所學校做訪問學者。兩人決定設計自己的電路板,並引入了Banzi 的學生 David Mellis 為電路板設計編程語言。兩天以後,David Mellis 就寫出了程式碼。又過了三天,電路板就完工了。這塊電路板被命名為Arduino。幾乎任何人,即使不懂電腦編程,也能用 Arduino 做出很酷的東西,比如對感測器作出回應,閃爍燈光,還能控制馬達。隨後Banzi,Cuartielles,和Mellis把設計圖放到了網上。保持設計 的開放源碼理念,因為版權法可以監管開源軟體,卻很難用在硬體上,他們決定採用創用CC許可。創用CC是為保護開放版權行為而出現的類似GPL的一種許可 (license)。在創用CC許可下,任何人都被允許生產電路板的複製品,還能重新設計,甚至銷售原設計的複製品。你不需要付版稅,甚至不用取得 Arduino 團隊的許可。然而,如果你重新發佈了引用設計,你必須說明原始Arduino 團隊的貢獻。如果你調整或改動了電路板,你的最新設計必須使用相同或類似的創用CC許可,以保證新版本的Arduino電路板也會一樣的自由和開放。唯一 被保留的只有Arduino這個名字。它被註冊成了商標。如果有人想用這個名字賣電路板,那他們可能必須付一點商標費用給 Arduino 的核心開發團隊成員。

8GB 的 RK3588 能跑多聰明的 LLM?ROCK 5C 用 NPU 實測 5 個模型

先講結論,省得你滑到最後: 在一片 8GB 的 RK3588 板子上,用 NPU 跑得最聰明的是 Qwen3-4B-Instruct-2507,我出的 7 題全對,但每秒只吐 3.7 個 token。 想要順一點的對話體驗,Qwen3.5-2B 的 8.4 tok/s 是比...