news 2026/10/26 11:07:21

RC522+51单片机门禁读卡开发:软件SPI、寻卡选卡与M1扇区读写

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
RC522+51单片机门禁读卡开发:软件SPI、寻卡选卡与M1扇区读写

简介:面向51单片机与RFID入门及进阶开发者的RC522学习资料包,围绕13.56MHz非接触通信与ISO/IEC 14443A标准,解决从硬件连接到读写程序实现的完整流程问题。内容涉及RC522与51单片机的SPI接线及SCK、MISO、MOSI、SS、INT引脚功能,SPI时钟极性与相位配置,复位、工作模式选择、天线调谐等初始化命令流程,以及卡片检测、防冲突、数据加密解密和读写响应处理。压缩包约91.47MB,上游未提供文件总数及类型明细,实际内容以解压后的程序与文档为准。已有1219人学习。资料价值在于把零散知识点连成可复用的开发路径:可参考C语言或汇编示例理解命令收发与错误处理,借助门禁、电子标签读写器等案例把握系统设计思路,并补充天线设计与调谐、电源管理等容易被忽略的工程细节,适合课程设计、毕业设计和DIY项目查漏补缺。

1. 从一张门禁卡说起:RC522 加 51 单片机这套资料能跑出什么

接手一个门禁小改款,甲方只给了一片 51 和一张现成的 IC 卡,预算刚好够一个读卡模块。翻完手上这包 RC522 基于 51 单片机的资料,它算是把该给的都铺开了:13.56MHz 读卡芯片的 SPI 时序、寄存器配置、寻卡防冲突、M1 扇区读写全在 C 文件里,配着接线图和最小系统原理图,P1 口四根线接上去,卡号就能从串口吐出来。

它适合三类人:课程设计要交差的学生、拿 51 做门禁考勤小产品的开发者、以及想弄明白无接触卡到底怎么被读出来的自学者。别指望它是个封装好的库,代码偏底层,连寄存器都得自己移位拼地址;但正因为底层,出了问题不用对着黑匣子猜。下面按接线、驱动、卡操作、踩坑、二次开发的顺序拆一遍,照着走能跑通,也能改成自己的东西。

2. 硬件与通信层:引脚接线、3.3V 供电和软件模拟 SPI

2.1 引脚接线与供电的现实取舍

51 系列基本没有硬件 SPI,所以这套资料走的是软件模拟。好处是引脚随便挪,坏处是时序全靠代码保证,接错一根就读回一堆 0xFF。常见接法是把 RC522 挂在 P1 口,因为 P1 是标准准双向口,做输出时写 1 即可,做输入时也能靠内部弱上拉顶一下。

模块引脚51 侧接法说明
SDA / NSSP1.3片选,低电平有效,每个寄存器访问都要完整拉高一次
SCKP1.0时钟,主机输出
MOSIP1.1主机输出从机输入
MISOP1.2主机输入,读之前 P1 口要先写 1
RSTP1.4复位,低有效,上电必须拉低再放开
VCC3.3V裸模块严禁 5V,带稳压的红板另说
GNDGND必须共地,别指望 USB 那头的地能顶替
IRQ悬空这套代码走轮询,中断脚用不上

供电这块是第一个分水岭。裸模块(蓝色小板)只吃 2.5~3.3V,5V 上去基本就是一次性用品;市面上常见的红色小板背后焊了一颗 AMS1117-3.3,VCC 可以给 5V,但它的 SDA/SCK/MOSI 这些信号脚依旧是 3.3V 电平,51 输出 5V 高电平长期打进去并不合规。我一般会在 MOSI、SCK、NSS 三根线上各串一个 1k 电阻限流,成本几毛钱,比换芯片便宜。如果是 3.3V 的 STC 型号(比如 STC15W 系列),那就直接对接,省掉这一层。

天线部分没什么可调的,模块上的 TX1/TX2 和匹配电容出厂已经调好,别手贱去拆。真正影响读距的是周围环境和 RFCfgReg 里的接收增益,这个后面再说。

2.2 软件模拟 SPI:8 个循环把时序钉死

RC522 支持 SPI 模式 0(CPOL=0,CPHA=0),高位先行,一次 8 位。翻译成操作就是:时钟空闲为低,数据在时钟低电平期间准备好,从机在上升沿采样;主机在时钟高电平期间读 MISO。

