2025年11月7日 星期五

防衝突迴圈 (Anti-collision Loop)

 防衝突迴圈 (Anti-collision Loop) 

MIFARE ISO/IEC 14443 相關文件,防衝突迴圈 (Anti-collision Loop) 是 MIFARE 卡片啟動中至關重要的一環。它解決了多張卡片同時出現在讀卡器 (PCD) 範圍內時的數據衝突問題,並負責唯一識別每張卡片的 UID (Unique Identifier)

我將分為兩部分來說明:首先解釋 ISO/IEC 14443-A 防衝突機制,然後提供一個使用 Python Tkinter 模擬該流程的程式碼架構。


第一部分:MIFARE 卡片的 ISO/IEC 14443-A 防衝突機制說明

MIFARE 卡片遵循 ISO/IEC 14443 Type A 標準,其防衝突程序是基於 位元衝撞檢測 (Bit Collision Detection) 的機制,通常分為 1 至 3 個層級 (Cascade Levels),取決於卡片的 UID 長度(4 bytes, 7 bytes 或 10 bytes)。MIFARE 1K 採用 4-byte NUID 或 7-byte UID。

1. 基本原理:Cascade Levels (級聯層級)

  • UID 的結構: MIFARE 卡片的 UID 由多個部分組成,包括:

    • CT (Cascade Tag): 指示是否還有更多的 UID 部分需要獲取。

    • NUID/UID 部分: 真正的唯一識別碼數據。

  • 分段選擇: 如果卡片的 UID 超過 4 個位元組(例如 7 位元組),則需要使用多個層級 (Cascade Level) 來獲取完整的 UID。每個層級都會重複執行一次防衝突迴圈。

2. 防衝突迴圈 (Anti-collision Loop) 步驟

以下是單一級聯層級 (Level) 內發生的流程:

步驟動作主體傳輸內容 (命令/響應)目的
1. 請求響應PCD -> PICCCL1/CL2/CL3 命令PCD 發送特定級聯層級的防衝突命令。
2. 卡片響應PCD <- PICCNUID/UID 數據 + BCC所有在場卡片同時回覆 UID 的一個區段。若多卡在場,則發生位元衝突 (Collision)。
3. 衝突檢測PCD(內部操作)PCD 檢測到響應中的位元衝撞點。
4. 分組處理PCD -> PICCSELECT 命令 + 已解析的 UID 前綴PCD 根據衝突點,發送一個帶有 UID 部分前綴的 SELECT 命令,要求 UID 匹配該前綴的卡片繼續響應,其他卡片進入靜默。
5. 迴圈迭代PCD & PICC(重複步驟 2-4)如果仍有衝突,重複 SELECT 和響應,直到只剩下一張卡片響應完整的 4 個位元組。
6. 級聯判斷PCD <- PICCCT 標誌當一個層級的 4 個位元組 UID 被成功獲取後,PCD 檢查 UID 中的 CT 標誌。如果 CT 存在,則進入下一個級聯層級 (Level 2)。
7. 選擇卡片PCD -> PICC最終 SELECT 命令當完整的 UID(例如 7 bytes)獲取完畢後,PCD 發送最終 SELECT 命令。卡片回覆 SAK,表示卡片被唯一選定。

第二部分:Python Tkinter 防衝突迴圈模擬

以下是使用 tkinter 建立 GUI 來視覺化單一級聯層級 (Level) 內,讀卡器如何透過不斷發送帶有解析後 UID 前綴的 SELECT 命令來解決衝突的過程。










import tkinter as tk

from tkinter import ttk

import random


