news 2026/10/26 19:54:56

Mifare Classic协议分析:从寻卡防冲突到Crypto1认证与数据解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Mifare Classic协议分析:从寻卡防冲突到Crypto1认证与数据解析

简介:面向Android与物联网开发者的Mifare卡协议分析及NFC读卡实践资料,围绕MF卡的SELECT、AUTHENTICATE、READ/WRITE命令和扇区密钥机制展开,配套可运行的工程与调试素材,适合需要把卡片认证、块读写落到代码中的初中级开发者。压缩包共245个文件,约10.12MB,以xml布局与配置、java源码、txt说明、png截图、json配置和bin数据为主,另有cmake/ninja构建产物、so库与gradle脚本,兼顾源码、编译缓存和测试安装包,便于对照排查环境问题。目前已有454人学习。读者可获得NfcAdapter读卡模式注册、MifareClassic连接认证与块读写、异常处理和交互提示等实现参考,并借助协议解析材料梳理扇区、密钥与访问控制关系,形成从协议理解到Android端读卡验证的学习样本。

1. 拿一张 MF 卡贴到读卡器上时,屏幕跳出一串十六进制,看起来像“把内容读出来了”。真实发生的过程却要复杂得多:整场对话里,卡片从来没有主动发送过任何一个字节,它只是根据 13.56 MHz 载波的强弱,反复开合自己的天线负载。做 MF卡协议分析,本质上分析的就是这套被动式半双工规则——读卡器供电、读卡器定节拍、读卡器决定下一句问什么,卡片唯一的自由,是在载波上“敲”出 0 和 1。

之所以值得把这层扒开,是因为从“贴上去”到“拿到 16 字节”,中间横着四道关:寻卡、防冲突、选卡、三次认证。每一道关都对应一个具体的数据结构和一个具体的失败码。分不清是哪一道没通过,就只能对着“读卡失败”这四个字反复换设备、换手机、换姿势——最后归因成玄学。

2. 寻卡与防冲突:ATQA、SAK 和 UID 如何把“一堆卡”统一成“一张卡”

2.1 从射频场到 ATQA:卡片是怎么“开口”的

读卡器只要上电,天线就会持续输出未经调制的载波,这叫射频场(RF field)。场里没有卡时,一切都是安静的;一旦有一张卡进入场区,卡片内部的整流电路从载波里“取电”,芯片上电复位,然后进入等待状态。

注意这个顺序:卡片是先被供电,然后才能说话。所以读卡器永远不知道场里什么时候多了卡——它只能定期喊。这一声喊就是 REQA,一个 7 位的短帧,值0x26(二进制0100110)。之所以用 7 位而不是 8 位,是因为短帧没有校验位、没有帧尾,长度固定,可以用最省电、最快的方式发出去。

场里所有处于 IDLE 状态的卡片收到 REQA 后,会一起回一个 2 字节的应答,叫 ATQA(Answer To Request type A)。Mifare Classic 1K 的 ATQA 是0x0004,4K 是0x0002,Mini 是0x0004但 SAK 不同。ATQA 的位域里编码了两件事:UID 的长度(单倍/双倍/三倍),以及这张卡是否支持按位防冲突。

工程上有个很常见的误解:看到 ATQA 就以为已经“找到卡”了。其实 ATQA 只是一句“我在”,此时卡片还在 IDLE 状态,既没有 UID 被选定,也没有进入可以认证的 ACTIVE 状态。直接跳过后续步骤去发认证命令,卡片根本不会理你——这就是很多人第一次写读卡程序时遇到的“ATQA 有了但什么都读不到”。

除了 REQA,还有一个 WUPA,值0x52。区别在于 REQA 只能唤醒 IDLE 状态的卡,而 WUPA 能唤醒 IDLE和HALT 状态的卡。HALT 是卡片被显式“挂起”后的状态,用 HLTA 命令(0x50 0x00)进入。如果你的程序里做过一次选卡又没有复位,再发 REQA 是没反应的,得用 WUPA,或者干脆断一下场重新上电。