/* 引脚集中定义,换板子只改这一处 */ sbit RC522_SCL = P1^0; sbit RC522_MOSI = P1^1; sbit RC522_MISO = P1^2; sbit RC522_NSS = P1^3; sbit RC522_RST = P1^4; /* 全双工:发一个字节的同时收一个字节 */ unsigned char SPI_WriteByte(unsigned char dat) { unsigned char i; unsigned char recv = 0; for (i = 0; i < 8; i++) { RC522_SCL = 0; /* 时钟拉低,摆数据 */ if (dat & 0x80) /* 先发最高位 */ RC522_MOSI = 1; else RC522_MOSI = 0; dat <<= 1; RC522_SCL = 1; /* 上升沿,RC522 采样 MOSI */ recv <<= 1; if (RC522_MISO) /* 高电平期间读回 MISO */ recv |= 0x01; } RC522_SCL = 0; /* 收尾把时钟放回空闲低 */ return recv; }

参数dat是待发送的字节,返回值是同一时刻从 MISO 上读回来的字节。SPI 是全双工,所以写寄存器时第二个字节随便发什么都行,而读寄存器时必须发一个哑字节 0xFF 把数据“顶”出来——这一点新手最容易卡住,觉得读操作凭啥还要写。

速度上不用太担心。11.0592MHz 晶振、12T 模式下,这个循环一轮大概 8~10 个机器周期,折算下来 SPI 时钟不到 1MHz,而 RC522 的 SPI 上限是 10Mbps,余量很大。但如果你换成了 1T 的 STC 且主频拉到 35MHz,那最好在 SCL 拉高后插一两个_nop_()。这种问题很玄学:实验室里跑一天没事,装到现场偶发读错,示波器一挂才看得见时钟毛刺。

2.3 复位、软复位与版本寄存器自检

上电之后别急着寻卡,先把芯片叫醒并确认它到底在不在。这套资料里的初始化顺序是硬复位、软复位、配定时器、开天线,一步都不能省。

void RC522_Init(void) { RC522_RST = 0; /* 硬复位,至少保持 10ms */ DelayMs(10); RC522_RST = 1; DelayMs(50); /* 等内部振荡器稳定 */ RC522_WriteReg(CommandReg, 0x0F); /* SoftReset */ while (RC522_ReadReg(CommandReg) & 0x10); /* 等 PowerDown 位自清 */ /* 定时器:预分频 0x0D3E = 3390,13.56MHz/(2*3390+1) ≈ 2kHz */ RC522_WriteReg(TModeReg, 0x8D); /* 低 4 位是预分频高位 */ RC522_WriteReg(TPrescalerReg, 0x3E); RC522_WriteReg(TReloadRegH, 0x00); RC522_WriteReg(TReloadRegL, 30); /* 30/2kHz ≈ 15ms 超时 */ RC522_WriteReg(TxASKReg, 0x40); /* M1 卡必须 100% ASK */ RC522_WriteReg(ModeReg, 0x3D); /* CRC 初值 0x6363 */ RC522_WriteReg(RFCfgReg, 0x70); /* 接收增益 48dB */ RC522_AntennaOn(); /* TxControlReg 低两位置 1 */ }

几个参数值得单独说。TModeReg = 0x8D的低 4 位是预分频高 4 位,和TPrescalerReg拼起来才是完整的 12 位预分频值,很多人只改低 8 位,结果超时时间对不上,程序在等中断的地方卡死。TxASKReg = 0x40是 M1 卡能不能被寻到的关键,默认调制方式不对的话,卡贴到天线上也只会返回“无卡”。ModeReg = 0x3D把 CRC 预设值设成 0x6363,M1 卡的 CRC_A 就是这个初值,设错会导致选卡永远失败。

初始化完补一句自检,能省掉后面 80% 的排查时间:

unsigned char RC522_CheckVersion(void) { unsigned char v = RC522_ReadReg(VersionReg); /* 地址 0x37 */ if (v == 0x91 || v == 0x92) return 1; /* 原厂片 */ if (v == 0x12) return 2; /* 兼容片,能用但增益偏低 */ return 0; /* 0x00 或 0xFF,通信没通 */ }

读回 0x00 和 0xFF 是两种不同的病:0x00 多半是 MISO 一直为低(线没接、或者 P1 口没先写 1),0xFF 多半是 NSS 没拉高或者模块根本没供电。这个判断比拿万用表一根根量快得多。