class AntiCollisionSimulator:

    def __init__(self, master):

        self.master = master

        master.title("MIFARE 防衝突迴圈 (ISO/IEC 14443-A Tkinter 模擬)")


        # 模擬在場的卡片 UID (簡化為 4 byte NUID)

        self.card_uids = ["04F1AABB", "04C2CCDD", "04D3EEFF"] 

        self.present_cards = list(self.card_uids) # 當前響應的卡片

        self.resolved_uid_part = ""               # 已成功解析的 UID 前綴


        # 狀態變數

        self.status_text = tk.StringVar()

        self.log_text = tk.StringVar()

        self.collision_status = tk.StringVar()

        self.selected_card = tk.StringVar()


        # 初始化介面

        self.create_widgets()

        self.reset_simulator()


    def create_widgets(self):

        # 標題與在場卡片

        ttk.Label(self.master, text="模擬環境:多卡衝突解決 (單一級聯)", font=('Arial', 12, 'bold')).grid(row=0, column=0, columnspan=2, pady=5)

        ttk.Label(self.master, text=f"在場卡片 UIDs (簡化): {', '.join(self.card_uids)}", foreground='blue').grid(row=1, column=0, columnspan=2, pady=5)


        # 狀態顯示

        status_frame = ttk.LabelFrame(self.master, text="迴圈狀態")

        status_frame.grid(row=2, column=0, columnspan=2, padx=10, pady=5, sticky="ew")

        ttk.Label(status_frame, textvariable=self.status_text, wraplength=450, justify=tk.LEFT, font=('Arial', 10)).grid(row=0, column=0, padx=5, pady=5, sticky='w')

        

        # 衝突與解析結果

        ttk.Label(self.master, text="衝突狀態:", font=('Arial', 10, 'bold')).grid(row=3, column=0, sticky='w', padx=10)

        self.collision_label = ttk.Label(self.master, textvariable=self.collision_status, foreground='red', font=('Arial', 10, 'bold'))

        self.collision_label.grid(row=3, column=1, sticky='w')

        

        ttk.Label(self.master, text="已解析UID前綴:", font=('Arial', 10, 'bold')).grid(row=4, column=0, sticky='w', padx=10)

        self.resolved_label = ttk.Label(self.master, textvariable=self.selected_card, foreground='darkgreen', font=('Courier', 10, 'bold'))

        self.resolved_label.grid(row=4, column=1, sticky='w')


        # 操作按鈕

        self.next_button = ttk.Button(self.master, text="執行下一輪 SELECT", command=self.perform_select_step)

        self.next_button.grid(row=5, column=0, pady=10, padx=10, sticky='e')

        

        self.reset_button = ttk.Button(self.master, text="重置流程", command=self.reset_simulator)

        self.reset_button.grid(row=5, column=1, pady=10, padx=10, sticky='w')

        

    def reset_simulator(self):

        self.present_cards = list(self.card_uids)

        self.resolved_uid_part = ""

        self.status_text.set("流程重置。PCD 已發送防衝突命令 (CL1)。等待卡片響應。")

        self.collision_status.set("未檢測衝突")

        self.selected_card.set("N/A")

        self.next_button.config(state=tk.NORMAL, text="開始防衝突迴圈")


    def perform_select_step(self):

        if len(self.present_cards) == 1 and len(self.resolved_uid_part) == 8: # 4 bytes * 2 hex chars/byte = 8

            # 流程結束:已唯一識別

            self.status_text.set(f"UID 解析完畢!PCD 發送最終 SELECT 命令,卡片 {self.resolved_uid_part} 回覆 SAK。\n-> 卡片已被唯一選定,防衝突結束。")

            self.collision_status.set("防衝突結束")

            self.next_button.config(state=tk.DISABLED)

            return


        if not self.resolved_uid_part:

            # 第一次響應 (多卡衝突)

            self.status_text.set(

                f"1. PCD 發送防衝突命令 (CL1)。\n"

                f"2. {len(self.present_cards)} 張卡片同時響應,發生位元衝突。\n"

                f"3. PCD 檢測衝突點,準備分組選擇。"

            )

            # 模擬衝突點:假設在第 4 個十六進制字元發生衝突 ('04F1', '04C2', '04D3',在第三個字元 'F', 'C', 'D' 處衝突)

            # 我們將解析到 '04'

            self.resolved_uid_part = "04"

            self.collision_status.set("檢測到多卡衝突,正在解析...")

            self.next_button.config(text="執行下一輪 SELECT")

            self.selected_card.set(f"解析中: {self.resolved_uid_part} (2/8)")

            return

            

        

        # 模擬衝突解決步驟

        current_len = len(self.resolved_uid_part)

        next_char_index = current_len // 2 # 每兩個十六進制字元 (1 byte) 處理一次衝突


        if current_len < 8:

            

            # 找到在當前解析長度下,所有卡片響應的下一個字元

            next_chars = [uid[current_len:current_len+2] for uid in self.present_cards]

            

            if len(set(next_chars)) == 1 and current_len < 6:

                # 簡單情況:下一個字元沒有衝突

                new_part = next_chars[0]

                self.resolved_uid_part += new_part

                self.status_text.set(

                    f"PCD 發送 SELECT 命令 (前綴: {self.resolved_uid_part[:-2]})。\n"

                    f"下一位元組 ({new_part}) 無衝突。\n"

                    f"-> 解析前綴更新為 {self.resolved_uid_part}。"

                )

            else:

                # 複雜情況:發生衝突或需要分組

                # 模擬 PCD 選擇一個分組(這裡我們選擇 UID 最小的那一組來繼續)

                

                # 過濾出所有UID以resolved_uid_part為前綴的卡片

                candidate_cards = [uid for uid in self.card_uids if uid.startswith(self.resolved_uid_part)]


                if not candidate_cards:

                     self.status_text.set("錯誤:沒有卡片匹配當前前綴。流程中止。")

                     self.next_button.config(state=tk.DISABLED)

                     return


                # 選擇下一步解析的 UID (這裡我們直接選擇最小的那個 UID 進行下一步,模擬分組選擇)

                next_resolved_char = candidate_cards[0][current_len:current_len+2] 

                

                # 模擬 PCD 只讓以 next_resolved_char 開頭的卡片響應

                self.resolved_uid_part += next_resolved_char

                self.present_cards = [uid for uid in self.present_cards if uid.startswith(self.resolved_uid_part)]


                self.status_text.set(

                    f"PCD 發送 SELECT 命令 (前綴: {self.resolved_uid_part[:-2]})。\n"

                    f"檢測到衝突,PCD 選擇前綴為 {self.resolved_uid_part} 的卡片響應 ({len(self.present_cards)} 張)。\n"

                    f"-> 解析前綴更新為 {self.resolved_uid_part}。"

                )

                

            self.collision_status.set(f"剩餘 {len(self.present_cards)} 張卡片響應。")

            self.selected_card.set(f"解析中: {self.resolved_uid_part} ({len(self.resolved_uid_part)}/8)")

            

            # 檢查是否已完成 UID 擷取

            if len(self.resolved_uid_part) == 8:

                self.present_cards = [uid for uid in self.card_uids if uid.startswith(self.resolved_uid_part)]

                self.next_button.config(text="執行最終 SELECT 並獲取 SAK")


        else:

            self.perform_select_step() # 重新呼叫以執行最終 SELECT