2.2 防冲突:位碰撞与 CL1/CL2/CL3 的级联

场里只有一张卡时,读卡器发什么它答什么。但现实是场里可能有三张卡——一张门禁卡、一张公交卡、一张贴着玩的空白卡。三张卡同时应答,载波上的负载调制会叠加,读卡器收到的是三个 UID 的按位“或”,这就是碰撞。

ISO14443-A 的解法不是让卡随机退避(那是以太网的做法),而是让读卡器主导逐位仲裁。读卡器发一条 SEL 命令,格式是SEL + NVB,其中 SEL 是级联标号:0x93对应 CL1,0x95对应 CL2,0x97对应 CL3;NVB(Number of Valid Bits)告诉卡片“我这次要你回多少位”。

# 构造 SEL 帧:请求完整 UID(NVB = 0x20,表示 SEL 和 NVB 两个字节都有效) def sel_frame(cascade_level: int, nvb: int = 0x20) -> bytes: # cascade_level 取 0x93 / 0x95 / 0x97,对应单倍/双倍/三倍 UID return bytes([cascade_level, nvb]) # 卡片应答 5 字节:4 字节 UID 片段 + 1 字节 BCC 校验 # BCC = UID0 ^ UID1 ^ UID2 ^ UID3,任何一位翻转都会导致校验失败 def check_bcc(resp: bytes) -> bool: if len(resp) != 5: return False return (resp[0] ^ resp[1] ^ resp[2] ^ resp[3]) == resp[4]

NVB = 0x20的含义是:SEL 和 NVB 这两个字节是“有效位”,剩下的位让卡片自己填。此时场里所有卡同时回 5 个字节,读卡器逐位比较,遇到第一个分歧位就停下来,把“已知前缀 + 分歧位 + 1”写进 NVB,重发一次。NVB 的编码规则是:高 4 位表示完整字节数,低 4 位表示额外的位数。所以0x20是 2 个完整字节、0 个额外位;0x25是 2 个完整字节加 5 位;一直到0x70,表示 7 个字节全都要。

这套机制叫按位防冲突,它的代价是收敛轮数取决于 UID 的分布。两张 UID 只差最后一位的卡,要走到第 32 轮才分得开。实际读卡器固件通常会在 NVB 推进时做优化,遇到已经唯一的前缀就一次性要完整 UID,这也是为什么有的读卡器读多卡很快,有的慢得像卡住。

BCC 那个字节容易被忽略,但它是排错的关键。BCC 是 4 个 UID 字节的异或,如果读回来的 5 个字节异或结果不为 0,说明传输过程中有误码,必须重来。很多“读到 UID 但时对时错”的问题,根子就在天线调谐不好导致的误码,而不是协议栈写错了。

双倍 UID 的卡还有一个细节:第一级应答是0x88 + UID0 + UID1 + UID2 + BCC,那个0x88叫 CT(Cascade Tag),是告诉读卡器“我还没说完,请继续发下一级 SEL”。读卡器看到 CT 就要发0x95,而不是把这 5 个字节当成最终 UID。多级级联最多到 CL3,也就是 10 字节 UID。

2.3 SAK 决定后续识别路径

选卡的最后一步,读卡器发SEL + NVB(0x70)加 4 字节 UID 加 BCC,卡片回一个 3 字节的 SAK(Select Acknowledge)。SAK 是最容易被低估的一个字节,它直接决定了你后面该用哪套命令集。

SAK 值卡类型后续命令
0x08Mifare Classic 1K0x60/0x61 认证 + 0x30 读块
0x18Mifare Classic 4K同上,但块号范围到 255
0x09Mifare Mini同上,扇区数更少
0x20符合 ISO14443-4 的卡进入 RATS,走 APDU
0x00只回 UID,不声明类型需要自己猜