3. 驱动层拆解:从寄存器读写到寻卡选卡的完整链路

3.1 地址移位:RC522 的 SPI 地址字节规则

RC522 的寄存器地址是 6 位的,但 SPI 上发出去的地址字节是 8 位:最高位是读写标志(1 读 0 写),中间 6 位是地址,最低位固定 0。所以写寄存器发(addr << 1) & 0x7E,读寄存器再或上 0x80。

void RC522_WriteReg(unsigned char addr, unsigned char val) { RC522_NSS = 0; /* 片选拉低,一帧开始 */ SPI_WriteByte((unsigned char)((addr << 1) & 0x7E)); /* 写:bit7 = 0 */ SPI_WriteByte(val); RC522_NSS = 1; /* 帧结束必须拉高 */ } unsigned char RC522_ReadReg(unsigned char addr) { unsigned char val; RC522_NSS = 0; SPI_WriteByte((unsigned char)(((addr << 1) & 0x7E) | 0x80)); /* 读:bit7 = 1 */ val = SPI_WriteByte(0xFF); /* 哑字节顶出数据 */ RC522_NSS = 1; return val; } void RC522_SetBit(unsigned char addr, unsigned char mask) { RC522_WriteReg(addr, RC522_ReadReg(addr) | mask); } void RC522_ClearBit(unsigned char addr, unsigned char mask) { RC522_WriteReg(addr, RC520_ReadReg(addr) & (unsigned char)~mask); }

RC522_SetBit和RC522_ClearBit是读改写,看着简单,但它们是后面 FIFO 清空、启动发送、开天线的通用手段。注意RC522_NSS必须在每次寄存器访问的末尾拉高,不能一口气连着读好几个寄存器只拉高一次——RC522 靠 NSS 的上升沿判定一帧结束,不拉高会出现地址错位,读回来的值全是对的但位置全错,这种 bug 查起来非常难受。

3.2 PcdComMF522:一条命令从 FIFO 到天线的全过程

所有跟卡交互的动作最终都汇到这一个函数里:把命令字塞进 FIFO,启动发送,等中断,再从 FIFO 把应答捞出来。它是整个驱动的中枢。

#define PCD_IDLE 0x00 #define PCD_AUTHENT 0x0E #define PCD_TRANSCEIVE 0x0C #define PCD_CALCCRC 0x03 #define MI_OK 0 #define MI_NOTAGERR 1 #define MI_ERR 2 unsigned char PcdComMF522(unsigned char cmd, unsigned char *pIn, unsigned char inLen, unsigned char *pOut, unsigned int *pOutLen) { unsigned char status = MI_ERR; unsigned char irqEn = 0x00; unsigned char waitFor = 0x00; unsigned char n, lastBits; unsigned int i; switch (cmd) { case PCD_AUTHENT: /* 认证:等空闲中断或错误中断 */ irqEn = 0x12; waitFor = 0x10; break; case PCD_TRANSCEIVE: /* 收发:等接收完成或空闲 */ irqEn = 0x77; waitFor = 0x30; break; default: return MI_ERR; } RC522_WriteReg(ComIEnReg, irqEn | 0x80); /* bit7 是中断总使能 */ RC522_ClearBit(ComIrqReg, 0x80); /* 清 Set1 标志 */ RC522_WriteReg(ComIrqReg, 0x7F); /* 清全部中断标志位 */ RC522_WriteReg(CommandReg, PCD_IDLE); RC522_SetBit(FIFOLevelReg, 0x80); /* FlushBuffer,清空 FIFO */ while (inLen--) /* 命令字 + 参数进 FIFO */ RC522_WriteReg(FIFODataReg, *pIn++); RC522_WriteReg(CommandReg, cmd); if (cmd == PCD_TRANSCEIVE) RC522_SetBit(BitFramingReg, 0x80); /* StartSend,启动发射 */ i = 2000; /* 软件超时,防死等 */ do { n = RC522_ReadReg(ComIrqReg); } while ((--i) && !(n & 0x01) && !(n & waitFor)); RC522_ClearBit(BitFramingReg, 0x80); /* 关掉 StartSend */ if (i == 0) /* 硬件没响应,直接退出 */ return MI_ERR; if (RC522_ReadReg(ErrorReg) & 0x1B) /* 溢出/奇偶/协议错误 */ return MI_ERR; if (n & waitFor) { status = MI_OK; if (n & irqEn & 0x01) status = MI_NOTAGERR; /* 定时器到点,没卡应答 */ if (cmd == PCD_TRANSCEIVE) { lastBits = RC522_ReadReg(ControlReg) & 0x07; /* 最后一字节有效位数 */ n = RC522_ReadReg(FIFOLevelReg); *pOutLen = lastBits ? ((n - 1) * 8 + lastBits) : (n * 8); if (n == 0) n = 1; if (n > 16) n = 16; /* FIFO 只有 64 字节,保护一下 */ for (i = 0; i < n; i++) pOut[i] = RC522_ReadReg(FIFODataReg); } } else { status = MI_ERR; } return status; }