if __name__ == "__main__":

    root = tk.Tk()

    app = AntiCollisionSimulator(root)

    root.geometry("550x350") 

    root.mainloop()


程式碼執行說明:

  1. 環境設定: 模擬器假設有三張卡片在場,UIDs 分別為 04F1AABB, 04C2CCDD, 04D3EEFF

  2. 操作流程:

    • 點擊「開始防衝突迴圈」(即第一輪 SELECT)。

    • 模擬器將顯示檢測到多卡衝突,並解析出第一個共同的 UID 前綴 (04)。

    • 重複點擊「執行下一輪 SELECT」。在每一次點擊中,PCD 會在內部進行位元解析,並發送更長的前綴來排除不匹配的卡片。

    • 最終,當 已解析UID前綴 達到 04F1AABB (8 個十六進制字元) 時,表示只有一張卡片(UID 為 04F1AABB)響應。

    • 此時,再次點擊按鈕將執行最終 SELECT,卡片回覆 SAK,流程結束,該卡片被唯一選定。

這個 Tkinter 模擬器展示了 PCD 如何透過不斷細化 SELECT 命令中的 UID 前綴來隔離並唯一識別多卡環境中的單一卡片,從而完成防衝突迴圈。


沒有留言:

張貼留言

WOKWI 模擬 MFRC522 RFID Reader + 5個 Tag 發行到MQTT 上

WOKWI 模擬 MFRC522 RFID Reader + 5個 Tag  發行到MQTT 上 MQTTgo.io # MQTT 設定 MQTT_BROKER = "mqttgo.io" MQTT_PORT = 1883 MQTT_TOPIC = ...