☰
KLM900 RFID模块上位机开发:从串口接线到Java读卡实现
2026/10/28 8:35:51 网站建设 项目流程

简介:KLM900.rar 是一套面向 RFID 开发者的 M900 模块资料包,围绕芯片上电握手、固件升级与上位机程序编写展开。它适配 ISO 14443A、ISO 15693 等主流协议,既提供 C# 示例工程,也适合用 Java 通过串口进行二次开发,解决“如何写”与“跟上位机”的实际问题。压缩包共 74 个文件,整体仅 2.07MB;dll/exe 为可直接调用的动态库与演示程序,pdf/doc 含上电握手过程、固件使用及上位机说明,cs/sln/resx 等构成完整 VS 工程,另有 chm 帮助文档便于检索。目前已有 283 人学习。内容覆盖模块初始化、命令构造、数据包解析、返回指令处理等关键流程,并附 M900_Demo 供参考,可帮助快速完成串口联调、协议适配和 Java/C# 程序移植,显著提升 RFID 项目的开发效率。

1. KLM900 是什么,"m900跟上位机"到底卡在哪

很多做考勤闸机、图书自助借还、工具柜领用的人,拿到 KLM900 这类高频 RFID 读写器模块后,第一件事不是写 Java 业务逻辑,而是先解决它和电脑之间最基本的通信问题。KLM900 本身是非接触式读卡模块,负责把天线感应到的卡号和数据通过串口送给上位机;所谓"m900 跟上位机",就是在电脑端写一个上位机程序去控制它、接收它上报的卡号。这条链路一旦没打通,读卡器指示灯再亮,Java 程序里也只有一个黑匣子一样的串口对象,拿不到任何卡号。有人用 C#,有人用 Qt,有人用 Java,串口协议是共通的。这篇笔记就把链路完整拆开:先看接线和参数,再看帧格式,最后用 Java 做一套能持续读卡的上位机,并把容易翻车的几个坑提前标出来。

2. 把 KLM900 接到上位机:串口接线、电平与参数匹配

2.1 KLM900 的三种连接方式:USB 转 TTL、RS232 与 RS485

KLM900 这类嵌入式 RFID 模块,板子上直接引出的一般是 TTL 串口:VCC、GND、TXD、RXD 四个引脚。上位机电脑要么没有原生串口,要么只有 USB 口,中间必须有一个电平转换的桥。常见做法有三种。

第一种是 USB 转 TTL 调试线,芯片以 CH340、CP2102 居多。调试阶段它最省事,插上后电脑里出现一个虚拟串口,供电和信号线都从 USB 口走,适合开发阶段和短距离部署。第二种是模块自带 RS232 电平转换,比如板载 MAX232,可以接 DB9 串口线到工控机,现场拉线距离比 TTL 远一些,但要注意 TTL 和 RS232 的电平标准不一样,不能直接把 TXD、RXD 接到 DB9 上。第三种是 RS485 方式,适合距离超过十米或需要挂多台 KLM900 的产线,但上位机这边要额外处理收发切换,协议层也往往要多套一个 Modbus 之类的壳,调试成本明显更高。

我一般建议第一次接触的人直接选 CH340 的 USB 转 TTL 线,因为设备管理器里能直接看到端口号,出问题时能快速区分是驱动问题还是协议问题。RS485 看着工业级更专业,但上位机开发阶段容易在收发切换上反复折腾。等把帧格式跑通了,再考虑换成 RS485 也不迟。

2.2 接线顺序:先共地再信号,TX 和 RX 必须交叉

接线看着只有四根线,出错率却很高。按这张表接就不会接反:

KLM900 引脚USB 转 TTL 对端说明
VCC5V按模块标称电压取电,多数是 5V
GNDGND共地必须接,否则电平没有参考点
TXDRXDKLM900 发送端接电脑接收端
RXDTXDKLM900 接收端接电脑发送端