这张表解释了一个高频困惑:为什么同样的读卡代码,读 A 卡正常,读 B 卡就返回一串0x04的 NAK。因为 B 卡的 SAK 是0x20,它走的是 T=CL 的 APDU 通道,你拿 Mifare Classic 的块读命令0x30去问它,它当然听不懂。协议分析的第一步不是看数据,是看 SAK。

3. Crypto1 认证:为什么知道密钥也可能读不动,不知道密钥却可能读得动

3.1 三次认证的四个报文

选卡成功之后,卡片进入 ACTIVE 状态,此时它只认两类命令:认证和读写。认证命令是0x60(用 Key A 认证)或0x61(用 Key B 认证),后面跟一个字节的绝对块号——注意是块号,不是扇区号,这是新手最容易搞混的地方之一。

整个认证过程是四个报文:

  1. 读卡器 → 卡:0x60+ 块号
  2. 卡 → 读卡器:nt,4 字节明文随机数
  3. 读卡器 → 卡:nr ⊕ ks1(4 字节)、ar ⊕ ks2(4 字节)
  4. 卡 → 读卡器:at ⊕ ks3(4 字节)

四个报文走完,双方各自握有nt / nr / ar / at四个 32 位随机数和那把 48 位的密钥,同时初始化 Crypto1 状态机。此后这条链路上所有的数据都被密钥流异或,包括你发的读块命令和卡片返回的 16 字节。所以从 wire 上看,读块返回的从来不是明文。

这里有个设计上的关键点:认证是按扇区生效的,不是按卡。你用 Key A 认证了扇区 3,就只能读扇区 3 的块;要读扇区 4,得再走一遍完整的四次报文。这也是为什么读一整张 1K 卡(16 个扇区)要发 16 次认证,每次都会消耗几十毫秒——不是程序写得慢,是协议就这么设计的。

3.2 Crypto1 的结构:48 位 LFSR 与非线性滤波

Crypto1 的骨架是一个 48 位的线性反馈移位寄存器(LFSR),加上一个从 20 个抽头取值的非线性滤波函数 f。每来一个时钟,f 从当前状态算出一个输出位,这个位既是密钥流的一位,同时又参与控制 LFSR 是否推进——这种时钟控制是为了打乱纯粹的线性关系,让它看起来不像个纯 LFSR。

密钥 48 位,加上四个 nonce 一共 96 位,装进状态里。听起来密钥空间是 2^48,暴力搜也不现实。真正的问题不在密钥长度,而在结构:

  • 非线性滤波函数 f 只有 20 个输入抽头,而 LFSR 是 48 位。这意味着 f 的输出对状态的“可见范围”是受限的,存在可被利用的相关性。
  • 四个 nonce 里,nt是明文发出的,只有 32 位。32 位熵意味着同一张卡在一次会话内的随机性极其有限。
  • 认证成功后,密钥流仍在继续,读卡器发的每一条命令、卡片回的每一块数据都是明文异或同一个密钥流。相当于给了攻击者大量已知明文-密文对。

这三条叠在一起,就构成了后来一系列密钥恢复方法的理论基础。机制上的失守点非常清晰:Crypto1 把“机密性”和“认证”绑在同一把 48 位密钥上,而认证过程本身又主动泄露了 32 位状态。一旦攻击者能观测到足够多的(nonce, 密钥流)对,48 位状态就可以通过分区穷举把复杂度压到工程可承受的量级。

从设计取舍的角度看,1994 年的这张卡面对的场景是门禁和公交:卡片成本要压到几毛钱,芯片面积、功耗、认证延迟都是硬约束。48 位密钥在当时不算离谱。真正的问题是这套设计沿用了二十年,威胁模型早就变了。

