# Python串口通信实战:手把手教你用RFID高频ISO15693标签读写数据(附完整代码)
如果你正在物联网或智能硬件的世界里摸索,想给一个设备“注入灵魂”,让它能识别、记录甚至与周围的物品“对话”,那么RFID技术很可能就是你正在寻找的钥匙。而ISO15693,作为高频RFID领域的一个成熟标准,因其较远的读写距离和良好的抗冲突能力,在资产管理、门禁控制、生产流程追踪等场景中应用广泛。今天,我们不谈枯燥的理论,直接上手实战。我将带你从零开始,用Python这门简洁而强大的语言,通过最基础的串口通信,一步步实现对ISO15693标签的寻卡、信息读取与数据写入。无论你是刚入行的嵌入式工程师,还是对硬件交互充满好奇的软件开发者,亦或是相关专业的学生,这篇内容都将为你提供一个清晰、可复现的实践路径。你会发现,让代码与物理世界交互,并没有想象中那么复杂。
## 1. 环境搭建与硬件连接
在开始编写任何一行代码之前,确保你的“战场”准备就绪是成功的第一步。这包括软件环境的配置和物理硬件的正确连接。一个稳定、可预测的起点,能避免后续调试中许多令人头疼的“玄学”问题。
### 1.1 硬件清单与连接指南
你需要准备以下硬件设备,它们构成了我们这次实验的物理基础:
* **一台个人电脑**:操作系统推荐使用Windows 10或11,macOS和Linux同样支持,但串口配置方式略有不同。确保有可用的USB接口。
* **RFID读写器模块**:核心设备,需支持ISO15693协议,工作频率为13.56MHz。市面上常见的模块如基于STM32或专用射频芯片(如MFRC522的升级版芯片,但需注意MFRC522主要支持ISO14443A)的开发板。模块需提供**串口(UART)通信接口**。
* **ISO15693标签卡**:实验对象,一张空白或可擦写的高频标签卡。
* **USB转TTL串口线(或模块)**:这是连接电脑和RFID模块的桥梁。如果您的RFID模块自带USB接口,则可直接连接;否则,就需要这条线将模块的TX、RX、GND引脚与电脑相连。
**连接步骤至关重要**:
1. 将USB转TTL串口线的**TX**引脚连接到RFID模块的**RX**引脚。
2. 将USB转TTL串口线的**RX**引脚连接到RFID模块的**TX**引脚。
3. 确保两者的**GND(地线)** 引脚牢固连接。
4. 为RFID模块接通所需的电源(通常是3.3V或5V,请严格参照模块说明书)。
> 注意:TX与RX必须交叉连接,即发送端接接收端。连接错误将导致通信完全失败。首次连接后,建议在设备管理器中确认串口号(如COM3、COM4)。
### 1.2 Python开发环境配置
Python环境是我们的“软件工作台”。我强烈建议使用**Anaconda**或**Miniconda**来管理环境,它能有效解决包依赖冲突。
1. **安装Python**:前往Python官网下载并安装3.8或以上版本。安装时务必勾选“Add Python to PATH”。
2. **创建虚拟环境(推荐)**:打开命令行(CMD或PowerShell),执行以下命令创建一个名为`rfid_env`的独立环境。
```bash
conda create -n rfid_env python=3.9
conda activate rfid_env
```
如果你不使用Conda,也可以用`venv`:
```bash
python -m venv rfid_env
# Windows激活
.\rfid_env\Scripts\activate
# macOS/Linux激活
source rfid_env/bin/activate
```
3. **安装核心库**:本次实战的核心是`pyserial`库,它提供了跨平台的串口访问能力。在激活的虚拟环境中运行:
```bash
pip install pyserial
```
为了更好的代码体验,你也可以安装`ipython`用于交互式调试。
4. **选择代码编辑器**:VS Code、PyCharm或任何你顺手的文本编辑器均可。VS Code配合Python插件能提供优秀的代码提示和调试体验。
## 2. 理解ISO15693与串口通信协议
在动手写代码前,花点时间理解我们正在与之对话的对象和规则,会让后续的编码工作事半功倍。这就像你要和一个人有效沟通,必须先知道他说什么语言、遵循什么礼仪。
### 2.1 ISO15693协议简析
ISO15693是一种针对 vicinity cards(邻近卡)的标准,读写距离通常在几十厘米到一米左右,比常见的门禁卡(ISO14443A)要远。其通信是**读写器先问,标签后答**的模式。
一次完整的指令交互,通常包含以下几个关键部分,它们被组织成一个**帧(Frame)**:
| 部分 | 说明 | 示例/常见值 |
| :--- | :--- | :--- |
| **帧起始符** | 标识一帧数据的开始,用于同步。 | 例如 `0xEE, 0xCC` |
| **指令码** | 指明要执行的操作,如寻卡、读块、写块。 | `0x01`(寻卡) |
| **数据长度** | 指示后续数据域的长度。 | 可变 |
| **数据域** | 包含具体的参数,如要操作的块地址、要写入的数据等。 | 卡号、块地址、用户数据 |
| **校验和** | 用于验证数据在传输过程中是否出错,常见的有CRC校验。 | 计算得出 |
| **帧结束符** | 标识一帧数据的结束。 | 例如 `0x0D, 0x0A`(回车换行) |
**重要概念:应用族标识(AFI)与数据存储**
* **应用族标识(AFI)**:一个字节的值,用于对标签进行分类。例如,图书馆可以用AFI=0x01标识图书,仓库用AFI=0x02标识工具。读写器可以只对特定AFI的标签进行操作,实现快速筛选。
* **数据存储结构**:标签内存被划分为多个**块(Block)**,每个块有固定的长度(如4字节、8字节)。读写操作都以块为单位进行。你需要知道总块数和块长度,才能正确规划数据存储。
### 2.2 串口通信参数与pyserial基础
串口是一种非常基础的异步通信方式。`pyserial`库让我们能用统一的Python接口操作不同操作系统上的串口。核心参数包括:
* **端口(Port)**:在Windows上是`COM3`、`COM4`等;在Linux/macOS上是`/dev/ttyUSB0`、`/dev/ttyACM0`等。
* **波特率(Baudrate)**:通信速度,必须与RFID模块设置一致,常见的有9600, 115200等。**115200**是很多高速模块的默认值。
* **数据位(Bytesize)**:通常为8位。
* **停止位(Stopbits)**:通常为1位。
* **校验位(Parity)**:通常为`serial.PARITY_NONE`(无校验)。
一个最基础的串口对象创建与配置如下:
```python
import serial
# 创建串口对象
ser = serial.Serial()
# 配置参数
ser.port = 'COM4' # 请根据实际情况修改
ser.baudrate = 115200
ser.bytesize = serial.EIGHTBITS
ser.parity = serial.PARITY_NONE
ser.stopbits = serial.STOPBITS_ONE
ser.timeout = 1 # 读超时时间(秒),防止read()无限等待
# 打开串口
try:
ser.open()
except serial.SerialException as e:
print(f"打开串口失败: {e}")
exit(1)
if ser.is_open:
print("串口打开成功!")
```
`timeout`参数非常实用,它设定了读操作的等待时间。在等待RFID模块返回数据时,设置一个合理的超时可以避免程序卡死。
## 3. 构建可复用的RFID通信类
将通信逻辑封装成一个类,是让代码从“一次性脚本”升级为“可复用工具”的关键。这不仅能提升代码的整洁度,更能方便地进行功能扩展和错误处理。
### 3.1 设计通信帧构建与解析方法
我们的RFID类需要核心的“编解码”能力:将高级指令(如“寻卡”)编码成模块能识别的原始字节流,并将模块返回的原始字节流解码成我们能理解的信息。
首先,我们定义一个`RFID15693`类,并初始化串口连接:
```python
import time
import serial
from typing import Optional, Tuple
class RFID15693:
"""ISO15693 RFID读写器通信类"""
# 协议常量定义(根据你的模块手册修改)
FRAME_HEADER = bytes([0xEE, 0xCC])
FRAME_TAIL = bytes([0x0D, 0x0A])
CMD_INVENTORY = 0x01 # 寻卡指令码
CMD_READ_SYS_INFO = 0x0C # 读取系统信息
CMD_WRITE_AFI = 0x05 # 写AFI
CMD_READ_BLOCK = 0x07 # 读块
CMD_WRITE_BLOCK = 0x08 # 写块
def __init__(self, port: str, baudrate: int = 115200):
"""
初始化RFID读写器连接
:param port: 串口号,如 'COM4', '/dev/ttyUSB0'
:param baudrate: 波特率,默认115200
"""
self.ser = serial.Serial(
port=port,
baudrate=baudrate,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=1.5 # 较长的超时,适应RFID操作
)
if not self.ser.is_open:
raise ConnectionError(f"无法打开串口 {port}")
print(f"[INFO] 已连接到读写器 @ {port}")
```
接下来,我们添加构建指令帧的私有方法。这是协议层的核心,需要严格按照模块手册的格式来组装字节。
```python
def _build_frame(self, cmd: int, data: bytes = b'') -> bytes:
"""
构建完整的指令帧
:param cmd: 指令码
:param data: 数据域字节
:return: 完整的帧字节序列
"""
# 长度 = 指令码(1) + 数据域长度
length = 1 + len(data)
# 组装帧:帧头 + 长度 + 指令码 + 数据域 + 帧尾
frame = self.FRAME_HEADER + bytes([length, cmd]) + data + self.FRAME_TAIL
return frame
```
然后,我们需要一个发送指令并接收响应的通用方法。这里要处理通信的稳定性,比如重试机制和超时判断。
```python
def _send_and_recv(self, frame: bytes, expected_min_len: int = 10) -> Optional[bytes]:
"""
发送指令帧并接收响应
:param frame: 要发送的指令帧
:param expected_min_len: 期望的响应最小长度
:return: 响应的原始字节数据,失败则返回None
"""
self.ser.reset_input_buffer() # 清空输入缓冲区,避免旧数据干扰
self.ser.write(frame)
time.sleep(0.1) # 给模块一点处理时间
# 等待并读取响应
start_time = time.time()
response = b''
while time.time() - start_time < self.ser.timeout:
if self.ser.in_waiting:
response += self.ser.read(self.ser.in_waiting)
# 检查是否收到完整的帧(以帧尾结束)
if response.endswith(self.FRAME_TAIL) and len(response) >= expected_min_len:
return response
time.sleep(0.01)
print(f"[WARN] 接收响应超时或数据不完整。收到: {response.hex()}")
return None
```
### 3.2 实现核心功能:寻卡与信息读取
有了基础的通信框架,我们就可以实现具体的业务功能了。首先是**寻卡(Inventory)**,这是所有操作的前提。
```python
def inventory(self) -> Optional[bytes]:
"""
执行单张标签寻卡操作
:return: 成功则返回8字节卡号(UID),失败返回None
"""
frame = self._build_frame(self.CMD_INVENTORY)
resp = self._send_and_recv(frame)
if resp and len(resp) > 15: # 确保响应长度足够包含卡号
# 假设卡号位于响应帧的固定偏移位置(例如第7-14字节)
# 这个位置需要根据你的模块协议手册确认!
uid = resp[7:15]
print(f"[SUCCESS] 寻卡成功!卡号: {uid.hex().upper()}")
return uid
else:
print("[FAIL] 寻卡失败,未检测到标签或通信错误。")
return None
```
寻到卡后,我们通常需要读取标签的**系统信息**,以了解其AFI、存储结构等。
```python
def read_system_info(self, uid: bytes) -> Optional[dict]:
"""
读取指定标签的系统信息
:param uid: 8字节卡号
:return: 包含系统信息的字典,失败返回None
"""
# 构建数据域:卡号
data_field = uid
frame = self._build_frame(self.CMD_READ_SYS_INFO, data_field)
resp = self._send_and_recv(frame)
if resp and len(resp) > 10:
# 解析响应,位置需根据协议调整
afi = resp[7] # 应用族标识
dsfid = resp[8] # 数据存储格式标识
block_num = resp[9] + 1 # 数据块数量
block_size = resp[10] + 1 # 数据块长度(字节)
info = {
'uid': uid.hex().upper(),
'afi': afi,
'dsfid': dsfid,
'block_num': block_num,
'block_size': block_size,
'total_memory': block_num * block_size
}
print(f"[INFO] 系统信息: AFI=0x{afi:02X}, 块数={block_num}, 块大小={block_size}字节")
return info
return None
```
## 4. 数据块读写与应用族标识管理
掌握了标签的基本信息后,我们就可以进行实质性的数据操作了:读写用户数据块和管理AFI。这是RFID应用中最核心的部分。
### 4.1 安全地读写数据块
数据块读写需要指定**块地址**。地址通常从0开始。**写操作需要格外小心**,因为某些标签的块可能是**一次可编程(OTP)** 的,写入后无法更改。
我们先实现读块功能。一个健壮的读块函数应该能处理单块或多块连续读取。
```python
def read_blocks(self, uid: bytes, start_block: int, num_blocks: int = 1) -> Optional[bytes]:
"""
从指定起始块读取一个或多个块的数据
:param uid: 卡号
:param start_block: 起始块地址
:param num_blocks: 要读取的块数
:return: 读取到的原始数据字节,失败返回None
"""
# 构建数据域:卡号(8) + 起始块地址(1) + 块数(1)
data_field = uid + bytes([start_block, num_blocks])
frame = self._build_frame(self.CMD_READ_BLOCK, data_field)
resp = self._send_and_recv(frame, expected_min_len=7 + num_blocks * block_size) # 假设已知块大小
if resp:
# 假设返回的数据从第7字节开始
data_start = 7
data = resp[data_start: data_start + num_blocks * self.block_size]
print(f"[SUCCESS] 从块{start_block}读取到{len(data)}字节: {data.hex().upper()}")
return data
return None
```
接下来是写块操作。在写入前,最好先读取一下目标块的内容,确认是否可写。
```python
def write_block(self, uid: bytes, block_addr: int, data: bytes) -> bool:
"""
向指定块写入数据
:param uid: 卡号
:param block_addr: 块地址
:param data: 要写入的数据,长度必须等于块大小
:return: 写入成功返回True,否则False
"""
if len(data) != self.block_size: # self.block_size可从系统信息获得
print(f"[ERROR] 写入数据长度必须为{self.block_size}字节,当前为{len(data)}字节。")
return False
# 构建数据域:卡号(8) + 块地址(1) + 数据
data_field = uid + bytes([block_addr]) + data
frame = self._build_frame(self.CMD_WRITE_BLOCK, data_field)
resp = self._send_and_recv(frame)
# 通常成功的响应会有一个特定的成功码(如0x00)
if resp and resp[6] == 0x00: # 假设第6字节是状态码
print(f"[SUCCESS] 数据成功写入块 {block_addr}。")
return True
else:
print(f"[FAIL] 写入块 {block_addr} 失败。")
return False
```
### 4.2 应用族标识(AFI)的读取与修改
AFI用于标签分类管理。修改AFI是一个需要谨慎对待的操作,因为有些标签的AFI字段可能是锁定的。
首先实现读取当前AFI(通常包含在`read_system_info`的返回中,这里我们实现一个独立的AFI读取指令,如果协议支持的话)。然后实现修改AFI。
```python
def write_afi(self, uid: bytes, new_afi: int) -> bool:
"""
修改标签的应用族标识
:param uid: 卡号
:param new_afi: 新的AFI值(0x00 - 0xFF)
:return: 修改成功返回True
"""
if not 0x00 <= new_afi <= 0xFF:
print("[ERROR] AFI值必须在0x00到0xFF之间。")
return False
# 构建数据域:卡号(8) + 新AFI值(1)
data_field = uid + bytes([new_afi])
frame = self._build_frame(self.CMD_WRITE_AFI, data_field)
resp = self._send_and_recv(frame)
if resp and resp[6] == 0x00:
print(f"[SUCCESS] AFI已成功修改为 0x{new_afi:02X}。")
return True
else:
print(f"[FAIL] 修改AFI失败。")
return False
```
### 4.3 完整实战流程与错误处理
现在,让我们将上述所有功能串联起来,形成一个完整的、健壮的实战流程,并加入必要的错误处理。
```python
def main_demo():
"""主演示函数"""
PORT = 'COM4' # 修改为你的实际串口号
BAUD = 115200
try:
# 1. 初始化读写器
reader = RFID15693(PORT, BAUD)
# 2. 寻卡
print("\n--- 步骤1: 寻卡 ---")
uid = reader.inventory()
if not uid:
print("未找到标签,请将标签靠近读写器天线。")
reader.close()
return
# 3. 读取系统信息
print("\n--- 步骤2: 读取系统信息 ---")
sys_info = reader.read_system_info(uid)
if not sys_info:
print("读取系统信息失败。")
reader.close()
return
# 将块大小等信息存储到reader实例中,供后续使用
reader.block_size = sys_info['block_size']
# 4. 修改AFI (示例:改为0x01)
print("\n--- 步骤3: 修改AFI ---")
if reader.write_afi(uid, 0x01):
# 再次读取系统信息确认修改
updated_info = reader.read_system_info(uid)
if updated_info:
print(f"确认AFI已更新为: 0x{updated_info['afi']:02X}")
# 5. 数据块读写演示
print("\n--- 步骤4: 数据块读写 ---")
target_block = 0 # 操作第0块
test_data = bytes([0xAA, 0xBB, 0xCC, 0xDD]) # 假设块大小为4字节
# 5.1 写入数据
print(f"尝试向块{target_block}写入数据: {test_data.hex()}")
if reader.write_block(uid, target_block, test_data):
# 5.2 读取数据以验证
time.sleep(0.1) # 稍作等待
read_back = reader.read_blocks(uid, target_block, 1)
if read_back and read_back == test_data:
print(f"验证成功!读回数据: {read_back.hex()}")
else:
print(f"验证失败!读回数据: {read_back.hex() if read_back else 'None'}")
# 6. 清理与关闭
print("\n--- 演示完成 ---")
reader.close()
except serial.SerialException as e:
print(f"串口通信错误: {e}")
except Exception as e:
print(f"程序运行出错: {e}")
if __name__ == "__main__":
main_demo()
```
这个演示流程覆盖了从连接到关闭的完整生命周期。在实际项目中,你可能需要将其模块化,例如将配置信息(端口、指令码偏移)放在配置文件中,或者增加更复杂的循环寻卡、多标签处理逻辑。
## 5. 高级技巧与调试实战
当你基本功能跑通后,可能会遇到一些“奇怪”的问题,或者想优化代码。这一部分分享几个我实践中总结的要点。
**调试是硬件编程的常态**。当通信失败时,一个可靠的调试方法是**打印并比对每一帧发送和接收的原始十六进制数据**。
```python
# 在_send_and_recv方法中增加调试输出
def _send_and_recv_debug(self, frame: bytes, expected_min_len: int = 10) -> Optional[bytes]:
print(f"[TX] {frame.hex().upper()}") # 打印发送的帧
# ... 发送逻辑 ...
if response:
print(f"[RX] {response.hex().upper()}") # 打印接收的帧
# ... 后续逻辑 ...
```
将打印出来的数据与你的RFID模块的**官方协议手册**进行逐字节比对。常见问题包括:
* 指令码错误。
* 数据域长度不对。
* 校验和计算错误(如果协议要求)。
* 响应超时(天线连接不良、标签不在场、波特率不匹配)。
**性能与稳定性优化**方面,对于需要快速连续读卡的场景(如流水线),要避免在循环内频繁创建和销毁串口对象。保持串口常开,并优化指令间隔时间。另外,考虑引入**重试机制**和**异常状态恢复**(如遇到通信错误时重置串口缓冲区)。
最后,关于**数据安全与完整性**,对于重要的数据写入,务必实现“**读-验证-写**”的流程。即先尝试读取目标块,确认其状态(是否可写、原有数据),再进行写入,写入后立即读取验证。对于关键应用,考虑在数据中加入自定义的校验码(如CRC16)或版本号,并在每次读取时进行校验。
硬件编程的魅力在于,你的代码直接作用于物理世界。当你看到打印出的卡号,或者成功改写标签数据时,那种成就感是纯粹的软件开发难以比拟的。希望这份指南能帮你顺利跨过RFID开发的第一道门槛。剩下的,就是发挥你的创意,将这些基础操作组合起来,去构建更酷的物联网应用了。如果在实践中遇到协议细节对不上的问题,记住,那份可能不太起眼的模块硬件手册,才是你最好的朋友。