两个关键点。第一,TXD 和 RXD 必须交叉:模块的 TXD 接 USB 转 TTL 的 RXD,模块的 RXD 接 TTL 的 TXD。如果做成了 TX 对 TX,表现是命令发得出去但永远收不到回复。第二,GND 不接,串口电平没有参考地,表现是命令有去无回、偶尔还出乱码。先接 VCC 和 GND,再插 TXD、RXD,上电后看模块指示灯是否正常,再往电脑侧确认端口。

Linux 下可以用下面这两条命令确认设备有没有被识别:

# 列出 USB 转串口设备 ls -l /dev/ttyUSB* # 找不到时看内核日志,ch341/cp210x 是常见驱动名 dmesg | grep -i "ch341\|cp210x\|ttyUSB" | tail -20

如果ls里出现了/dev/ttyUSB0,说明驱动已经加载;如果只有/dev/ttyS0这类主板原生串口,说明 USB 转 TTL 没插好或者芯片驱动没装。Windows 下则是打开设备管理器,看"端口 (COM 和 LPT)"里有没有新的 COM 号,CH340 一般会显示为 "USB-SERIAL CH340"。这个端口名在 Java 代码里要原样使用,写错一个字母都打不开。

2.3 串口参数匹配:波特率、数据位、停止位、校验位

上位机和 KLM900 通信前要约定一组串口参数:波特率、数据位长度、停止位个数、校验方式。大多数这类 RFID 模块出厂默认是 9600 或 115200,数据位 8,停止位 1,无校验,简写 8N1。少数考勤一体机会设成 19200 或偶校验,所以最稳妥的动作是翻手册,不是凭经验猜。

调试时先按手册默认参数跑通,再考虑提波特率。上位机设 115200、模块实际在 9600,收到的就是一串乱码或者帧校验错,而且这种问题很隐蔽:看着串口有数据,但数据对不上。参数匹配的原则一句话就能说明:读写器是什么参数,上位机就设什么参数。

有个小技巧:很多模块上电后会主动发一条版本信息。用串口助手分别用 9600、19200、115200 去收,哪个波特率下能看到可读的 ASCII 字符串或者固定帧头,默认波特率就是哪个。这个技巧在找不到原厂手册时尤其管用,比盲猜快得多。

3. 读懂 KLM900 的通信帧:命令码、返回包与校验算法

3.1 一条读卡指令的完整帧:帧头、地址、命令码、长度、校验

KLM900 这类模块大多用自定义串口命令帧,而不是 Modbus 那种公开标准。不同厂商的命令码有差异,但帧骨架高度一致:帧头 + 设备地址 + 命令码 + 数据长度 + 数据区 + 校验字节。用一个最常见的形式说明:

字段长度示例值说明
帧头1 字节AA固定帧头,用来找命令起点
设备地址1 字节00单机环境默认 00,多机时区分设备
命令码1 字节01/02读版本、寻卡、读写块等操作
数据长度1 字节00后面紧跟的数据区字节数
数据区n 字节空命令参数或返回数据
校验1 字节--对前面所有字节累加,取低 8 位

以"读版本号"为例,假设命令码是 0x01,完整帧就是AA 00 01 00 AB。其中 AA 是帧头,00 是设备地址,01 是读版本命令,00 表示没有参数数据,AB 是校验:0xAA + 0x00 + 0x01 + 0x00 = 0xAB。如果寻卡命令码是 0x02,对应帧就是AA 00 02 00 AC。注意,这里的命令码是演示值,你拿到 KLM900 后第一件事是把手册里的命令码表抄出来替换进去,这个流程是不变的。

3.2 寻卡、选卡与读写块命令的返回包怎么读

上位机做 RFID 业务,最常见三个操作:寻卡、选卡、读写块。

寻卡用来探测射频场里有没有卡,返回包通常会带一个卡类型字节和 4 字节卡号(也就是 UID)。两个关键点:一是返回包里以某个字节作为状态标志,比如 00 表示成功、FF 表示失败,代码里要先判状态再取卡号;二是多张卡同时出现在天线场里时,寻卡返回的往往只是其中一张的 UID,需要靠选卡命令把某一张固定下来。