常见的实现思路里,恢复密钥的路径大致分三种。第一种最简单:试默认密钥。大量出厂未改的卡,六个扇区密钥全是FF FF FF FF FF FF,尾块访问位是FF 07 80 69。第二种叫嵌套认证——先用已知密钥认证一个扇区,把卡片玩到“状态已知”,再对未知扇区发起认证,此时卡片回的nt被已知密钥流加密,一异或就出明文。拿到明文 nonce 后,48 位密钥可以分段搜。第三种针对随机数质量好的卡,需要收集同一张卡上大量不同 nonce 的样本,靠的是滤波函数输出与 LFSR 状态之间的相关性做统计区分。

要强调的是,这些手段只应该在你拥有或获得明确授权的卡片和系统上使用。协议分析的价值在于理解机制边界,不在于拿到别人系统的通行证。

3.3 尾块布局:Key A / 访问位 / Key B 的 16 字节怎么分

每个扇区的最后一块(1K 卡是每扇区的第 3 块,4K 卡的后 8 个扇区是第 15 块)是尾块,结构和普通数据块完全不同:

字节 0..5 : Key A(6 字节,读永远返回全 0) 字节 6..8 : 访问位 Access Bits(3 字节,带反码冗余) 字节 9 : General Purpose Byte / 用户字节 字节 10..15 : Key B(6 字节,可配置为可读)

访问位这 3 个字节的编码是整张卡里最反直觉的地方。它用 3 个位(C1/C2/C3)描述一个块的权限,每个位有 4 份(对应扇区内 4 个块),再加上反码做冗余校验,塞进 3 个字节:

def parse_access_bits(b6: int, b7: int, b8: int) -> dict: """按 MF1S50 数据手册的布局解出 C1/C2/C3""" # b7 高 4 位 = C1_3..C1_0 c1 = (b7 >> 4) & 0x0F # b8 低 4 位 = C2_3..C2_0 c2 = b8 & 0x0F # b8 高 4 位 = C3_3..C3_0 c3 = (b8 >> 4) & 0x0F # 冗余校验:反码必须对得上,否则说明尾块被写坏了 if ((~c2) & 0x0F) != ((b6 >> 4) & 0x0F): raise ValueError("C2 反码不匹配,尾块可能损坏") if ((~c1) & 0x0F) != (b6 & 0x0F): raise ValueError("C1 反码不匹配,尾块可能损坏") if ((~c3) & 0x0F) != (b7 & 0x0F): raise ValueError("C3 反码不匹配,尾块可能损坏") return {"C1": c1, "C2": c2, "C3": c3}

拿出厂默认值FF 07 80代进去:c1 = (0x07 >> 4) & 0x0F = 0,c2 = 0x80 & 0x0F = 0,c3 = (0x80 >> 4) & 0x0F = 8,即C3_3 = 1、其余为 0。查手册,C1C2C3 = 001对应的正是出厂传输配置:Key A 永不可读、访问位可由 A 或 B 写、Key B 可读但不可写。

数据块的权限表则是另一套:

C1C2C3读写加减
000A 或 BA 或 BA 或 B
010A 或 B永不可永不可
100A 或 BB永不可
110A 或 BBB
001A 或 B永不可A 或 B
011BB永不可
101B永不可永不可
111永不可永不可永不可

111那一行是真实存在的——整块彻底锁死,密钥都对也没用,这种卡叫“自杀卡”。见过有人把访问位写成FF 07 80之外的组合,把整块锁死,血泪经验就是:写尾块之前先把原值备份,Mifare Classic 没有后悔药。

4. 块结构与数据布局:16 个字节里到底住着什么

4.1 三种块语义

Mifare Classic 的存储单元固定是 16 字节一块。1K 卡是 16 个扇区 × 4 块 = 64 块,4K 卡是 32 个扇区 × 4 块 + 8 个扇区 × 16 块 = 256 块。块号在 4K 卡上是不连续的——前 32 个扇区每扇区 4 块,编号 0..127;后 8 个扇区每扇区 16 块,编号 128..255。这个跳变是 4K 卡读乱的主要来源。

