防衝突迴圈 (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 -> PICC | CL1/CL2/CL3 命令 | PCD 發送特定級聯層級的防衝突命令。 |
| 2. 卡片響應 | PCD <- PICC | NUID/UID 數據 + BCC | 所有在場卡片同時回覆 UID 的一個區段。若多卡在場,則發生位元衝突 (Collision)。 |
| 3. 衝突檢測 | PCD | (內部操作) | PCD 檢測到響應中的位元衝撞點。 |
| 4. 分組處理 | PCD -> PICC | SELECT 命令 + 已解析的 UID 前綴 | PCD 根據衝突點,發送一個帶有 UID 部分前綴的 SELECT 命令,要求 UID 匹配該前綴的卡片繼續響應,其他卡片進入靜默。 |
| 5. 迴圈迭代 | PCD & PICC | (重複步驟 2-4) | 如果仍有衝突,重複 SELECT 和響應,直到只剩下一張卡片響應完整的 4 個位元組。 |
| 6. 級聯判斷 | PCD <- PICC | CT 標誌 | 當一個層級的 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()
程式碼執行說明:
環境設定: 模擬器假設有三張卡片在場,UIDs 分別為
04F1AABB,04C2CCDD,04D3EEFF。操作流程:
點擊「開始防衝突迴圈」(即第一輪 SELECT)。
模擬器將顯示檢測到多卡衝突,並解析出第一個共同的 UID 前綴 (
04)。重複點擊「執行下一輪 SELECT」。在每一次點擊中,PCD 會在內部進行位元解析,並發送更長的前綴來排除不匹配的卡片。
最終,當
已解析UID前綴達到04F1AABB(8 個十六進制字元) 時,表示只有一張卡片(UID 為04F1AABB)響應。此時,再次點擊按鈕將執行最終 SELECT,卡片回覆 SAK,流程結束,該卡片被唯一選定。
這個 Tkinter 模擬器展示了 PCD 如何透過不斷細化 SELECT 命令中的 UID 前綴來隔離並唯一識別多卡環境中的單一卡片,從而完成防衝突迴圈。







沒有留言:
張貼留言