irqEn和waitFor是两个位掩码,前者决定打开哪些中断源,后者决定等哪些标志才算结束。认证命令等的是 IdleIRq(0x10),收发命令等的是 RxIRq(0x20)加 IdleIRq。i = 2000那句软件超时是血泪经验:早期版本没有它,模块一旦没接触好,程序就永久停在这个 do-while 里,看门狗都救不回来。

*pOutLen算的是有效位数而不是字节数,因为寻卡返回的是 16 位(2 字节),选卡返回 24 位(3 字节),读写块返回 128 位(16 字节),全用位来对齐才统一。

3.3 寻卡、防冲突、选卡:三帧握手

一张卡从进入射频场到被选中,要走三步:寻卡(REQA)、防冲突(Anticollision)、选卡(Select)。这三步走完,你才算真正拿到了一张卡的 UID。

/* 第一步:寻卡,req_code = 0x26 寻空闲卡,0x52 唤醒休眠卡 */ unsigned char PcdRequest(unsigned char req_code, unsigned char *tagType) { unsigned char status; unsigned int backBits = 0; RC522_WriteReg(BitFramingReg, 0x07); /* 只发 7 位,短帧 */ tagType[0] = req_code; status = PcdComMF522(PCD_TRANSCEIVE, tagType, 1, tagType, &backBits); if ((status != MI_OK) || (backBits != 0x10)) /* ATQA 一定是 16 位 */ status = MI_ERR; return status; } /* 第二步:防冲突,拿回 4 字节 UID + 1 字节 BCC */ unsigned char PcdAnticoll(unsigned char *snr) { unsigned char status, i, bcc = 0; unsigned int backBits = 0; RC522_WriteReg(BitFramingReg, 0x00); /* 冲突检测用整字节 */ snr[0] = 0x93; /* 防冲突命令字 */ snr[1] = 0x20; status = PcdComMF522(PCD_TRANSCEIVE, snr, 2, snr, &backBits); if (status != MI_OK) return status; for (i = 0; i < 4; i++) bcc ^= snr[i]; /* BCC 是 UID 四字节异或 */ if (bcc != snr[4]) status = MI_ERR; return status; } /* 第三步:选卡,UID + BCC + CRC 一起发过去 */ unsigned char PcdSelect(unsigned char *snr) { unsigned char status, i, buf[9]; unsigned int backBits = 0; RC522_WriteReg(BitFramingReg, 0x00); buf[0] = 0x93; buf[1] = 0x70; for (i = 0; i < 5; i++) buf[i + 2] = snr[i]; /* UID + BCC */ PcdCalcCRC(buf, 7); /* 后两字节填 CRC_A */ status = PcdComMF522(PCD_TRANSCEIVE, buf, 9, buf, &backBits); if ((status == MI_OK) && (backBits == 0x18)) /* SAK 是 24 位 */ return MI_OK; return MI_ERR; }

PcdCalcCRC走的是芯片自带的 CRC 协处理器:把数据写进 FIFO,发PCD_CALCCRC命令,等DivIrqReg的 bit2(CRCIRq)置位,再从CRCResultRegL/H读回两个字节。这比软件查表算 CRC 快得多,也少写一堆代码。

PcdRequest里BitFramingReg = 0x07那句别漏,REQA 是 7 位短帧,不是 8 位。设成整字节发出去,卡是能收到,但协议上不合法,某些卡会直接不应答——这类“有的卡能读有的卡不能读”的怪现象,八成就是这里。

3.4 主循环:把卡号从串口吐出来

三帧走完就拿到了 4 字节 UID,剩下的事就是格式化输出。资料里的主流程很朴素:初始化串口(9600、8N1),初始化 RC522,然后死循环寻卡。