同样叫“块”,语义有三种:

  • 厂商块:只有块 0。前 4 字节是 UID,第 5 字节是 BCC,第 6 字节是 SAK,第 7、8 字节是 ATQA,剩下是厂商数据。出厂只读,改了就废。
  • 数据块:普通存储,读写都行,也可能被配成值块。
  • 尾块:每扇区最后一块,存密钥和权限。

块 0 值得单独说一句。很多程序把块 0 当普通数据读,读出来发现前几个字节跟 UID 对得上,就以为“这块能随便写”。实际上块 0 的写保护是硬件级的,即使认证通过,写命令也会返回 NAK。有些所谓的“UID 可写卡”,是在硅片层面做了特殊处理,属于另一类产品,不在标准协议的讨论范围内。

4.2 值块:取反备份为什么能防写坏

值块(Value Block)是一个很有意思的设计。它的 16 个字节不是随便排的:

字节 0..3 : 值(小端) 字节 4..7 : 值的按位取反 字节 8..11 : 值(再来一遍,冗余) 字节 12 : 地址字节 addr 字节 13 : addr 的取反 字节 14 : addr 字节 15 : addr 的取反

同一个值存了两遍,其中一遍是反码;地址存了两遍,也都是反码。卡片在执行 increment / decrement 时,会先把三份值都校验一遍,任何一份对不上就拒绝操作。这个设计针对的是写过程中掉电:如果写到一半断电,三份副本里至少有一份是完整的旧值,卡片复位后能据此判断并回滚。

代价是值块只能存 4 字节有符号数,而且一旦被当成普通数据块写坏了,就再也回不到值块语义——除非你手动把 16 个字节按上面的格式重建一遍。

实际项目里,值块常用于电子钱包类的应用:扣款走 decrement,充值走 increment,转账走 restore + transfer。transfer 那一步是把值块内容写回同扇区的目标数据块,本质上是一次原子搬运。

4.3 NDEF 与 TLV:为什么手机能读出 URL

手机贴上去直接弹出网址,这个体验背后是 NFC Forum 定义的一套封装。Mifare Classic 被当作 Type 2 风格的标签使用时,存储器起始处会放一个能力容器(Capability Container),最前面两个字节固定是0xE1 0x10——0xE1是魔法字节,0x10表示版本 1.0。手机读卡器扫到这两个字节,就知道“这不是私有数据,是一条标准 NDEF 消息”。

容器后面跟着的是 TLV 序列:

TLV 类型含义
0x00NULL,填充用,跳过
0x01Lock Control TLV,描述锁定字节位置
0x02Memory Control TLV,描述可用内存区域
0x03NDEF Message TLV,真正的报文
0xFETerminator,结束标记

TLV 的格式是“类型 + 长度 + 值”。长度字段有个特殊规则:如果值长度小于 255,长度就是一个字节;如果是0xFF,后面跟两个字节表示真实长度。这个转义规则忘了处理,长 URL 就会解析失败。

def parse_tlv(data: bytes) -> bytes: """从标签存储区里抽出 NDEF 报文字节""" i = 0 while i < len(data): t = data[i] if t == 0x00: # NULL TLV,跳过 i += 1 continue if t == 0xFE: # 终止符,后面都是填充 break if i + 1 >= len(data): break length = data[i + 1] i += 2 if length == 0xFF: # 三字节长度转义 if i + 2 > len(data): break length = (data[i] << 8) | data[i + 1] i += 2 value = data[i:i + length] if t == 0x03: return value i += length return b""

