CC2530/CC2531无线IC卡考勤机实战:ZigBee组网与RC522读卡
简介这份资源面向嵌入式开发初学者与物联网方向的技术人员提供一套基于ZStack协议栈、用CC2530或CC2531实现无线IC卡考勤机的完整工程。CC2530为单芯片方案CC2531额外集成USB接口二者均属CC253X系列可共用同一套协议栈代码适合用来理解ZigBee组网、RFID读卡与考勤数据无线传输的完整链路。压缩包共313个文件约6.51MB以148个h头文件与122个c源文件为主体另有lib库、xcl链接配置、cfg网络参数、s51汇编及少量exe工具与工程文件覆盖驱动、协议栈配置与编译调试环节。资源已有162人学习内容涉及硬件接口、ZStack网络参数定制、无线通信机制、RFID调制解调、安全加密认证与低功耗管理等关键知识点读者可据此搭建可运行的考勤机原型并掌握从节点入网到数据安全传输的排错思路。1. 用 CC2530 做无线 IC 卡考勤机为什么这套老方案还在被反复翻出来如果你手上正好有一批 CC2530 或 CC2531 模块又想做一个能真正跑起来、不是点个灯就完事的项目无线 IC 卡考勤机是个很合适的方向。它把三件事揉在一起ZigBee 组网、RFID 读卡、数据回传。CC2530 自带 8051 内核和 2.4GHz 射频前端CC2531 则是带 USB 的版本适合做网关侧。两者都跑 ZStack 协议栈代码结构一致所以标题里说“支持 CC253X 系列”不是客套话是实打实的兼容。这套方案解决的核心问题是多个考勤点分散布置不方便拉网线但需要把刷卡记录汇总到一处。ZigBee 的自组网特性刚好对上——终端节点读卡路由节点中继协调器收集后通过串口或 USB 交给上位机。适合谁适合已经学过 51 单片机、想往无线和物联网方向迈一步的人也适合需要做小规模考勤 Demo 的团队。下面从选型、协议栈改造、读卡驱动到避坑一步步拆开讲。2. 硬件选型与 ZStack 工程搭建先把最小系统跑通2.1 CC2530 和 CC2531 到底怎么分工很多人拿到模块第一反应是“哪个都行”但实际分工很明确。CC2530 有 21 个 GPIO适合接 RC522 读卡模块、OLED 屏、按键CC2531 自带全速 USB 控制器插到上位机就能当串口用省掉 CH340 这类转换芯片。常见做法是协调器用 CC2531 做 USB 网关终端节点用 CC2530 接读卡器。型号内核射频USB典型角色注意点CC2530增强型 80512.4GHz IEEE 802.15.4无终端/路由节点需外接串口芯片调试CC2531增强型 80512.4GHz IEEE 802.15.4全速 USB协调器/网关USB 固件需单独配置选型时还要看 Flash 大小。CC2530 有 32KB/64KB/128KB/256KB 几个版本ZStack 加上读卡逻辑建议至少 128KB否则编译时容易报空间不足。CC2531 一般是 256KB宽裕一些。2.2 用 IAR 建立第一个可编译的 ZStack 工程ZStack 的官方例程通常以 IAR Embedded Workbench 为编译环境。假设你拿到的是 ZStack-CC2530-2.5.1a 这类经典版本目录结构里会有 Projects\zstack\Samples\ 下的 SampleApp。不要直接改 SampleApp复制一份重命名比如 AttendanceDev。# 目录结构示意以实际解压为准 ZStack-CC2530-2.5.1a/ ├── Components/ ├── Projects/ │ └── zstack/ │ └── Samples/ │ └── SampleApp/ │ ├── CC2530DB/ │ │ └── SampleApp.eww # IAR 工作区 │ └── Source/ │ ├── SampleApp.c │ ├── SampleAppHw.c │ └── OSAL_SampleApp.c └── Tools/打开 .eww 后先别急着写代码做三件事在 Workspace 下拉框里选 CoordinatorEB 或 EndDeviceEB确认预编译宏里 ZDO_COORDINATOR 或 RTR_NWK 是否正确在 f8wConfig.cfg 里改 PAN ID 和信道编译一次确认零错误。这一步是后面所有工作的地基地基不稳后面读卡读不出来你会怀疑人生。// f8wConfig.cfg 中几个必须确认的宏 -DDEFAULT_CHANLIST0x00000800 // 信道 110x0800 对应 bit11 -DZDAPP_CONFIG_PAN_ID0x1234 // 自定义 PAN ID避免和周围网络撞车 -DNWK_MAX_DEVICE_LIST10 // 协调器允许直接关联的子设备数逻辑说明DEFAULT_CHANLIST 是位掩码0x00000800 表示只用信道 11。如果周围 2.4GHz 设备多可以改成 0x00001800 同时开 11 和 12让协议栈自动选。PAN ID 建议避开 0xFFFF 和 0x0000这两个有特殊含义。NWK_MAX_DEVICE_LIST 决定协调器能直接带多少节点考勤场景一般 5 到 10 个点足够。参数怎么改如果终端节点数量超过 10要么加大这个值要么让部分节点走路由。加大它会占 RAMCC2530 的 RAM 只有 8KB别贪多。2.3 协调器和终端节点的角色配置ZStack 里角色由编译选项决定不是运行时切换。协调器工程定义 ZDO_COORDINATOR终端节点定义 RTR_NWK如果要做路由或者什么都不定义纯终端。在 IAR 的 Project Options → C/C Compiler → Preprocessor 里能看到这些宏。一个容易翻车的点终端节点如果定义了 RTR_NWK它会尝试做路由但如果你没给它接稳定电源路由表会频繁变化导致网络不稳定。考勤机的终端节点通常用电池或 USB 供电建议纯终端模式别让它承担路由职责。协调器则必须常供电它是整个网络的黑匣子断电后网络要重新组网。3. RC522 读卡驱动移植从 SPI 时序到 IC 卡卡号读取3.1 CC2530 的 SPI 引脚映射与 RC522 接线RC522 是 13.56MHz 读卡芯片支持 SPI、I2C、UART 三种接口SPI 最常用。CC2530 的 SPI 有 USART0 和 USART1 两组注意引脚映射USART0 的 SPI 模式在 P0_2SCK、P0_3MOSI、P0_4MISO、P0_5SSN但 SSN 通常用普通 GPIO 控制方便多从机。RC522 引脚CC2530 引脚说明SDASSP1_4片选低有效SCKP0_2SPI 时钟MOSIP0_3主机输出MISOP0_4主机输入RSTP1_5复位高有效VCC3.3V不要接 5VGNDGND共地接线时注意RC522 模块有的标 3.3V 有的标 5V但芯片本身是 3.3VCC2530 的 IO 也是 3.3V直接对接没问题。如果你买的模块带电平转换确认转换方向别接反。3.2 用 GPIO 模拟 SPI 还是用硬件 SPIZStack 里硬件 SPI 可能被其他组件占用而且 SPI 时钟极性、相位配置不对读出来全是 0xFF。我一般会先用 GPIO 模拟 SPI时序完全可控调试直观。等读卡稳定了再考虑换硬件 SPI 省 CPU。// GPIO 模拟 SPI 写一个字节模式 0CPOL0, CPHA0 uint8 RC522_SPI_WriteByte(uint8 data) { uint8 i; for (i 0; i 8; i) { RC522_SCK_LOW(); if (data 0x80) { RC522_MOSI_HIGH(); } else { RC522_MOSI_LOW(); } data 1; RC522_SCK_HIGH(); // 上升沿采样 if (RC522_MISO_READ()) { data | 0x01; } } RC522_SCK_LOW(); return data; }逻辑说明模式 0 下数据在 SCK 低电平时准备好上升沿采样。先发最高位循环 8 次。MISO 在上升沿之后读取拼到 data 最低位。注意最后要把 SCK 拉低否则下次通信第一个时钟沿会错位。参数说明RC522_SCK_LOW/HIGH 是宏对应 P0_2 置 0 或 1。RC522_MISO_READ 读 P0_4 电平。如果你用的引脚不同改宏定义即可不用动这个函数。3.3 寻卡、防冲突、读卡号的完整流程RC522 读一张卡要经过寻卡、防冲突、选卡、读块几个步骤。考勤机只需要卡号所以读到 UID 就够了不用读块数据。// 寻卡并读取 UID返回 0 表示成功 uint8 RC522_ReadCardUID(uint8 *uid) { uint8 status; uint8 buffer[5]; // 1. 寻卡RequestIdle 模式 status PcdRequest(PICC_REQIDL, buffer); if (status ! MI_OK) { return 1; // 没卡 } // 2. 防冲突获取 UID status PcdAnticoll(buffer); if (status ! MI_OK) { return 2; // 冲突 } // 3. 选卡 status PcdSelect(buffer); if (status ! MI_OK) { return 3; } // 4. 拷贝 UIDbuffer[0..3] 是卡号buffer[4] 是校验 memcpy(uid, buffer, 4); return 0; }逻辑说明PcdRequest 的参数 PICC_REQIDL 表示寻感应区内空闲卡。PcdAnticoll 返回 5 字节前 4 字节是 UID第 5 字节是异或校验。PcdSelect 用 UID 选中卡片之后才能读块。考勤场景读到 UID 就可以上报了不需要 PcdSelect 之后的读块操作但选卡能确认卡还在避免重复上报。参数说明uid 缓冲区至少 4 字节。返回 0 成功1 无卡2 冲突3 选卡失败。实际部署时无卡是常态不要当错误处理直接返回就行。4. 考勤数据通过 ZigBee 回传从串口到上位机的链路4.1 终端节点把卡号打包成 ZigBee 消息读到 UID 后终端节点要把它发给协调器。ZStack 里用 AF_DataRequest 发应用层数据。先定义自己的端点号和簇 ID别和 SampleApp 默认的冲突。// 自定义端点与簇 #define ATTENDANCE_ENDPOINT 10 #define ATTENDANCE_CLUSTER_ID 0x0001 // 发送卡号uid 为 4 字节 void Attendance_SendUID(uint8 *uid) { afAddrType_t destAddr; uint8 msg[6]; destAddr.addrMode afAddr16Bit; destAddr.addr.shortAddr 0x0000; // 协调器短地址固定为 0 destAddr.endPoint ATTENDANCE_ENDPOINT; msg[0] 0xA5; // 帧头 memcpy(msg[1], uid, 4); // 卡号 msg[5] 0xA5 ^ uid[0] ^ uid[1] ^ uid[2] ^ uid[3]; // 简单校验 AF_DataRequest(destAddr, Attendance_epDesc, ATTENDANCE_CLUSTER_ID, 6, msg, Attendance_TransID, AF_DISCV_ROUTE, AF_DEFAULT_RADIUS); }逻辑说明destAddr.shortAddr 设为 0x0000这是协调器的固定短地址。msg 共 6 字节帧头 0xA5 用于上位机同步最后 1 字节是异或校验防止射频干扰导致误码。AF_DISCV_ROUTE 让协议栈自动发现路由AF_DEFAULT_RADIUS 是默认跳数。参数说明Attendance_epDesc 是端点描述符需要在应用初始化时注册。Attendance_TransID 是事务 ID每次发送递增用于确认。如果网络里节点多建议在 msg 里加上节点短地址方便上位机区分是哪个考勤点。4.2 协调器收到数据后通过串口输出协调器收到 AF 消息后在接收回调里把数据通过串口发给上位机。CC2531 用 USB CDCCC2530 用 UART0。// 协调器接收回调 void Attendance_ReceiveCallback(afIncomingMSGPacket_t *pkt) { if (pkt-clusterId ATTENDANCE_CLUSTER_ID) { uint8 *msg pkt-cmd.Data; if (msg[0] 0xA5) { // 校验 uint8 check 0xA5 ^ msg[1] ^ msg[2] ^ msg[3] ^ msg[4]; if (check msg[5]) { // 通过串口输出节点地址 卡号 HalUARTWrite(0, (uint8 *)CARD:, 5); HalUARTWrite(0, msg[1], 4); HalUARTWrite(0, (uint8 *)\r\n, 2); } } } }逻辑说明先判断簇 ID再判断帧头再校验。校验通过才输出避免把干扰数据当卡号。HalUARTWrite 的第二个参数是数据指针第三个是长度。输出格式是 CARD: 加 4 字节卡号加换行上位机按行解析即可。参数说明HalUARTWrite 的第一个参数 0 表示 UART0。如果协调器用 CC2531 的 USB需要把串口重定向到 USB CDC具体在 hal_uart.c 里改 HAL_UART_ISR 相关配置。波特率默认 115200在 f8wConfig.cfg 里改。4.3 上位机端用 Python 做最小解析上位机不需要复杂框架一个串口读取加解析的脚本就能验证链路。import serial ser serial.Serial(COM3, 115200, timeout1) while True: line ser.readline().decode(ascii, errorsignore).strip() if line.startswith(CARD:): uid line[5:] print(f考勤卡号: {uid}) # 这里可以写入数据库或 CSV逻辑说明readline 按换行读strip 去掉空白。startswith 判断帧头uid 就是 4 字节卡号的十六进制字符串。实际使用时把 print 换成写文件或插数据库。参数说明COM3 换成实际串口号Linux 下是 /dev/ttyUSB0。timeout1 避免阻塞。如果卡号有重复上报可以在上位机做去重比如同一卡号 3 秒内只记一次。5. 避坑与排查那些让你怀疑射频坏了的瞬间5.1 现象终端节点入网后频繁掉线原因电源不稳。CC2530 发射时电流能到 30mA如果 USB 线太长或供电不足电压跌落会导致复位。另一个原因是信道拥挤周围 WiFi 路由器多2.4GHz 频段被占满。解决终端节点加 100uF 电解电容靠近 VCC 引脚。信道改用 15 或 20避开 WiFi 常用的 1、6、11。在 f8wConfig.cfg 里改 DEFAULT_CHANLIST 即可。5.2 现象RC522 寻卡偶尔成功大部分时间返回错误原因SPI 时序太快。GPIO 模拟 SPI 时如果没加延时CC2530 的 8051 内核跑 32MHz翻转速度可能超过 RC522 的 10MHz 上限。另一个原因是天线匹配模块上的电感电容如果虚焊读卡距离会从 3cm 缩到贴卡才行。解决在 SCK 翻转之间加 __delay_cycles(10) 或几个 nop。用示波器看 SCK 频率确保低于 10MHz。天线问题只能换模块或补焊软件无解。5.3 现象协调器串口输出乱码原因波特率不匹配。ZStack 默认 115200但有的例程改成 38400上位机没跟着改。或者 CC2531 的 USB CDC 描述符里波特率是虚拟的实际不受串口助手设置影响但驱动可能枚举成其他值。解决先确认 f8wConfig.cfg 里的 HAL_UART_DMA 和波特率宏。CC2531 用 USB 时在 hal_usb.c 里确认 CDC 波特率设置。用逻辑分析仪抓 UART 波形测一个位的宽度反推波特率。5.4 现象多张卡同时靠近读出来的卡号一样原因防冲突流程没做完整。PcdAnticoll 只处理一级冲突如果两张卡 UID 前几位相同后几位不同可能返回同一张卡的 UID。考勤场景虽然很少同时刷两张卡但测试时会遇到。解决在 PcdAnticoll 之后加 PcdSelect选卡成功再读 UID。如果还是冲突用 PcdAnticoll2 或自己做位冲突检测。实际部署时读卡区做小一点物理上避免两张卡同时进入。5.5 现象ZStack 编译报错 “Error[e16] Too much data”原因Flash 或 RAM 超了。CC2530 如果只有 64KB FlashZStack 加读卡驱动加串口很容易超。RAM 只有 8KB全局变量多了也报错。解决在 IAR 的 Linker 配置里看 .map 文件找占用最大的模块。关掉不用的功能比如 LCD、按键、多余的簇。把读卡缓冲区改小UID 只要 4 字节别开 64 字节的数组。实在不够就换 128KB 或 256KB 的 CC2530。6. 进阶技巧让考勤机更稳、更省电、更好维护6.1 用 NV 存储保存卡号白名单考勤机如果只记录不判断那谁都能刷。加一个白名单机制协调器把授权卡号下发到终端节点终端节点存到 NV非易失存储里刷卡时先比对合法才上报。ZStack 的 NV 操作在 osal_nv.c 里。// 保存卡号到 NV索引自定义 #define NV_INDEX_CARD_WHITELIST 0x0400 void SaveCardToNV(uint8 *uid) { uint8 buf[4]; osal_nv_read(NV_INDEX_CARD_WHITELIST, 0, 4, buf); // 简单追加实际项目用链表或数组管理 osal_nv_write(NV_INDEX_CARD_WHITELIST, 0, 4, uid); }逻辑说明osal_nv_read 先读旧值osal_nv_write 写入新值。NV 索引要避开 ZStack 已用的范围0x0400 之后一般可用。注意 NV 写入有寿命限制别每次刷卡都写只在授权变更时写。参数说明NV_INDEX_CARD_WHITELIST 是自定义索引不同工程别冲突。写入长度 4 字节正好一个 UID。如果要存多张卡用数组加索引管理别反复写同一地址。6.2 低功耗终端电池供电的考勤点如果考勤点不方便拉电源可以用电池加低功耗模式。CC2530 的 PM2 模式电流约 1uA但 RC522 待机也有 10mA 左右所以不能一直开着读卡。做法是终端节点定时唤醒开 RC522 寻卡 100ms没卡就睡。// 低功耗循环示意 while (1) { RC522_Init(); if (RC522_ReadCardUID(uid) 0) { Attendance_SendUID(uid); } RC522_DeInit(); // 关天线降功耗 osal_pwrmgr_task_state(Attendance_TaskID, PWRMGR_CONSERVE); osal_start_timerEx(Attendance_TaskID, WAKE_EVT, 2000); osal_set_cpu_ready(); }逻辑说明每次唤醒后重新初始化 RC522读完就 DeInit 关天线。PWRMGR_CONSERVE 让任务进入省电模式定时器 2000ms 后唤醒。实际电流能降到 1mA 以下两节 5 号电池撑几个月。参数说明WAKE_EVT 是自定义事件在任务初始化时注册。2000ms 是唤醒间隔考勤场景可以设 1 到 5 秒太短费电太长刷卡反应慢。6.3 用抓包工具确认 ZigBee 数据到底发出去没有调无线最怕“我以为发了”。用 CC2531 做嗅探器配合 Wireshark 抓 802.15.4 包能看到 AF_DataRequest 有没有真正发出、协调器有没有回 ACK。如果抓不到包先查信道和 PAN ID 是否一致。排查项正常表现异常处理信道嗅探器和节点一致改 f8wConfig.cfgPAN ID抓包显示相同检查 ZDAPP_CONFIG_PAN_ID短地址终端有 0xXXXX入网失败查协调器允许列表AF 包有簇 ID 和数据检查端点注册这个表格是我每次调 ZigBee 都会过一遍的清单比盲猜快得多。6.4 一个我踩过的坑别在中断里调 AF_DataRequest早期版本我把读卡中断里直接调 AF_DataRequest结果网络时好时坏。原因是 AF_DataRequest 会操作协议栈内部队列中断上下文里调用可能破坏状态机。后来改成中断里只置事件标志由任务循环处理发送稳定性立刻上来了。// 中断里只置标志 void RC522_IRQHandler(void) { osal_set_event(Attendance_TaskID, CARD_READ_EVT); } // 任务循环里处理 if (events CARD_READ_EVT) { if (RC522_ReadCardUID(uid) 0) { Attendance_SendUID(uid); } return (events ^ CARD_READ_EVT); }这个习惯我保持到现在中断只做最短的事协议栈操作一律放任务里。希望帮到你。本文还有配套的精品资源点击获取