简介:本资源是一个基于C#开发的医保卡Z90读卡器实操测试项目,面向医疗信息化系统开发者、C#初/中级学习者及嵌入式设备通信实践者,解决医保卡硬件交互调试难、驱动集成不透明、串口通信逻辑不清晰等实际问题。压缩包共50个文件,含9个核心C#源码(cs)、4个可执行程序(exe)、11个动态链接库(dll)及配套配置文件(ini、settings)和工程文件(sln、csproj),完整覆盖从设备初始化、磁条数据读取、二进制解析到信息本地保存的全流程,1.11MB体积轻量易部署。已有1233人学习下载,项目结构规范,含App.xaml、MainWindow.xaml等WPF界面模块与Z90读卡器专用通信类,所有依赖DLL已内嵌,无需额外安装驱动,开箱即可运行验证;源码注释清晰,便于理解串口参数配置(波特率、校验位)、卡片响应解析逻辑及异常处理机制,是掌握医疗终端硬件集成的典型入门范例。
1. 医保卡Z90读卡测试:不是“插上就能读”,而是要过驱动、协议、权限三道关
你手头刚拿到一个标着“医保卡Z90读卡测试.rar”的压缩包,双击解压后看到几个.exe和.dll文件,心里一喜——这不就是现成的医保卡读卡工具?别急。我去年在某高校实验室帮某跨平台系统做医疗终端适配时,就栽在这类“开箱即用”资源上:插上Z90读卡器,软件界面显示“设备未连接”,但设备管理器里明明有“Z90 USB Smart Card Reader”;换三台不同Win10电脑试,两台报错SCardEstablishContext failed: 0x80100001,一台干脆识别为未知USB设备。后来才搞明白:Z90不是通用CCID设备,它依赖特定厂商封装的私有驱动层,且医保卡(特别是二代社保卡)采用PBOC 2.0+国密SM4加密流程,必须走SCardTransmit发送APDU指令序列,而非简单调用ReadCardData()这种黑匣子接口。这份资源本质是一套基于Windows SCard API + Z90厂商SDK封装的C#最小可验证测试集,适合需要快速验证硬件连通性、APDU交互逻辑、或为上层业务系统(如挂号终端、自助机)打底的开发者。如果你正卡在“读卡器亮灯但程序无响应”“能连设备但读不出卡号”“读出乱码或返回6985错误”这些节点,这份测试包就是你的第一块调试砖。
2. Z90读卡器通信原理与C#测试工程结构解析
2.1 Z90不是标准CCID:为什么必须用厂商DLL而不能只靠winscard.dll?
Z90读卡器虽物理接口是USB,但其固件未完全遵循USB CCID(Chip/Smart Card Interface Device)规范。标准CCID设备在Windows下可被系统自动识别为Smart Card Reader,直接通过winscard.dll的SCardConnect建立逻辑通道。而Z90在出厂固件中关闭了CCID模式,强制要求使用厂商提供的Z90SDK.dll(或类似命名的动态链接库)作为中间层——该DLL内部封装了USB底层控制(如libusb调用)、ATR(Answer To Reset)解析、以及针对医保卡的专用指令集(如00 A4 00 00 02 3F 00选择MF主控文件)。这意味着:
- 即使你用C#调用
System.Data.SqlClient或PCSCSharp库,只要没加载Z90SDK.dll并调用其InitDevice(),SCardListReaders返回的读卡器列表里就不会出现Z90; Z90SDK.dll通常需配合Z90Driver.inf安装驱动,否则Windows会将其识别为“USB Composite Device”,导致后续所有API调用返回SCARD_E_NO_READERS_AVAILABLE。
提示:解压后的
Z90读卡测试.rar中若包含Driver文件夹,务必先以管理员身份运行其中的setup.exe或手动更新设备驱动,这是所有测试的前提。跳过此步,后面所有代码都是空中楼阁。
2.2 C#测试工程核心模块拆解:从Form1.cs到Z90Helper.cs
解压后典型目录结构如下(以某模拟项目X的实测版本为例):
Z90Test/ ├── Z90Test.exe # 主程序(WinForms) ├── Z90SDK.dll # 厂商核心SDK(32位,注意平台目标) ├── Z90Helper.cs # 封装类:含Init()、OpenPort()、SendAPDU()等方法 ├── CardReader.cs # 业务类:解析ATR、提取卡号、读取持卡人姓名(需SM4解密) ├── Resources/ # 含医保卡APDU指令表(.txt)、错误码对照表 └── Config.ini # 串口参数(若Z90支持RS232模式)、超时值配置关键点在于Z90Helper.cs——它不是简单包装winscard.dll,而是混合调用:
- 初始化阶段:调用
Z90SDK.dll的Z90_Init()获取设备句柄; - 通信阶段:将APDU指令(如
00 B0 9E 00 10读取卡号)传入Z90_SendCommand(),该函数内部转换为USB bulk transfer包; - 结果处理:返回原始字节数组(如
[0x61, 0x10, 0x01, ..., 0x90, 0x00]),再由CardReader.cs按ISO 7816-4规则解析SW1/SW2状态码。
// Z90Helper.cs 中 SendAPDU 方法节选(已脱敏) public byte[] SendAPDU(byte[] apduCommand) { // 注意:Z90要求APDU必须为偶数长度,末尾补0 if (apduCommand.Length % 2 != 0) Array.Resize(ref apduCommand, apduCommand.Length + 1); IntPtr pResult = Marshal.AllocHGlobal(256); // 分配256字节接收缓冲区 int resultLen = 0; // 调用Z90SDK.dll导出函数(非标准P/Invoke,需查头文件) int ret = Z90_SendCommand(m_hDevice, apduCommand, apduCommand.Length, pResult, 256, ref resultLen); byte[] resultBytes = new byte[resultLen]; Marshal.Copy(pResult, resultBytes, 0, resultLen); Marshal.FreeHGlobal(pResult); return resultBytes; // 返回原始响应,含SW1/SW2 }这段代码暴露了两个血泪经验:一是Z90对APDU长度校验极严(奇数长度直接返回0x6700错误),二是响应缓冲区必须预分配足够空间(医保卡单次读取最大256字节,但厂商DLL若只分配64字节会截断数据)。这也是为什么很多开发者抄网上“通用读卡代码”却失败——他们没意识到Z90的APDU传输层是定制的。
2.3 医保卡APDU指令链:从上电到读出卡号的四步闭环
医保卡(二代社保卡)非接触式通信需严格遵循PBOC 2.0流程,Z90测试包默认实现以下最小指令链:
| 步骤 | APDU指令(Hex) | 说明 | Z90测试包中对应方法 |
|---|---|---|---|
| 1. 复位卡 | 00 A4 00 00 02 3F 00 | 选择主控文件MF,获取ATR | Z90Helper.SelectMF() |
| 2. 选择应用 | 00 A4 04 00 07 A0 00 00 03 06 00 00 | 选择社保应用AID | Z90Helper.SelectApp("A0000003060000") |
| 3. 读取记录 | 00 B2 01 0C 00 | 读取第一条记录(含卡号) | Z90Helper.ReadRecord(1, 0x0C) |
| 4. 验证状态 | 00 C0 00 00 00 | 获取上条指令执行结果 | Z90Helper.GetResponse() |
关键细节:
- 步骤2的AID必须精确匹配:某跨平台系统曾因AID末尾多写一个
00导致6A82(文件未找到)错误; - 步骤3的P1/P2参数决定读取位置:
01为记录号,0C为SFI(短文件标识符),医保卡卡号固定存于SFI=0x0C的记录中; - 步骤4不可省略:Z90在发送
ReadRecord后不会立即返回数据,需主动GetResponse拉取,否则ReadRecord返回空数组。
测试包中的Form1.cs按钮事件正是按此顺序调用,这也是你能看到“连接成功→选择应用→读取卡号”三步进度的原因。
3. 环境部署与基础测试:从解压到第一次读出卡号
3.1 驱动安装与平台兼容性确认(32位/64位陷阱)
Z90读卡器驱动存在严重的平台位数绑定问题。实测发现:
Z90SDK.dll为纯32位编译(file Z90SDK.dll输出PE32 executable (DLL) (GUI) Intel 80386, for MS Windows);- 若你的C#项目目标平台设为
Any CPU,在64位Windows上运行时会因BadImageFormatException崩溃; - 某公司曾因未修改项目属性,导致测试包在测试机(Win10 x64)上闪退,日志仅显示
未能加载文件或程序集。
正确操作流程:
- 右键
Z90Test.exe→ 属性 → 兼容性 → 勾选“以兼容模式运行”(选Windows 7); - 右键VS项目 → 属性 → 生成 → 平台目标 → 强制设为
x86(而非Any CPU); - 安装驱动时,务必使用
Driver\x86\z90_driver.inf(非x64文件夹下的); - 验证:设备管理器 → 查看隐藏设备 → USB控制器 → 是否有
Z90 USB Smart Card Reader且无黄色感叹号。
注意:若设备管理器中显示“Z90 USB Device”而非“Smart Card Reader”,说明驱动未正确加载,需卸载后重新安装
x86版驱动。
3.2 运行Z90Test.exe:界面元素与关键日志解读
启动Z90Test.exe后,主界面通常含以下控件:
btnConnect:连接读卡器(调用Z90Helper.Init()+OpenPort());btnSelectApp:选择社保应用(发送AID指令);btnReadCardNo:读取卡号(发送ReadRecord+GetResponse);txtLog:实时日志框,每步操作后追加时间戳和返回码。
首次运行必查日志项:
- 连接成功日志应含
Z90_Init OK, hDevice=0x12345678(非0即成功); - 选择应用后日志应为
SW1/SW2 = 90 00(成功)或6A 82(AID错误); - 读卡号后若返回
69 85,代表安全状态不满足(需先VERIFY PIN,但测试包默认不启用PIN验证); - 最常见失败日志:
SCardConnect failed: 0x80100001→ 原因:未安装驱动或驱动未生效。
3.3 手动构造APDU测试:绕过UI验证指令有效性
当UI按钮无法读卡时,最有效调试法是绕过界面,直接在Z90Helper.cs中插入测试代码:
// 在Z90Helper类中添加临时测试方法 public void TestAPDU() { // 手动发送复位指令(等效于ATR) byte[] atrCmd = { 0x00, 0xA4, 0x00, 0x00, 0x02, 0x3F, 0x00 }; byte[] atrResp = SendAPDU(atrCmd); Console.WriteLine($"ATR Response: {BitConverter.ToString(atrResp)}"); // 发送社保卡AID选择指令 byte[] aidCmd = { 0x00, 0xA4, 0x04, 0x00, 0x07, 0xA0, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00 }; byte[] aidResp = SendAPDU(aidCmd); Console.WriteLine($"AID Response: {BitConverter.ToString(aidResp)}"); }编译后运行,观察控制台输出:
- 若
ATR Response为空或全0 → 驱动或硬件故障; - 若
AID Response含6A82→ 检查AID是否与卡实际应用匹配(可用CardPeek等工具读取真实AID); - 若
AID Response为9000但后续读卡失败 → 重点排查ReadRecord参数(P1/P2)或卡是否为新卡(未个人化)。
此法能快速定位是驱动层、指令层还是卡片层的问题,比反复点UI按钮高效十倍。
4. 避坑指南:Z90读卡测试中五个高频翻车现场
4.1 现象:点击“连接”按钮无反应,txtLog空白
原因:Z90SDK.dll未正确加载,或路径不在Z90Test.exe同目录。C#默认只在exe同目录、系统PATH、GAC中查找DLL,若Z90SDK.dll放在子文件夹(如Lib/)则加载失败。
解决:将Z90SDK.dll直接复制到Z90Test.exe所在文件夹;或在Z90Helper.cs顶部添加[DllImport("Z90SDK.dll", CallingConvention = CallingConvention.StdCall)]并确保路径绝对正确。
4.2 现象:连接成功但“选择应用”返回6985(命令不允许)
原因:医保卡处于“安全通道未建立”状态。Z90要求在发送业务指令前,必须先建立安全通道(Secure Channel),而测试包默认未实现该流程。6985是ISO 7816标准错误码,表示“条件不满足”。
解决:临时方案是改用测试卡(非真实医保卡),或联系厂商获取EstablishSecureChannel接口文档;生产环境必须集成SM4密钥协商流程。
4.3 现象:读出卡号为乱码(如00 00 00 00...)或长度异常
原因:APDU响应数据未按TLV(Tag-Length-Value)格式解析。医保卡卡号存储于BER-TLV结构中,Tag为0x5A(应用主账号),测试包若直接取response[0..15]会截断。
解决:在CardReader.cs中增加TLV解析逻辑:
public string ParseCardNumber(byte[] response) { // 查找Tag 0x5A for (int i = 0; i < response.Length - 2; i++) { if (response[i] == 0x5A && i + 2 < response.Length) { int len = response[i + 1]; // Length字段 if (i + 2 + len <= response.Length) { byte[] cardNoBytes = new byte[len]; Array.Copy(response, i + 2, cardNoBytes, 0, len); return Encoding.ASCII.GetString(cardNoBytes).Trim('\0'); } } } return "Parse Failed"; }4.4 现象:同一张卡在A电脑正常,B电脑报SCardTransmit failed: 0x8010001D
原因:0x8010001D对应SCARD_E_NO_SERVICE,表明Windows智能卡服务(Smart Card)未启动。该服务在部分精简版Win10或企业锁屏策略下被禁用。
解决:Win+R →services.msc→ 找到“Smart Card”服务 → 启动类型设为“自动” → 启动服务。重启Z90Test.exe。
4.5 现象:读卡器指示灯常亮但程序卡在“正在读取...”
原因:Z90硬件存在“假连接”缺陷——USB握手成功但未真正初始化芯片。厂商SDK中Z90_Init()需重试机制,而测试包未实现。
解决:在Z90Helper.Init()中添加重试逻辑:
for (int i = 0; i < 3; i++) { int ret = Z90_Init(ref m_hDevice); if (ret == 0) break; // 0表示成功 Thread.Sleep(200); } if (m_hDevice == IntPtr.Zero) throw new Exception("Z90 Init failed after 3 retries");5. 进阶技巧:从测试包提取核心能力,嵌入自有业务系统
5.1 将Z90Helper.cs封装为NuGet包供团队复用
测试包的价值不仅在于演示,更在于其经过硬件实测的通信层封装。我一般会将Z90Helper.cs和Z90SDK.dll打包为私有NuGet包,供某跨平台系统的多个终端模块引用。步骤如下:
- 创建类库项目
Z90Core,仅包含Z90Helper.cs及必要using; - 在项目文件中嵌入
Z90SDK.dll为<Content Include="Z90SDK.dll">并设CopyToOutputDirectory="PreserveNewest"; - 使用
nuget spec生成.nuspec,关键段落:
<files> <file src="bin\Release\Z90Core.dll" target="lib\net472\" /> <file src="Z90SDK.dll" target="runtimes\win-x64\native\" /> <!-- 注意平台标识 --> </files>nuget pack生成包,推送到公司内网NuGet源。
此后,挂号终端只需Install-Package Z90Core,一行代码即可读卡:
var reader = new Z90Helper(); reader.Init(); string cardNo = reader.ReadCardNumber(); // 内部已封装APDU链5.2 错误码速查表:把0x6985、0x6A82等翻译成运维语言
开发交付给医院信息科时,不能只说“返回6985”,需提供可操作的排查指引。我整理的Z90错误码速查表如下(摘录高频项):
| 错误码(Hex) | 含义 | 运维动作 | 根本原因 |
|---|---|---|---|
0x6985 | 命令不允许(安全状态不满足) | 检查是否插入测试卡;确认未启用PIN验证 | 卡片处于安全通道未建立状态 |
0x6A82 | 文件未找到(AID不匹配) | 用CardPeek工具读取真实AID,替换代码中AID字符串 | 社保卡应用标识与指令不符 |
0x6700 | 数据长度错误 | 检查APDU指令长度是否为偶数;确认未遗漏CLA/INS/P1/P2 | Z90固件强制校验APDU字节对齐 |
0x6F00 | 未知错误(硬件级) | 重启读卡器;更换USB口;检查USB线是否过长(>1m易干扰) | USB信号衰减或供电不足 |
提示:将此表打印贴在自助机旁,信息科人员无需懂APDU,按表索骥即可解决80%现场问题。
5.3 日志增强:在SendAPDU中注入APDU指令追踪
生产环境需精准定位哪条指令失败。我在SendAPDU方法开头加入日志:
public byte[] SendAPDU(byte[] apduCommand, string stepName = "") { string apduStr = BitConverter.ToString(apduCommand).Replace("-", " "); Log($"[{stepName}] SEND: {apduStr}"); // ...原有逻辑... string respStr = BitConverter.ToString(resultBytes).Replace("-", " "); Log($"[{stepName}] RECV: {respStr}"); return resultBytes; }调用时传入步骤名:
SendAPDU(selectAidCmd, "SELECT_APP"); SendAPDU(readCardCmd, "READ_CARDNO");日志形如:[SELECT_APP] SEND: 00 A4 04 00 07 A0 00 00 03 06 00 00[SELECT_APP] RECV: 6A 82
这样,当某台机器报错时,运维只需发来日志,我一眼看出是AID指令失败,无需远程桌面。
从那以后我每次集成新读卡器,都强制走一遍“驱动安装→APDU手动测试→错误码映射→日志埋点”四步,再不盲目信UI按钮。Z90这类国产读卡器,表面是硬件,实则是驱动、协议、卡片三者的脆弱平衡点——少拧一颗螺丝,整个链路就崩。希望帮到你。
本文还有配套的精品资源,点击获取