拿到的 NDEF 报文本身还是分层的:头字节里有 MB(消息开始)、ME(消息结束)、CF(分片)、SR(短记录)、IL(ID 长度存在)、TNF(类型名格式);然后是类型长度、载荷长度、类型、载荷。一个 URL 记录的 TNF 通常是0x01(标准 URI),类型字段是字符U,载荷的第一个字节是 URI 前缀缩写码(0x04是http://,0x02是https://),后面才是剩下的字符串。

所以“手机读出一串网址”这件事,实际上经过了四层解封装:块 → 字节流 → TLV → NDEF 记录 → URI 前缀码 + 字符串。任何一层错位,出来的都是乱码。

5. 排查:读卡读不出来时,先分清是哪一层失败

5.1 现象:能收到 ATQA,选卡却超时或返回 NAK

这是最常见的“看起来快成功了”的失败。原因通常有三个:一是场里不止一张卡,防冲突没走完就去发 SEL+NVB(0x70),读卡器拿到的 UID 是拼凑的;二是 NVB 推进逻辑写错了,低 4 位的“额外位数”算成了字节数,导致下一轮请求的位数不对,卡片直接不应答;三是 BCC 校验没过却没重试,直接把错数据当成 UID 往下走。

解决办法是老老实实按位收敛:每次收到 5 字节先校验 BCC,失败就退回去重新发0x93 0x20,不要把错误数据带进下一轮。另外,双倍 UID 的卡要处理 CT 字节0x88,看到它就切到0x95,别当成 UID 的第一位。

5.2 现象:认证总是返回 0x04,密钥明明是对的

最可能的原因是块号传错了。认证命令后面跟的是绝对块号,不是扇区号。想认证扇区 5(1K 卡),块号应该是 20,不是 5。传 5 进去,实际认证的是扇区 1 的块 1,如果你的密钥是按扇区 5 配的,当然对不上。

第二个原因是 Key A / Key B 搞混。0x60是 A,0x61是 B。有些卡的尾块把 Key B 配成不可读,但认证仍然可以用 B——可读和可用是两回事,这一点坑过不少人。

第三个原因是认证成功后没有保持链路。每次认证都会重置 Crypto1 状态,如果程序里两条命令之间插了一次重新寻卡,状态就全丢了,后续读块必然是乱码。

5.3 现象:尾块 Key B 读出来永远是 000000000000

这不是 bug,是设计。Key A 在任何配置下都永远读不回来,读出来恒为 6 个 0;Key B 能不能读,取决于尾块访问位里C1C2C3那三位的组合,出厂默认001是“Key B 可读但不可写”。

想确认到底是“不可读”还是“真是 0”,得去看访问位的解码结果,而不是看返回值。如果解码出来 Key B 不可读,那读回的 6 个 0 就是占位符,没有任何信息量。

5.4 现象:同一张卡,手机能读、自己的读卡模块读不到

先怀疑天线。13.56 MHz 的天线是个谐振回路,匹配电容的偏差、周围金属的耦合、板子走线的寄生参数,都会让谐振点偏移。偏移之后,卡能取到电(所以能回 ATQA),但负载调制的深度不够,读卡器解不出后续的帧。表现就是“寻卡偶尔成功、选卡经常失败”。

其次是场强和命令间隔。卡片刚进场时内部电容还在充电,此时发命令容易被吞。稳妥的做法是上电后先等 5~10 ms 再发第一条 REQA,失败就重试三次,别一次不成就判定“无卡”。

还有一种情况是卡片停在 HALT 状态。上一轮程序结束时发了 HLTA 却没复位,下一轮用 REQA 叫不醒,得改用 WUPA。

5.5 现象:4K 卡读到第 32 扇区以后数据全是错的

因为块号在 4K 卡上不连续。前 32 个扇区(0~31)每扇区 4 块,绝对块号 =扇区号 × 4 + 块内偏移,范围 0~127。从扇区 32 开始,每扇区变成 16 块,块号从 128 起算,公式变成128 + (扇区号 - 32) × 16 + 块内偏移。

如果程序里统一用扇区号 × 4算块号,到扇区 32 就会算成块 128,而这恰好是扇区 32 的第 0 块——看起来对,但扇区 33 会算成 132,实际应该是 144,从此全错。写 4K 卡支持时,这个分段函数一定要单独测。

6. 设计取舍:Crypto1 的失败边界,与“够用就好”的代价

回头看 Crypto1,它其实不是被“攻破”的,是被时间攻破的。

1994 年的设计目标很明确:一张卡的成本要压到能贴在公交票上,认证延迟要短到人能无感通过闸机,密钥要能塞进当时有限的硅片面积。48 位 LFSR 加一个 20 抽头的非线性滤波,在当时的算力下是个合理的平衡点——它比明文强,比 RSA 便宜,比 DES 省电。

问题出在两个层面。

第一个层面是密码学直觉的局限。设计者把“不可预测”寄托在非线性滤波函数上,但非线性不等于安全。滤波器只用 20 个抽头,意味着 48 位状态里有 28 位的信息要绕很大一圈才能体现在输出上。这种结构上的相关性,在 1994 年没有被

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

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

MFC ListCtrl标题栏多行显示实现与DPI适配

简介&#xff1a;本资源是面向VC中级开发者与MFC界面定制需求者的实用控件扩展方案&#xff0c;聚焦解决标准ListView控件标题栏不支持自动换行的痛点问题——当列标题文本过长时&#xff0c;原生HeaderCtrl仅截断显示省略号&#xff0c;严重影响多列数据界面的信息可读性与用户…

作者头像 李华
网站建设 2026/10/26 19:53:31

DDD实践指南:如何避免领域驱动设计的常见误区

1. 当我们在谈论"DDD"时究竟在说什么十年前我第一次接触DDD&#xff08;领域驱动设计&#xff09;时&#xff0c;被那些充满诱惑力的承诺所吸引——清晰的领域边界、灵活的架构设计、可维护的代码结构。但现实往往比理想骨感得多&#xff0c;我见过太多团队把DDD做成…

作者头像 李华
网站建设 2026/10/26 19:52:23

BMI160 驱动开发:寄存器时序、FIFO 中断与官方驱动分层实践

简介&#xff1a;面向嵌入式与智能硬件开发者整理的 BMI160 六轴惯性测量单元开发资料包&#xff0c;整合博世官方驱动源码与数据手册&#xff0c;适合从事运动追踪、姿态检测、无人机、可穿戴及物联网产品开发的初中级工程师查阅。压缩包共 17 个文件&#xff0c;整体约 2.22M…

作者头像 李华
网站建设 2026/10/26 19:52:23

STM32F10x标准库3.6.0工程配置与调试排错指南

简介&#xff1a;STM32F10x标准库3.6.0最终版是面向STM32F10x系列单片机开发者的官方外设固件库合集&#xff0c;适合刚接触寄存器操作、希望用库函数快速完成外设初始化与工程搭建的嵌入式学习者及工程师&#xff0c;也可作为老项目维护与代码移植的参考资料。压缩包共收录960…

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

STM32智能小车循迹避障综合实验:状态机与PD控制

简介&#xff1a;围绕STM32智能小车的黑线循迹与红外避障综合实验&#xff0c;这套工程源码面向刚接触嵌入式控制、希望从电机驱动与传感器采集入手理解小车运动逻辑的学习者。代码以定时器配置电机为核心&#xff0c;封装前进、刹车、左转、左旋转、右转、右旋转、后退等基础运…

作者头像 李华
网站建设 2026/10/26 19:50:51

Pi 1.0 终端 Agent 框架解析:MCP 与 Pi Durable 如何实现可恢复任务

1. 从一个终端窗口说起&#xff1a;Pi 1.0 到底想解决什么问题第一次看到 Pi 1.0 这个名字&#xff0c;很多人会下意识以为又是一个套壳的对话工具。但我把它的几个核心模块——终端 Agent、MCP、Pi Durable——拆开用了一段时间之后&#xff0c;发现它真正想做的事情&#xff…

作者头像 李华