ESP32 雙核心 RFID 讀取與 MQTT 發佈
這個程式將實現:
- Core 0 (PRO_CPU):專門負責 MQTT 連線管理和數據發佈。
- Core 1 (APP_CPU):專門負責 MFRC522 RFID 卡片讀取。
1. 硬體連接
與單核心版本相同:
請再次確認:您的 ESP32 開發板的 GPIO 腳位定義。MFRC522 VCC 務必連接到 3.3V。
2. 軟體設定 (Arduino IDE)
確保您已經安裝了以下函式庫:
- ESP32 Board 支援
- MFRC522 函式庫 (
MFRC522 by Udo Klein或其他相容版本) - PubSubClient 函式庫 (
PubSubClient by Nick O'Leary)
3. 程式碼
Arduino
#include <SPI.h> // MFRC522 依賴的 SPI 函式庫
#include <MFRC522.h> // MFRC522 RFID 讀卡器函式庫
#include <WiFi.h> // ESP32 Wi-Fi 函式庫
#include <PubSubClient.h> // MQTT 客戶端函式庫
#include <Arduino.h> // 確保 FreeRTOS 相關函數可用
/********************************** 常數定義 **********************************/
// Wi-Fi 網路設定
const char* ssid = "YOUR_WIFI_SSID"; // 替換成您的 Wi-Fi 名稱
const char* password = "YOUR_WIFI_PASSWORD"; // 替換成您的 Wi-Fi 密碼
// MFRC522 模組設定 (SPI 連接)
#define RST_PIN 22 // MFRC522 重置腳位 (RST/Reset)
#define SS_PIN 21 // MFRC522 選擇腳位 (SDA/SS/CS)
// MQTT Broker 設定
const char* mqtt_server = "mqttgo.io"; // MQTT Broker 位址
const int mqtt_port = 1883; // MQTT 端口
const char* mqtt_client_id = "ESP32_RFID_DualCore_Client"; // MQTT 客戶端 ID
const char* mqtt_topic = "alex9ufo/rfidUID"; // MQTT 發佈主題
// 任務堆疊大小 (根據任務複雜度調整)
#define TASK_STACK_SIZE 4096
// 發佈頻率控制 (避免重複發送相同的 UID)
const long publishInterval = 2000; // 2000 毫秒 = 2 秒
/********************************** 全域變數與物件 **********************************/
// MFRC522 讀卡器物件
MFRC522 mfrc522(SS_PIN, RST_PIN);
// WiFiClient 物件
WiFiClient espClient;
// PubSubClient 物件
PubSubClient mqttClient(espClient); // 注意這裡改名為 mqttClient 以區別
// 任務句柄
TaskHandle_t TaskRFIDRead = NULL;
TaskHandle_t TaskMQTTPublish = NULL;
// 使用 FreeRTOS 的 Queue 來在兩個任務之間傳遞 UID
// 佇列可儲存一個 String 物件,長度為 1
QueueHandle_t rfidUIDQueue;
// 保護共享資源 (lastUID 和 lastPublishTime) 的互斥鎖
SemaphoreHandle_t xSemaphore;
String lastUID = "";
unsigned long lastPublishTime = 0;
/********************************** 輔助函數 **********************************/
// 連接到 Wi-Fi 網路的函數
void setup_wifi() {
delay(10);
Serial.println();
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
int retries = 0;
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
retries++;
if (retries > 20) { // 最多等待 10 秒
Serial.println("\nWiFi connection failed! Retrying...");
retries = 0;
WiFi.disconnect();
WiFi.begin(ssid, password);
}
}
Serial.println("");
Serial.println("WiFi connected");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}
// MQTT 訊息回調函數 (此程式主要發佈數據,接收功能可根據需求擴展)
void mqtt_callback(char* topic, byte* payload, unsigned int length) {
Serial.print("Message arrived [");
Serial.print(topic);
Serial.print("] ");
for (int i = 0; i < length; i++) {
Serial.print((char)payload[i]);
}
Serial.println();
}
// 連接到 MQTT Broker 的函數
void reconnect_mqtt() {
while (!mqttClient.connected()) {
Serial.print("Attempting MQTT connection...");
if (mqttClient.connect(mqtt_client_id)) {
Serial.println("connected");
// 如果需要訂閱主題,請在這裡添加
// mqttClient.subscribe("your/subscribe/topic");
} else {
Serial.print("failed, rc=");
Serial.print(mqttClient.state());
Serial.println(" try again in 5 seconds");
delay(5000); // 等待 5 秒後重試
}
}
}
// 將 UID 轉換為十六進位字串的函數
String uid_to_string(byte *buffer, byte bufferSize) {
String uidString = "";
for (byte i = 0; i < bufferSize; i++) {
if (buffer[i] < 0x10) { // 如果是個位數,前面補 0
uidString += "0";
}
uidString += String(buffer[i], HEX); // 轉換為十六進位
}
uidString.toUpperCase(); // 轉換為大寫
return uidString;
}
/********************************** 核心任務函數 **********************************/
// Core 1 (APP_CPU) 上的 RFID 讀取任務
void TaskRFIDReader(void *pvParameters) {
Serial.println("RFID Reader Task running on Core 1");
SPI.begin(); // 啟動 SPI 介面
mfrc522.PCD_Init(); // 初始化 MFRC522
Serial.println("MFRC522 initialized on Core 1.");
for (;;) { // 無限循環
// 檢查是否有新的 RFID 卡片感應到
if (mfrc522.PICC_IsNewCardPresent() && mfrc522.PICC_ReadCardSerial()) {
String currentUID = uid_to_string(mfrc522.uid.uidByte, mfrc522.uid.size);
Serial.print("[Core1] Card detected! UID: ");
Serial.println(currentUID);
// 嘗試獲取互斥鎖,保護共享變數
if (xSemaphoreTake(xSemaphore, (TickType_t)10) == pdTRUE) {
unsigned long currentMillis = millis();
// 檢查 UID 是否與上次的相同,並控制發佈頻率
if (currentUID != lastUID || (currentMillis - lastPublishTime >= publishInterval)) {
// 將 UID 放入佇列,讓 MQTT 任務處理
if (xQueueSend(rfidUIDQueue, ¤tUID, (TickType_t)10) == pdPASS) {
Serial.println("[Core1] UID sent to MQTT Queue.");
// 只有在成功發送給 MQTT 任務後才更新 lastUID 和 lastPublishTime
lastUID = currentUID;
lastPublishTime = currentMillis;
} else {
Serial.println("[Core1] Failed to send UID to MQTT Queue (queue full).");
}
} else {
Serial.println("[Core1] Same card, not sending (within interval).");
}
xSemaphoreGive(xSemaphore); // 釋放互斥鎖
} else {
Serial.println("[Core1] Failed to take semaphore.");
}
// 停止 PICC 和 PCD 以節省電力並防止重複讀取
mfrc522.PICC_HaltA();
mfrc522.PCD_StopCrypto1();
}
vTaskDelay(pdMS_TO_TICKS(50)); // 每次循環延遲 50ms,避免過度佔用 CPU
}
}
// Core 0 (PRO_CPU) 上的 MQTT 發佈任務
void TaskMQTTPublisher(void *pvParameters) {
Serial.println("MQTT Publisher Task running on Core 0");
mqttClient.setServer(mqtt_server, mqtt_port);
mqttClient.setCallback(mqtt_callback);
for (;;) { // 無限循環
if (!mqttClient.connected()) {
reconnect_mqtt();
}
mqttClient.loop(); // 處理 MQTT 數據包和保持連線
String uidToPublish;
// 嘗試從佇列接收 UID,等待 100ms
if (xQueueReceive(rfidUIDQueue, &uidToPublish, (TickType_t)100) == pdPASS) {
Serial.print("[Core0] Received UID from Queue: ");
Serial.println(uidToPublish);
// 發佈 UID 到 MQTT 主題
if (mqttClient.publish(mqtt_topic, uidToPublish.c_str())) {
Serial.println("[Core0] UID published successfully to MQTT!");
} else {
Serial.println("[Core0] Failed to publish UID to MQTT.");
// 如果發佈失敗,可以考慮將 UID 再次放入佇列或記錄下來以便後續重試
}
}
vTaskDelay(pdMS_TO_TICKS(10)); // 每次循環延遲 10ms,讓出 CPU 給其他任務
}
}
/********************************** Setup & Loop (主要執行緒) **********************************/
void setup() {
Serial.begin(115200);
delay(100);
// 創建互斥鎖
xSemaphore = xSemaphoreCreateMutex();
if (xSemaphore == NULL) {
Serial.println("Failed to create mutex semaphore.");
// 處理錯誤,可能重啟或其他錯誤指示
}
// 創建 RFID UID 佇列
rfidUIDQueue = xQueueCreate(1, sizeof(String)); // 佇列深度為 1,每次只存一個 UID
if (rfidUIDQueue == NULL) {
Serial.println("Failed to create RFID UID queue.");
// 處理錯誤
}
// 設定 Wi-Fi (通常在 Core 0 執行,但因為是 FreeRTOS,最好在任務啟動前完成)
setup_wifi();
// 創建 RFID 讀取任務,並將其固定在 Core 1 (APP_CPU)
xTaskCreatePinnedToCore(
TaskRFIDReader, // 任務函數
"RFID_Reader", // 任務名稱
TASK_STACK_SIZE, // 任務堆疊大小
NULL, // 任務參數
1, // 任務優先級 (0 最低,portTICK_RATE_MS 相關)
&TaskRFIDRead, // 任務句柄
1 // 運行在 Core 1
);
// 創建 MQTT 發佈任務,並將其固定在 Core 0 (PRO_CPU)
xTaskCreatePinnedToCore(
TaskMQTTPublisher, // 任務函數
"MQTT_Publisher", // 任務名稱
TASK_STACK_SIZE, // 任務堆疊大小
NULL, // 任務參數
1, // 任務優先級
&TaskMQTTPublish, // 任務句柄
0 // 運行在 Core 0
);
Serial.println("Tasks created. Entering main loop...");
}
void loop() {
// 主循環中不執行任何阻塞操作,僅讓 FreeRTOS 調度器運行
// 所有的 RFID 讀取和 MQTT 發佈都在各自的任務中進行
vTaskDelete(NULL); // 刪除主任務,釋放 Core 1 (預設主任務在 Core 1)
// 如果您希望主任務持續運行但無所事事,可以只放一個 vTaskDelay
}
4. 運行與測試
- 接線:按照硬體連接說明,將 MFRC522 模組連接到您的 ESP32。
- 修改程式碼:
- 將
YOUR_WIFI_SSID和YOUR_WIFI_PASSWORD替換為您的 Wi-Fi 網路名稱和密碼。 - 檢查
RST_PIN和SS_PIN的定義是否與您的 MFRC522 實際連接的 ESP32 腳位一致。
- 將
- 上傳程式碼:
- 在 Arduino IDE 中,選擇正確的 ESP32 開發板 (例如 "ESP32 Dev Module") 和正確的 COM 埠。
- 點擊 "上傳" 按鈕將程式碼燒錄到 ESP32。
- 開啟序列埠監控器:
- 燒錄完成後,打開 "工具" -> "序列埠監控器",設定鮑率為
115200。 - 您將會看到兩個核心啟動任務的訊息,Wi-Fi 連線進度,MQTT 連線狀態。
- 當感應到卡片時,您會看到類似
[Core1] Card detected! UID: ...和[Core0] Received UID from Queue: ...、[Core0] UID published successfully to MQTT!的訊息。
- 燒錄完成後,打開 "工具" -> "序列埠監控器",設定鮑率為
- MQTT 客戶端監控:
- 使用任何 MQTT 客戶端 (例如 MQTTX, Mosquitto Client, Node-RED 等) 連接到
mqttgo.ioBroker,並訂閱alex9ufo/rfidUID主題。 - 您將會看到 ESP32 發佈的卡片 UID。
- 使用任何 MQTT 客戶端 (例如 MQTTX, Mosquitto Client, Node-RED 等) 連接到
製作一個軟體模組 (或稱 模擬器 / 樁函式),來模擬 MFRC522 的讀取行為。這樣,您就可以在 Wokwi 上驗證您的 MQTT 發佈邏輯和雙核心任務分配是否正確,而不需要實際的 MFRC522 硬體。
這個軟體模組會:
- 替代 MFRC522 函式庫的呼叫:當您的程式碼嘗試讀取卡片時,它會返回預設的或模擬的 UID。
- 模擬卡片感應行為:您可以設定一個時間間隔,讓它像真的感應到卡片一樣"觸發" UID 的讀取和發送。
模擬 MFRC522 的軟體模組程式碼
這個版本會用一個簡單的計數器來模擬不同的 RFID 卡片 UID,並將其整合到之前的雙核心架構中。
#include <WiFi.h> // ESP32 Wi-Fi 函式庫
#include <PubSubClient.h> // MQTT 客戶端函式庫
#include <Arduino.h> // 確保 FreeRTOS 相關函數可用
// 注意:這裡不再需要 MFRC522 相關函式庫,因為我們將模擬其功能
/********************************** 常數定義 **********************************/
// Wi-Fi 網路設定
const char* ssid = "Wokwi-GUEST"; // 替換成您的 Wi-Fi 名稱 (例如 Wokwi 的 "Wokwi-GUEST")
const char* password = ""; // 替換成您的 Wi-Fi 密碼 (例如 Wokwi 的空字串 "")
// MQTT Broker 設定
const char* mqtt_server = "mqttgo.io"; // MQTT Broker 位址
const int mqtt_port = 1883; // MQTT 端口
const char* mqtt_client_id = "MQTTGO-9527419059"; // MQTT 客戶端 ID (建議獨一無二)
const char* mqtt_topic = "alex9ufo/rfidUID"; // MQTT 發佈主題
// 任務堆疊大小 (根據任務複雜度調整)
#define TASK_STACK_SIZE 4096
// 發佈頻率控制 (避免重複發送相同的 UID)
const long publishInterval = 2000; // 2000 毫秒 = 2 秒,每 2 秒最多發送一次數據
// MFRC522 模擬模組設定
const long RFID_SIM_INTERVAL = 3000; // 每 3 秒模擬一次 RFID 卡片感應
unsigned long lastRFIDSimTime = 0;
int simulatedUIDCounter = 0; // 用於生成不同的模擬 UID
/********************************** 全域變數與物件 **********************************/
// 創建 WiFiClient 物件,用於 Wi-Fi 連線
WiFiClient espClient;
// 創建 PubSubClient 物件,用於 MQTT 通訊
PubSubClient mqttClient(espClient);
// 任務句柄
TaskHandle_t TaskRFIDSimulate = NULL;
TaskHandle_t TaskMQTTPublish = NULL;
// 使用 FreeRTOS 的 Queue 來在兩個任務之間傳遞 UID
// 佇列可儲存一個 String 物件,長度為 1
QueueHandle_t rfidUIDQueue;
// 保護共享資源 (lastUID 和 lastPublishTime) 的互斥鎖
SemaphoreHandle_t xSemaphore;
String lastUID = "";
unsigned long lastPublishTime = 0;
/********************************** 輔助函數 **********************************/
// 連接到 Wi-Fi 網路的函數
void setup_wifi() {
delay(10);
Serial.println();
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
int retries = 0;
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
retries++;
if (retries > 20) { // 最多等待 10 秒
Serial.println("\nWiFi connection failed! Retrying...");
retries = 0;
WiFi.disconnect();
WiFi.begin(ssid, password);
}
}
Serial.println("");
Serial.println("WiFi connected");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}
// MQTT 訊息回調函數 (此程式主要發佈數據,接收功能可根據需求擴展)
void mqtt_callback(char* topic, byte* payload, unsigned int length) {
Serial.print("Message arrived [");
Serial.print(topic);
Serial.print("] ");
for (int i = 0; i < length; i++) {
Serial.print((char)payload[i]);
}
Serial.println();
}
// 連接到 MQTT Broker 的函數
void reconnect_mqtt() {
while (!mqttClient.connected()) {
Serial.print("Attempting MQTT connection...");
if (mqttClient.connect(mqtt_client_id)) {
Serial.println("connected");
// 如果需要訂閱主題,請在這裡添加
// mqttClient.subscribe("your/subscribe/topic");
} else {
Serial.print("failed, rc=");
Serial.print(mqttClient.state());
Serial.println(" try again in 5 seconds");
delay(5000); // 等待 5 秒後重試
}
}
}
/********************************** 核心任務函數 **********************************/
// Core 1 (APP_CPU) 上的 RFID 模擬讀取任務
void TaskRFIDSimulator(void *pvParameters) {
Serial.println("RFID Simulator Task running on Core 1");
for (;;) { // 無限循環
unsigned long currentMillis = millis();
// 每 RFID_SIM_INTERVAL 毫秒模擬一次卡片感應
if (currentMillis - lastRFIDSimTime >= RFID_SIM_INTERVAL) {
lastRFIDSimTime = currentMillis;
// 使用亂數產生 UID,格式為六個十六進位字符
char uidBuffer[7]; // 6 個字符 + 1 個 null 終止符
// 產生一個 24 位元(6 個十六進位字符)的亂數
uint32_t randomUID = random(0xFFFFFF + 1); // random(max) 會產生 0 到 max-1 的數
// 所以 max+1 才能包含 0xFFFFFF
sprintf(uidBuffer, "%06X", randomUID); // 格式化為六位大寫十六進位,不足補零
String simulatedUID = String(uidBuffer);
simulatedUID.toUpperCase(); // 轉換為大寫
Serial.print("[Core1] Simulated Card Detected! UID: ");
Serial.println(simulatedUID);
// 嘗試獲取互斥鎖,保護共享變數
if (xSemaphoreTake(xSemaphore, (TickType_t)10) == pdTRUE) {
// 檢查模擬 UID 是否與上次的相同,並控制發佈頻率 (這部分在模擬中可能較不重要,但保留以保持結構一致)
if (simulatedUID != lastUID || (currentMillis - lastPublishTime >= publishInterval)) {
// 將模擬 UID 放入佇列,讓 MQTT 任務處理
if (xQueueSend(rfidUIDQueue, &simulatedUID, (TickType_t)10) == pdPASS) {
Serial.println("[Core1] Simulated UID sent to MQTT Queue.");
// 只有在成功發送給 MQTT 任務後才更新 lastUID 和 lastPublishTime
lastUID = simulatedUID;
lastPublishTime = currentMillis;
} else {
Serial.println("[Core1] Failed to send simulated UID to MQTT Queue (queue full).");
}
} else {
Serial.println("[Core1] Same simulated card, not sending (within interval).");
}
xSemaphoreGive(xSemaphore); // 釋放互斥鎖
} else {
Serial.println("[Core1] Failed to take semaphore.");
}
}
vTaskDelay(pdMS_TO_TICKS(50)); // 每次循環延遲 50ms,避免過度佔用 CPU
}
}
// Core 0 (PRO_CPU) 上的 MQTT 發佈任務
void TaskMQTTPublisher(void *pvParameters) {
Serial.println("MQTT Publisher Task running on Core 0");
mqttClient.setServer(mqtt_server, mqtt_port);
mqttClient.setCallback(mqtt_callback);
for (;;) { // 無限循環
if (!mqttClient.connected()) {
reconnect_mqtt();
}
mqttClient.loop(); // 處理 MQTT 數據包和保持連線
String uidToPublish;
// 嘗試從佇列接收 UID,等待 100ms
if (xQueueReceive(rfidUIDQueue, &uidToPublish, (TickType_t)100) == pdPASS) {
Serial.print("[Core0] Received UID from Queue: ");
Serial.println(uidToPublish);
// 發佈 UID 到 MQTT 主題
if (mqttClient.publish(mqtt_topic, uidToPublish.c_str())) {
Serial.println("[Core0] UID published successfully to MQTT!");
} else {
Serial.println("[Core0] Failed to publish UID to MQTT.");
// 如果發佈失敗,可以考慮將 UID 再次放入佇列或記錄下來以便後續重試
}
}
vTaskDelay(pdMS_TO_TICKS(10)); // 每次循環延遲 10ms,讓出 CPU 給其他任務
}
}
/********************************** Setup & Loop (主要執行緒) **********************************/
void setup() {
Serial.begin(115200);
delay(100);
// 創建互斥鎖
xSemaphore = xSemaphoreCreateMutex();
if (xSemaphore == NULL) {
Serial.println("Failed to create mutex semaphore.");
// 處理錯誤,可能重啟或其他錯誤指示
}
// 創建 RFID UID 佇列
rfidUIDQueue = xQueueCreate(1, sizeof(String)); // 佇列深度為 1,每次只存一個 UID
if (rfidUIDQueue == NULL) {
Serial.println("Failed to create RFID UID queue.");
// 處理錯誤
}
// 設定 Wi-Fi
setup_wifi();
// 創建 RFID 模擬讀取任務,並將其固定在 Core 1 (APP_CPU)
xTaskCreatePinnedToCore(
TaskRFIDSimulator, // 任務函數
"RFID_Simulator", // 任務名稱
TASK_STACK_SIZE, // 任務堆疊大小
NULL, // 任務參數
1, // 任務優先級
&TaskRFIDSimulate, // 任務句柄
1 // 運行在 Core 1
);
// 創建 MQTT 發佈任務,並將其固定在 Core 0 (PRO_CPU)
xTaskCreatePinnedToCore(
TaskMQTTPublisher, // 任務函數
"MQTT_Publisher", // 任務名稱
TASK_STACK_SIZE, // 任務堆疊大小
NULL, // 任務參數
1, // 任務優先級
&TaskMQTTPublish, // 任務句柄
0 // 運行在 Core 0
);
Serial.println("Tasks created. Entering main loop...");
}
void loop() {
// 主循環中不執行任何阻塞操作,僅讓 FreeRTOS 調度器運行
// 所有的 RFID 模擬讀取和 MQTT 發佈都在各自的任務中進行
vTaskDelete(NULL); // 刪除主任務,釋放 Core 1 (預設主任務在 Core 1)
}


沒有留言:
張貼留言