void main(void) { unsigned char tagType[2], uid[5], i; UART_Init(); /* 9600, 8, N, 1 */ RC522_Init(); if (RC522_CheckVersion() == 0) { UART_SendString("RC522 not found\r\n"); while (1); } UART_SendString("RC522 ready\r\n"); while (1) { if (PcdRequest(0x52, tagType) == MI_OK) { /* 0x52 能唤醒休眠卡 */ if (PcdAnticoll(uid) == MI_OK) { if (PcdSelect(uid) == MI_OK) { UART_SendString("UID:"); for (i = 0; i < 4; i++) { UART_SendHex(uid[i]); /* 两位十六进制 */ UART_SendByte(' '); } UART_SendString("\r\n"); PcdHalt(); /* 让卡休眠,避免重复读 */ } } } DelayMs(100); /* 别把 CPU 跑满 */ } }

PcdHalt()这一句很关键。M1 卡被选中后一直处于激活态,如果不发 HALT 命令,下一次寻卡它不会重新应答,表现就是“第一次能读,第二次读不到”。HALT 命令(0x50 0x00 + CRC)会让卡回到休眠,下次用 0x52 唤醒就行,这也是为什么主循环里用 0x52 而不是 0x26。

4. 卡片数据操作:M1 扇区结构、密钥认证与读写块

4.1 一张 M1 卡里到底存了什么

M1 卡(也就是常见的 IC 卡、门禁卡)容量 1K 字节,分成 16 个扇区,每个扇区 4 个块,每块 16 字节。第 0 扇区第 0 块是厂商固化区,存着 UID 和厂商数据,只读;每个扇区的第 3 块(块号 3、7、11……63)是密钥区,存密钥 A(6 字节)、访问控制位(4 字节)、密钥 B(6 字节),出厂默认通常是FF FF FF FF FF FF+FF 07 80 69+FF FF FF FF FF FF。

块号用途可否写
0厂商块,UID + BCC + SAK只读
1、2数据块可写(需认证)
3密钥区(密钥 A + 控制位 + 密钥 B)可写,写错整扇区报废
4~6第 1 扇区数据块可写
7第 1 扇区密钥区谨慎
……以此类推,共 16 扇区 64 块

访问控制位决定每个块(含密钥区自己)用密钥 A 还是密钥 B 认证、能读能写还是只能加减。出厂值FF 07 80 69的含义是:密钥 A 可读可写,数据块用密钥 A 认证后可读可写。搞门禁小项目,直接用出厂密钥 A 认证就够,别轻易动控制位。

4.2 认证、读块、写块:三道必过的关

读任何一个数据块之前,必须先对该块所在扇区做一次密钥认证,认证通过后Status2Reg的 bit3(MFCrypto1On)会置 1,之后同扇区的块可以直接读,换扇区要重新认证。

/* 认证:mode = 0x60 用密钥 A,0x61 用密钥 B */ unsigned char PcdAuthState(unsigned char mode, unsigned char block, unsigned char *key, unsigned char *uid) { unsigned char status, buf[12], i; unsigned int backBits = 0; buf[0] = mode; buf[1] = block; for (i = 0; i < 6; i++) buf[2 + i] = key[i]; /* 6 字节密钥 */ for (i = 0; i < 4; i++) buf[8 + i] = uid[i]; /* 4 字节 UID */ status = PcdComMF522(PCD_AUTHENT, buf, 12, buf, &backBits); /* 光看返回值不够,必须确认加密单元真的打开了 */ if ((status != MI_OK) || !(RC522_ReadReg(Status2Reg) & 0x08)) status = MI_ERR; return status; } /* 读块:返回 16 字节 */ unsigned char PcdReadBlock(unsigned char block, unsigned char *data) { unsigned char status, buf[4]; unsigned int backBits = 0; buf[0] = 0x30; /* MIFARE READ */ buf[1] = block; PcdCalcCRC(buf, 2); /* 补 CRC_A,凑够 4 字节 */ status = PcdComMF522(PCD_TRANSCEIVE, buf, 4, data, &backBits); if ((status != MI_OK) || (backBits != 0x80)) /* 16 字节 = 128 位 */ status = MI_ERR; return status; } /* 写块:16 字节数据 + 2 字节 CRC,等 4 位 ACK */ unsigned char PcdWriteBlock(unsigned char block, unsigned char *data) { unsigned char status, buf[18], i; unsigned int backBits = 0; buf[0] = 0xA0; /* MIFARE WRITE */ buf[1] = block; for (i = 0; i < 16; i++) buf[2 + i] = data[i]; PcdCalcCRC(buf, 18); status = PcdComMF522(PCD_TRANSCEIVE, buf, 20, buf, &backBits); if ((status != MI_OK) || (backBits != 0x04)) return MI_ERR; if ((buf[0] & 0x0F) != 0x0A) /* ACK 是 4 位 1010b */ return MI_ERR; return MI_OK; }