选卡是拿着刚才寻卡得到的 UID 再去锁定这张卡,后续读块、写块、扣费命令都只对这张被选中的卡生效。如果每次操作前都重新寻卡,拿到的是哪张不一定。很多"漏读、读错人"的现场问题,其实不是天线不行,而是软件没有先选中卡就发了后续命令。

读写块前通常要验卡扇区密码,也就是先发一个密钥验证命令,验证通过后才能读写该扇区内的块。M1 卡的块结构是扇区制的,每个扇区有独立的密钥 A 和密钥 B,读 UID 不需要密钥,但读用户数据块必须要。业务数据的存储结构应该按块规划,比如前两块放用户 ID,后一块放大门权限,不要随手写。

3.3 校验和计算:累加和版本与 CRC16 选型

校验字节是命令帧里最容易写错的部分。很多模块采用累加和,也就是把帧头、地址、命令码、长度和数据区逐字节相加,丢弃溢出位,只保留低 8 位。用 Python 描述这个算法非常直观:

def frame_checksum(parts: bytes) -> int: """对传入的帧内容逐字节累加,取低 8 位作为校验""" return sum(parts) & 0xFF def make_frame(addr: int, cmd: int, payload: bytes = b"") -> bytes: """按帧头+地址+命令+长度+数据+校验组装一条命令""" head = bytes([0xAA, addr, cmd, len(payload)]) + payload return head + bytes([frame_checksum(head)])

这里sum(parts)会得到一个大于 255 的数,& 0xFF就是取累加和的低 8 位。注意发送端和接收端必须用同一个规则:有的模块把校验放在帧尾,有的放在帧尾前一位;有的模块默认不校验帧头,计算范围只包含地址、命令码、长度和数据区。手册怎么写,代码就怎么写,不要"感觉应该是全部字节"。

部分高可靠设备会用 CRC16 而不是累加和,因为累加和对数据位错误不敏感。CRC16 的选型也有讲究,常见的是 Modbus CRC16,多项式 0x8005、初值 0xFFFF,以及 CCITT 变种。如果模块手册只写了"CRC16"没写多项式,最稳妥的做法是抓一条原厂软件发出的命令,用不同参数反向推算,或者直接找厂商要校验样例。自己发明校验算法是上位机开发里最不值当的事,因为嵌入式端不认。

4. 用 Java 实现 KLM900 上位机:串口库选型与读卡命令封装

4.1 串口库选型:jSerialComm 比 RXTX 更适合新项目

Java 做串口上位机,常见选择是 RXTX 和 jSerialComm。RXTX 资历老,网上一搜教程一大把,但它的 64 位 Windows 下需要手工把rxtxSerial.dll放进 JDK 目录,Java 9 模块化之后兼容性问题更多,新项目用它很容易在环境上耗掉半天。jSerialComm 用 Maven 直接引入,Windows、Linux、macOS 都内置了对应二进制,API 设计也贴近现代串口编程习惯。

Maven 坐标:

<dependency> <groupId>com.fazecast</groupId> <artifactId>jSerialComm</artifactId> <!-- 选一个 2.x 的稳定版本即可 --> </dependency>

坐标里没有写死版本号,因为串口库本身很稳定,你仓库里能拉到的最新 2.x 都能用。项目里用 Java 8 以上都不需要额外设置什么。如果你是做 C# 上位机或 Qt 上位机,串口通信原理完全一样,后面的帧格式、超时、断包处理也照样适用,只是 API 换了一层。

4.2 打开串口并设置 8N1:最小 Java 代码

写一个最基础的类,把串口打开、配置成 8N1、再关闭。这段代码是后面所有命令的骨架:

import com.fazecast.jSerialComm.SerialPort; public class Klm900Serial { public static void main(String[] args) { // Windows 上通常是 COM3,Linux 下是 /dev/ttyUSB0 SerialPort port = SerialPort.getCommPort("/dev/ttyUSB0"); // 波特率按 KLM900 手册来,这里以 9600 为例 // 8 数据位,1 停止位,无校验,即 8N1 port.setComPortParameters(9600, 8, SerialPort.ONE_STOP_BIT, SerialPort.NO_PARITY); // 读超时 300ms:没有数据时最多等 300ms,避免主线程卡死 port.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING, 300, 0); if (!port.openPort()) { throw new IllegalStateException("打开串口失败,检查端口是否被占用"); } System.out.println("串口已打开: " + port.getDescriptivePort()); // 用完必须关闭,否则下次启动会报端口忙 port.closePort(); } }

这段代码里有几个参数值得说明。getCommPort()传入的是系统识别出的端口名,Linux 下写/dev/ttyUSB0,Windows 下写COM3,大小写和数字都要和系统一致。setComPortParameters的前四个参数分别是波特率、数据位、停止位、校验位;ONE_STOP_BIT对应 1 位停止位,NO_PARITY对应无校验。TIMEOUT_READ_SEMI_BLOCKING的意思是"有数据就立即返回,没数据最多阻塞 300 毫秒",这个模式适合读卡上位机,因为寻卡命令要循环发,不能让线程永远等在没有数据的串口上。

4.3 命令帧封装:把读卡指令拼成 byte[] 再发送

有了串口对象,下一步是把第 3 章的帧结构落到 Java 代码里。先写帧构建方法:

private static byte[] buildFrame(byte addr, byte cmd, byte[] payload) { int len = payload == null ? 0 : payload.length; byte[] frame = new byte[4 + len + 1]; frame[0] = (byte) 0xAA; // 帧头 frame[1] = addr; // 设备地址 frame[2] = cmd; // 命令码 frame[3] = (byte) len; // 数据长度 if (payload != null) { // 把数据区复制到帧的第 4 字节之后 System.arraycopy(payload, 0, frame, 4, len); } int sum = 0; for (int i = 0; i < 4 + len; i++) { sum += frame[i] & 0xFF; // 累加时要按无符号数处理 } frame[frame.length - 1] = (byte) (sum & 0xFF); // 取低 8 位 return frame; }

这里最容易错的是frame[i] & 0xFF。Java 的 byte 是有符号的,0xAA 直接参与运算会被当成 -86,如果不& 0xFF,累加和的低位就会算错。数据区拷贝用System.arraycopy而不是循环赋值,因为源和目标的长度都由前面算好了,一行的效率更高也更不易错。

接着写一个收发方法,负责发命令、收响应:

private static byte[] transceive(SerialPort port, byte[] cmd, int maxLen) { port.writeBytes(cmd, cmd.length); // 发送命令帧 byte[] buf = new byte[maxLen]; int total = 0; long deadline = System.currentTimeMillis() + 500; // 500ms 就超时 while (System.currentTimeMillis() < deadline && total < maxLen) { // 每次读入最多 maxLen-total 字节,从 total 偏移处开始填 int n = port.readBytes(buf, buf.length - total, total); if (n > 0) { total += n; } } return Arrays.copyOf(buf, total); }

readBytes的签名是读入到buf的total偏移量处,最多读maxLen-total字节。500ms 超时是经验值:KLM900 这类模块的响应一般几十毫秒内会到,留 500ms 是给极端情况留余量。要注意,不能只调用一次readBytes就认为收到了完整响应,因为串口数据可能分两个包到达。

4.4 持续读卡:不要让主线程卡在串口上

上位机读卡的本质是"上位机不断发寻卡命令,读写器返回卡号",而不是等读写器主动上报。所以读卡循环必须放在独立线程里,不能占住 UI 或业务线程。一个典型的循环这样写:

public void startReadLoop() { // 寻卡命令:AA 00 02 00 AC,命令码按手册替换 byte[] findCmd = buildFrame((byte) 0x00, (byte) 0x02, null); Runnable task = () -> { while (!Thread.currentThread().isInterrupted()) { byte[] resp = transceive(port, findCmd, 32); if (resp == null || resp.length < 6) { sleep(200); // 没有卡或响应不完整,稍后再试 continue; } if (isValidFrame(resp)) { onCard(resp); // 把卡号交给业务层处理 } sleep(50); // 给读写器留出处理时间,别发命令太猛 } }; Thread t = new Thread(task, "klm900-reader"); t.setDaemon(true); t.start(); }

两个参数值得关注:sleep(200)是空读时的轮询间隔,这个值太小会导致上位机 CPU 占用飙高,太大则会感觉卡片放到天线上一两百毫秒才被识别;sleep(50)是读卡成功后的间隔,用来控制命令频率,避免把模块的缓冲区塞满。线程设为 daemon 是为了程序退出时不用手动打断线程,JVM 会自己收掉。

有了这个循环,你只需要在onCard回调里做考勤记录、开锁、扣款等业务逻辑,不用再碰串口细节。这也是上位机软件开发里常见的分层方式:串口层只负责收发字节,业务层只处理解析后的卡号。

5. KLM900 上位机避坑清单:五个高频问题与排查方法

5.1 乱码与超时:先查波特率,再查校验位

现象:串口助手能收到十六进制返回,Java 程序里发完命令却等不到完整响应,或者收到的字节里到处是FF、7F之类的错值。

原因:十有八九是波特率不一致。模块工作在 115200,上位机设成了 9600,两边按不同速率采样电平,结果就是乱码和超时。少数情况是停止位或校验位不一致。

解决:开发阶段不要凭猜,先把手册里的出厂参数抄到setComPortParameters里。如果模块上电会主动发版本信息,用串口助手扫 9600、19200、115200、57600 几档,哪一档能收到可读的固定帧头,参数就是哪档。改完 Java 参数后重启程序,乱码通常会消失。

5.2 读卡时好时坏:供电不足比天线问题更常见

现象:模块刚上电能读卡,运行几分钟后开始漏读,靠近天线时好时坏,远程怪天线没有调好。

原因:很多 KLM900 模块需要 5V 供电且瞬时电流能到几百毫安。有人直接用电脑 USB 口供电,USB 口限流后电压跌落,射频场强度不稳,读卡距离变近甚至读不到。这种现象和天线调参毫无关系,是供电没有富余量。

解决:用独立 5V 电源给模块供电,USB 转 TTL 只接 TXD、RXD、GND,VCC 不接。电源和模块之间尽量用短而粗的线,细长杜邦线在大电流下会有明显压降。改完供电后如果读卡距离恢复正常,就和天线无关。

5.3 端口被占用:程序退出后仍报 COM 口忙

现象:Java 程序第一次跑正常,第二次启动直接抛"打开串口失败",或者报Port busy。有时把 IDE 停掉,串口还是被占用。

原因:程序退出时没有关闭串口,或者上一次运行的 JVM 进程还活着。Windows 下尤其常见,因为驱动的串口句柄没有随进程退出自动释放,需要系统识别到句柄关闭后才可用。

解决:把所有openPort放在 try-finally 里,finally 中调用port.closePort()。程序异常退出后,在任务管理器里找到残留的 Java 进程杀掉,或者拔插一次 USB 转 TTL 让驱动重置。再进一步,可以在代码里加上一个 ShutdownHook,让 JVM 收到退出信号时主动关闭串口。

5.4 粘包与断包:按帧长度截取,不要按固定字节数去读

现象:第一次读卡正常,快速连续读卡时偶尔出现卡号错位,比如上一次的卡号混进这一次的返回里,或者一条完整响应被拆成两段到达。

原因:串口是字节流,没有消息边界。读写器返回的 8 字节响应可能一次全到,也可能分两三次到;上位机如果每次readBytes只读固定字节数,就会把两条响应拼在一起或者截断一条。编程时最容易犯的错就是"读 8 字节"作为一帧。

解决:在代码里维护一个接收缓冲区,把每次读到的字节先追加进去,然后从缓冲区里按帧头搜索第一条完整帧:找到帧头后,读取地址、命令码、数据长度,算出整帧长度,再判断缓冲区里有没有这么多字节。有,就截取出来,剩下部分继续等下一条。这个逻辑叫按帧截取,成本极低但能解决大部分"偶发散数据"的现场问题。

5.5 多台设备互相串数据:模块地址需要显式区分

现象:现场部署了两台 KLM900,上位机只连一台,收到的卡号却偶尔来自另一台,或者同一条命令被两台设备同时响应,解析出来的数据完全错乱。

原因:模块出厂地址都是 00,如果两台设备挂在同一上位机总线上,它们会同时响应同一命令。串口物理层上不可能天然分开。

解决:给每台模块分配独立地址。多数此类读写器有"设置模块地址"命令,一般是AA ADDR CMD LEN DATA的形式,DATA 里放新地址。地址设置完成后,上位机发给 01 的命令只有地址为 01 的设备会响应。代码里发送任何命令前都把buildFrame的 addr 参数填成目标设备地址,不要省事写死 00。

6. 进阶:先写脚本验协议,再做可重连的上位机服务

6.1 用 Python 脚本快速验证 KLM900 的返回帧

接手一个新的 KLM900 项目,我现在的习惯是先不写 Java,先把 Python 串口脚本跑起来。Python 写串口验证比 Java 爽快得多,库也更简单,一条命令发出去,立刻能看原始返回,不用处理 JVM 编译和依赖。

import serial import time ser = serial.Serial('/dev/ttyUSB0', 9600, timeout=0.5) # 演示寻卡命令:AA 00 02 00 AC,具体命令码以 KLM900 手册为准 find = bytes.fromhex('AA 00 02 00 AC') while True: ser.write(find) resp = ser.read(32) # 最多读 32 字节,超时 0.5s if resp: print(resp.hex(' ')) time.sleep(0.2)

这段脚本验证两件事:一是确认命令码对不对,二是确认返回帧的格式和你预期的字段结构一致。如果脚本里连续读到的卡号稳定且状态字节正确,再把这些逻辑移植到 Java 的transceive方法里,几乎没有坑。这个验证顺序能帮你区分硬件问题和软件问题,能极大减少后面的血泪排查时间。

6.2 给上位机加看门狗:断线重连与日志留存

跑生产环境时,USB 转 TTL 可能会被误拔,模块可能断电,Java 程序不能因为一次异常就彻底死掉。常见做法是在串口层套一个看门狗:每次操作超时或读到乱数据后,记录错误次数,连续错超过 5 次就自动closePort(),等 1 秒再重新openPort()。重连成功后打印一条日志,继续读卡循环。这个逻辑不能放在业务层,要放在串口线程的外层,类似这样:

while (!stopped) { try { if (port == null || !port.isOpen()) { openPortWithRetry(); } readLoopOnce(); } catch (Exception e) { log.error("串口异常,准备重连", e); closePort(); sleep(1000); } }

同时,每一条收到的原始十六进制响应都应该写进日志文件,包括校验失败的帧。漏读、脏数据这类问题在事发时极难复现,有了原始日志,事后才能定位是模块返回错还是上位机解析错。我当年做一套工具柜 RFID 识别,Java 程序跑半小时后必丢卡,查了两天没头绪,最后就是靠日志发现是上位机缓冲区被一次超大返回包顶爆了。从那以后,我的开发顺序基本固定:先 Python 脚本验证协议,再搭 Java 串口壳,再加日志和重连,最后才写业务逻辑。

这套顺序也建议你照着走一遍。串口通信看起来到处都是玄学,但只要你把波特率、帧结构、校验算法这三样焊死,再把断包重连和日志补齐,KLM900 这个项目就能稳稳落地。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询