PcdAuthState里那句Status2Reg & 0x08是很多人漏掉的。认证命令返回 MI_OK 只代表“命令发出去了”,不代表“认证成功了”,真正的判据是加密单元有没有打开。少了这行,密钥错的时候你会在读块那一步收到一堆乱码,然后往通信线路上找问题,白折腾半天。

写块的 ACK 判断也要注意:返回长度是 4 位不是 8 位,backBits要等于 4;数据在buf[0]的低 4 位,等于 0x0A 才是成功,其余值(0x00、0x04、0x05)都是 NAK,代表目标块不可写、密钥权限不够或者卡已经移开。

4.3 卡号落地:白名单、防重复读与掉电保存

读出

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 11:05:56

C# Modbus TCP与S7-1200通讯:报文、配置与避坑

简介&#xff1a;面向工业自动化上位机开发者的C#通信实践包&#xff0c;围绕ModbusTcp协议与西门子1200 PLC的数据交互展开&#xff0c;适合已具备C#基础、希望打通以太网读写线圈、离散输入、保持寄存器与输入寄存器的开发者参考。压缩包共46个文件、约13.02MB&#xff0c;以…

作者头像 李华
网站建设 2026/10/26 11:04:15

OffsetTimeBaseProvider:分布式系统时间同步的核心技术

1. 时间同步与OffsetTimeBaseProvider概述 在分布式系统和工业自动化领域&#xff0c;精确的时间同步是确保系统可靠性的基石。OffsetTimeBaseProvider作为时间同步架构中的关键组件&#xff0c;负责处理时间基准的偏移补偿问题。简单来说&#xff0c;它就像一位专业的"时…

作者头像 李华
网站建设 2026/10/26 11:03:30

Docker部署Hadoop+Spark集群:环境一致性与可扩展架构实践

1. 为什么非得用Docker跑Hadoop和Spark集群&#xff1f;——从三台物理机到一个终端的实操逻辑 我第一次在某高校实验室带学生做大数据课程设计时&#xff0c;被一个问题卡了整整两天&#xff1a;A同学的Mac上Hadoop伪分布式环境跑得好好的&#xff0c;B同学的Windows笔记本却连…

作者头像 李华
网站建设 2026/10/26 11:03:29

QOpenGLWidget显示QImage:纹理链路与MVP交互实战

简介&#xff1a;面向Qt图形开发者的QOpenGLWidget图像显示与变换示例工程&#xff0c;围绕将QImage上传为OpenGL纹理并完成显示、缩放、旋转、平移等交互展开&#xff0c;适合已掌握Qt基础、希望从QPainter转向GPU渲染的初中级开发者&#xff0c;也可作为图像查看器原型的起步…

作者头像 李华
网站建设 2026/10/26 11:01:34

Anaconda数据恢复实战:从原理到环境重建

1. Anaconda数据恢复全攻略&#xff1a;从原理到实践 作为一名长期使用Anaconda进行Python开发的工程师&#xff0c;我深知数据丢失带来的痛苦。上周同事误删了整个conda环境目录&#xff0c;导致三个正在进行的项目环境全部丢失。幸好我们掌握了完整的数据恢复方案&#xff0c…

作者头像 李华
网站建设 2026/10/26 10:56:47

Delphi XE10.3 原生 DOCX 三件套实战:生成、报表与读取

简介&#xff1a;Axolot DOCXSuit 是面向 Delphi XE10.3 Rio 开发者的 DOCX 文档处理组件套件&#xff0c;适合需要在桌面应用中集成 Word 文档生成、报表设计与读写能力的开发者&#xff0c;无需依赖 Microsoft Office 即可完成文档操作。套件由 AXWWriter、AXWReports 与 DOC…

作者头像 李华