← 返回嵌入式学习资料第 9 章 · 驱动分层与设备协议

CHAPTER 9 · DRIVERS AND PROTOCOLS

驱动分层与设备协议
让每个接口有边界

从寄存器、HAL 和设备驱动的责任分界开始,建立生命周期合同、协议帧、硬件配置、故障注入与可回滚交付;把主机模型、目标机事实和停止条件明确分开。

6 个一级单元50 个递进小节主机路径可复现

TEXTBOOK MAP

CHAPTER 9 · MAP

从寄存器事实到可交接设备

六个一级单元对应六种能力:建立分层、写设备合同、定义协议、描述硬件、模拟故障、完成驱动交付。

先分清谁负责什么
  • 01 驱动层次与所有权
  • 02 生命周期与错误合同
再把字节和硬件写成数据
  • 03 协议帧与兼容
  • 04 Devicetree 与资源
最后让失败也能交接
  • 05 模拟、故障与 HIL
  • 06 版本、回滚与发布

小节数量按认知任务变化:协议、配置和交付需要更多递进步骤;每个单元都保留无目标机主机路径。

使用方式:先从左侧选择一个驱动认知,再按分层、合同、协议、配置、模拟和交付推进。每个单元底部有前后翻页;小节标题直接说明正在追踪的所有权、状态、字节、资源、故障或版本。
主机边界:默认使用标准库模型、协议向量、配置数据和故障矩阵;没有目标板也能完成状态、帧、校验、兼容和交付清单测试。
目标机边界:真实寄存器副作用、总线电气、IRQ/DMA、时钟/复位、供电、器件行为、工具链和回滚路径必须回到当前芯片、板卡、器件和官方资料。

01 · DRIVER LAYERS

驱动分层:谁拥有哪一个事实

驱动不是一组把寄存器写成某个数的函数,而是把硬件事实转换成稳定软件能力的边界。寄存器层知道位域和时序,HAL 组织通用外设操作,设备驱动管理一个具体器件,服务层解释单位和业务语义,应用层只依赖合同。本单元建立分层坐标,避免抽象泄漏和责任重叠。

看懂画出从寄存器到应用的责任边界。
说清区分硬件事实、软件策略和业务语义。
开始做为一条 API 写出输入、输出、错误和证据。
官方基线STM32CubeG0课程设备支持、启动代码与示例的锁定来源;链接指向固定 tag。锁定:v1.6.3 · 核对日期:2026-09-05

9.1.1 从寄存器事实到软件能力

一个可维护的驱动栈先回答‘谁拥有哪一个事实’:寄存器层拥有位域和清除语义,HAL 拥有一组外设的通用操作,设备驱动拥有器件初始化与状态,服务层拥有单位、范围和业务转换,应用拥有策略和用户目标。若每层都可以改同一个寄存器,故障就无法定位;若没有一层负责状态机,初始化、复位和恢复会散落在调用者之间。

9.1.2 位域、读改写与副作用

读写寄存器并不等同于完成动作。某些状态位写 1 清除,某些配置必须按顺序写入,某些字段只能在禁用状态修改,某些读操作会弹出 FIFO 或清除中断。把这些副作用藏在一个经过命名的低层函数里,才能让上层看到‘配置成功’或‘事件已取走’这样的可解释语义。

C · LAYERED DEVICE API
typedef struct { int (*read_raw)(void *ctx, uint8_t *buf, size_t n); int (*reset)(void *ctx); bool (*ready)(void *ctx); } device_ops_t; /* application sees the contract, not peripheral registers */

接口只暴露设备能力;寄存器、时钟、复位和总线副作用留在实现层。

PYTHON · LAYER CONTRACT
def call_read(ops, context, size): if size <= 0: raise ValueError("size must be positive") status, payload = ops(context, size) if status != "OK": raise RuntimeError(status) return payload

主机模型验证前后置条件;真实驱动仍需核对所有权、完成点和目标机资源。

9.1.3 LL、HAL 与 BSP 的不同职责

LL 常提供贴近寄存器的轻量入口,HAL 则以功能为中心组织初始化、轮询、中断和 DMA;BSP 再把时钟、引脚、供电和板级连接接起来。它们不是从低到高的同义词。迁移到另一颗 MCU 时,通用层可以保留,设备特有的时钟、复位、DMA 请求和寄存器布局必须重新核对。

表 1 · 驱动分层:谁拥有哪一个事实的证据判断表
层次应留下的证据不能直接推出
寄存器位域/副作用/时钟应用可直接写寄存器
驱动状态/资源/错误HAL 成功就是设备可用
服务单位/策略/质量最终值覆盖原始事实

9.1.4 设备驱动与服务层的分界

设备驱动应该返回设备能证明的事实,例如识别号、ready、busy、CRC 错误、FIFO 深度和原始计数;服务层才把这些事实解释为摄氏度、帕斯卡、文件或网络消息。若驱动直接返回‘正常温度’,应用会失去 raw、时间戳、量程和质量标志,异常也会被悄悄转换成一个看似合理的值。

9.1.5 句柄、实例和资源所有权

句柄不只是一个指针,它应表达实例、总线、配置、锁、状态和回调上下文。两个任务同时改变同一个设备的片选、寄存器指针或工作模式,哪怕每次调用都返回成功,组合结果也可能错误。所有权可以是单线程、互斥锁、消息队列或专用任务,但必须在接口和证据中可见。

图 1.1 · 驱动分层:谁拥有哪一个事实的判断路径每一格都要有自己的条件。
事实寄存器/时钟
能力HAL/驱动
语义服务/单位
证据日志/测量

从左到右是思考顺序,不是把代码、配置、协议和目标机现象压缩成一个“通过”。

9.1.6 同步、异步与完成语义

轮询函数返回通常只证明寄存器满足某个条件;启动 DMA 的函数只证明传输已提交;回调或事件才可能证明缓冲区完成。中断到达、DMA 写完和器件内部转换完成也不是同一时刻。接口应明确 completion_level、数据所有权、取消行为和超时单位,避免上层过早读取或重复消费。

9.1.7 抽象泄漏与性能证据

抽象有成本:函数跳转、锁、复制、日志和状态检查会消耗时间和内存,但直接下沉寄存器又会扩大耦合。应测量 API 调用耗时、最坏延迟、栈深度和缓冲占用,并把必须暴露的硬件选项作为能力查询或配置结构,而不是让应用偷偷包含芯片头文件。

9.1.8 本节交付:驱动分层卡

驱动分层卡至少写出 layer、owner、input、output、completion_level、error_space、resource、side_effect、timeout、concurrency、evidence 和 unverified_scope。它不是漂亮的架构图,而是后续设备合同、协议解析、模拟测试和目标机交付可以共同引用的边界。

CHECKPOINT 9.1把“寄存器、HAL、驱动、服务和应用之间,哪一层应该知道什么?”写成一份可复查记录

请围绕“驱动分层:谁拥有哪一个事实”写出对象、资源/输入条件、状态或协议机制、输出和完成语义、一个时间/安全约束、一个失败路径和对应证据。没有目标板时,运行主机路径并把未执行字段标为未执行;拥有目标机时,也要补齐芯片、板卡、总线、供电、工具链、仪器、配置和观察条件。不要用一个 API 返回值、一帧成功报文、一个 mock 结果或一张截图覆盖没有做过的验证。

合格记录要让没有现场上下文的读者知道先查什么、先运行什么、看到什么才算通过,以及哪一句结论仍然不能推出。未知项和停止条件不是扣分项,而是交付的一部分。

02 · DEVICE CONTRACT

设备合同:生命周期、状态与错误

设备合同把初始化、配置、运行、停止、复位和故障恢复写成状态机,并把每个 API 的前置条件、后置条件、超时、并发、功耗和错误分类固定下来。这样上层不必猜测‘返回 0 是不是已经有数据’,驱动也不必为每个调用者重复发明重试和恢复。

看懂用状态机描述设备生命周期。
说清为错误和超时建立可判读的合同。
开始做让调用者可以安全停止、重试和恢复。
官方基线STM32CubeG0课程设备支持、启动代码与示例的锁定来源;链接指向固定 tag。锁定:v1.6.3 · 核对日期:2026-09-05

9.2.1 探测、绑定与身份确认

探测成功可以只说明设备在总线上回应了一个地址或 ID,不说明固件所需功能已经可用。绑定前应确认身份、修订、能力和资源;例如同一总线上有响应的芯片可能是不同型号,某个寄存器可读也不代表它的供电、时钟或中断线路已经正确。

9.2.2 生命周期状态机

生命周期最少应区分 absent、bound、reset、configured、running、stopping、fault 和 recovering。每个转移要写触发条件、动作、输出和失败去向。把所有状态压缩成一个布尔值,会让调用者在设备忙、设备故障和设备根本不存在时做出相同的危险动作。

C · DEVICE LIFECYCLE
typedef enum { DEV_ABSENT, DEV_BOUND, DEV_CONFIGURED, DEV_RUNNING, DEV_FAULT } device_state_t; int device_start(device_t *dev) { if (dev->state != DEV_CONFIGURED) return ERR_STATE; dev->state = DEV_RUNNING; return 0; }

状态转移必须与复位、资源、并发和错误恢复合同一致。

PYTHON · RETRY POLICY
def retry_allowed(error, idempotent, attempt, limit): transient = {"timeout", "busy", "nack"} return error in transient and idempotent and attempt < limit

重试资格不等于动作已经安全;必须保存副作用和设备状态。

9.2.3 前置条件与后置条件

前置条件是调用者在函数前必须完成的事实,例如句柄有效、状态为 configured、缓冲区可写、锁已持有;后置条件是函数返回时可以相信的事实,例如数据长度、设备状态、错误阶段和所有权。没有后置条件的 void 函数只能靠日志猜测是否做完。

表 2 · 设备合同:生命周期、状态与错误的证据判断表
层次应留下的证据不能直接推出
状态生命周期/所有权任意状态都能调用
时间deadline/cancel/retry超时可以无限延长
恢复清理/复位/健康再次 init 一定修好

9.2.4 忙、就绪、故障与未知

busy 表示动作正在进行,ready 表示可接受下一动作,health 表示最近一次自检或通信仍在合同时间内,unknown 表示证据不足。它们互相不能替换。电源刚恢复、总线刚释放或驱动刚加载时,设备可能可达但还未 ready;超时后也不应直接把它标成正常。

9.2.5 超时、取消与等待者

超时必须从同一个时基计算,并在合同中说明是否包含排队、总线占用、器件转换和数据搬运。取消一个等待不一定取消了器件内部动作;如果硬件没有 cancel,驱动应进入 draining 或 fault,等待状态安全后再交付缓冲。无限延长 deadline 只是把故障推迟到更危险的位置。

图 2.1 · 设备合同:生命周期、状态与错误的判断路径每一格都要有自己的条件。
进入探测/复位
配置资源/参数
运行数据/事件
恢复故障/回滚

从左到右是思考顺序,不是把代码、配置、协议和目标机现象压缩成一个“通过”。

9.2.6 重试、幂等与重复副作用

重试前先问操作是否幂等。读取状态通常可以重试,写入配置可能需要读回验证,启动电机或提交不可逆命令则可能不能盲目重放。重试记录应包含 error_phase、attempt、backoff、device_state 和是否发生副作用;只在日志里写‘retry’无法让别人判断是否安全。

9.2.7 复位、重新初始化与资源释放

恢复不是再次调用 init 就结束。应先停止新请求、让 DMA/中断缓冲归还、清除锁和片选、复位器件或总线、重新读取身份与能力,再按依赖顺序配置,并做独立健康检查。恢复失败要进入明确的 fault/disabled 状态,不能把半配置设备暴露给应用。

9.2.8 错误码、日志和健康快照

错误码要能定位阶段:bind、power、reset、configure、transfer、decode、timeout、integrity、range 或 recovery。日志保留设备 ID、配置版本、请求序号、时间戳、原始响应和重试次数;健康快照则记录当前状态、最近错误、丢失计数和恢复结果,方便现场只采一份数据也能复盘。

9.2.9 本节交付:设备合同卡

设备合同卡包括 identity、states、preconditions、postconditions、completion_level、timeout、cancel、retry_policy、ownership、power、error_phase、recovery、health_snapshot 和 stop_conditions。它将协议层的帧、配置层的资源和应用层的策略连接起来,而不是把责任推给‘使用者自行注意’。

CHECKPOINT 9.2把“一个设备 API 怎样让调用者知道什么时候可以用、什么时候必须停?”写成一份可复查记录

请围绕“设备合同:生命周期、状态与错误”写出对象、资源/输入条件、状态或协议机制、输出和完成语义、一个时间/安全约束、一个失败路径和对应证据。没有目标板时,运行主机路径并把未执行字段标为未执行;拥有目标机时,也要补齐芯片、板卡、总线、供电、工具链、仪器、配置和观察条件。不要用一个 API 返回值、一帧成功报文、一个 mock 结果或一张截图覆盖没有做过的验证。

合格记录要让没有现场上下文的读者知道先查什么、先运行什么、看到什么才算通过,以及哪一句结论仍然不能推出。未知项和停止条件不是扣分项,而是交付的一部分。

03 · DEVICE PROTOCOL

设备协议:帧、版本与兼容性

设备协议的困难不在于把几个字节拼起来,而在于定义边界、长度、字节序、版本、能力、完整性、序号、确认、超时、重试、背压和未知字段的处理。协议层应让错误可定位、重复可判断、升级有路径,并且不把 CRC 当作身份认证。

看懂写出一份最小但完整的帧语法。
说清区分完整性、可靠性和安全性。
开始做为版本升级和重复消息设计兼容路径。
官方基线STM32CubeG0课程设备支持、启动代码与示例的锁定来源;链接指向固定 tag。锁定:v1.6.3 · 核对日期:2026-09-05

9.3.1 帧边界与解析状态

帧解析器不应把‘读到几个字节’当成消息。一个健壮解析器有明确状态,例如 WAIT_SYNC、READ_HEADER、CHECK_LENGTH、READ_PAYLOAD、CHECK_INTEGRITY 和 EMIT_RESULT;长度异常、超大帧、超时或 CRC 错误都有丢弃和重新同步路径。解析器日志要记录状态和偏移,才能解释为何后续好帧又恢复。

9.3.2 长度、字节序与字段编码

长度和字节序属于协议语法,不是编译器默认行为。固定宽度字段要明确有符号性、缩放、保留值和最大值;乘法、移位和长度加法要在目标整数宽度下检查溢出。主机端通过一个 Python 编码器不代表 8 位 MCU 的整数提升、对齐和端序实现也正确。

PYTHON · FRAME ENCODER
def encode(version, kind, sequence, payload, crc16): if len(payload) > 255: raise ValueError("payload too large") header = bytes((0xA5, version, kind, sequence, len(payload))) body = header + payload return body + crc16(body).to_bytes(2, "big")

帧语法要同时定义同步、长度、端序、CRC 参数、版本和最大值。

C · PARSER STATE
switch (parser->state) { case WAIT_SYNC: if (byte == 0xA5) parser->state = READ_HEADER; break; case READ_HEADER: parser_header_push(parser, byte); break; case READ_PAYLOAD: parser_payload_push(parser, byte); break; case CHECK_FRAME: return verify_frame(parser); }

解析器必须为截断、坏长度、坏 CRC、超时和重新同步保留可观察状态。

9.3.3 转义、同步与坏帧丢弃

转义是为了让同步字节、长度或控制字符可以出现在 payload 中;它必须有成对规则和截断处理。遇到非法转义、长度超限或中途超时时,解析器应丢弃当前候选帧并回到可证明的同步点,而不是把残留字节拼到下一条命令。

表 3 · 设备协议:帧、版本与兼容性的证据判断表
层次应留下的证据不能直接推出
帧边界/长度/端序读到字节就是一帧
可靠CRC/ACK/序号CRC 等于认证
演进版本/能力/迁移删除旧字段无影响

9.3.4 CRC、校验和与完整性边界

CRC 或校验和可以在给定参数、范围和传输方向下检测部分随机或突发错误,但不能证明消息来自可信设备,也不能证明字段语义正确。安全身份需要认证、密钥和重放防护;完整性测试则要保存多项式、初值、反射、终值和覆盖范围。

9.3.5 请求、响应、ACK 与 NACK

ACK 只证明某一层收到了请求,NACK 可能表示格式错、忙、权限不足、范围错或内部故障,TIMEOUT 则是证据不足。响应结构要带结果码、错误阶段、已执行副作用和可重试提示,不能用一个 0/1 把这些情况压扁。

图 3.1 · 设备协议:帧、版本与兼容性的判断路径每一格都要有自己的条件。
边界帧/长度
语义命令/响应
可靠序号/重试
演进版本/能力

从左到右是思考顺序,不是把代码、配置、协议和目标机现象压缩成一个“通过”。

9.3.6 序号、去重与幂等

序号让接收端识别重复和丢失,但去重窗口、重启后的持久化和序号回绕都要定义。幂等命令可以按序号返回缓存结果;非幂等命令则应要求事务 ID、状态查询或明确的‘可能已执行’错误。客户端重试必须记录 attempt 和原始响应。

9.3.7 背压、窗口与超时

发送方必须受到接收 FIFO、处理时间和总线带宽约束;窗口、credit、忙响应和节流都属于协议的一部分。超时要覆盖排队、传输、处理和响应,而不是从 write() 返回开始盲猜。背压缺失会把可恢复的慢设备变成持续丢帧和复位风暴。

9.3.8 版本、能力与未知字段

版本字段说明语法,能力字段说明当前端点支持什么;二者不能互相替代。新增字段应允许旧端跳过或按长度停止,删除字段要有迁移期,错误码和默认值也要保持兼容。保存一张版本×能力矩阵,比一句‘向后兼容’更可复查。

9.3.9 本节交付:设备协议卡

设备协议卡包括 framing、field_schema、endianness、limits、integrity、auth_scope、sequence、dedup、ack_semantics、timeout、backpressure、version、capabilities、compatibility 和 replay_vectors。它让下一层可以用同样的样例测试驱动,而不是每次靠抓包猜协议。

CHECKPOINT 9.3把“一串字节怎样变成可校验、可升级、可恢复的设备消息?”写成一份可复查记录

请围绕“设备协议:帧、版本与兼容性”写出对象、资源/输入条件、状态或协议机制、输出和完成语义、一个时间/安全约束、一个失败路径和对应证据。没有目标板时,运行主机路径并把未执行字段标为未执行;拥有目标机时,也要补齐芯片、板卡、总线、供电、工具链、仪器、配置和观察条件。不要用一个 API 返回值、一帧成功报文、一个 mock 结果或一张截图覆盖没有做过的验证。

合格记录要让没有现场上下文的读者知道先查什么、先运行什么、看到什么才算通过,以及哪一句结论仍然不能推出。未知项和停止条件不是扣分项,而是交付的一部分。

04 · HARDWARE DESCRIPTION

设备描述与配置:让硬件成为数据

当硬件拓扑写在设备树、Kconfig、板级配置或生成文件里,驱动代码可以复用于多个板卡,但数据描述也会成为合同的一部分。compatible、寄存器、IRQ、时钟、复位、供电、总线父节点和默认参数必须描述硬件事实,不能为了让驱动 probe 成功而编造节点。

看懂区分硬件描述与驱动策略。
说清会读资源、依赖和 compatible 的边界。
开始做用校验和证据阻止配置漂移。
官方基线STM32CubeG0课程设备支持、启动代码与示例的锁定来源;链接指向固定 tag。锁定:v1.6.3 · 核对日期:2026-09-05

9.4.1 硬件描述不是代码注释

设备树、板级配置或 Kconfig 是硬件描述数据,不是为绕过编译错误而填的参数表。它告诉系统设备在哪里、挂在哪个总线、需要哪些资源;驱动再根据这些资源实现行为。把策略、默认超时和当前驱动限制全部写进节点,会让同一硬件无法被另一种软件正确理解。

9.4.2 compatible 与能力匹配

compatible 应尽量具体,并按真实软件可支持的范围列 fallback;‘看起来兼容’不等于驱动能安全使用。新增修订若改变寄存器布局、复位时序或错误语义,就需要新的 compatible 或显式能力分支。绑定文件和示例必须一起评审,避免字符串变成无人负责的 ABI。

DTS · RESOURCE SHAPE
sensor@48 { compatible = "demo,pressure-v2"; reg = <0x48>; interrupts = <5>; reset-gpios = <&gpio0 12 0>; status = "okay"; };

节点描述硬件事实;具体地址、IRQ、GPIO 和兼容字符串必须按板卡资料核对。

PYTHON · CONFIG CHECK
def required_fields(node): required = ("compatible", "reg", "status") missing = [name for name in required if name not in node] if missing: raise ValueError("missing: " + ",".join(missing)) return node["compatible"]

主机校验能发现数据缺失,不能证明地址译码、IRQ 线、供电和 pinctrl 已连接。

9.4.3 reg、IRQ、clock 与 reset 资源

reg、interrupts、clocks、resets、power supplies、pinctrl 和 DMA 通道任何一项错误,都可能表现成驱动超时或数据错乱。静态字段存在只证明文本完整,仍要将编译后的 DTB、内核/RTOS 解析结果和板卡原理图、实测引脚逐项对账。

表 4 · 设备描述与配置:让硬件成为数据的证据判断表
层次应留下的证据不能直接推出
描述节点/compatible为驱动创建节点
资源reg/IRQ/clock/reset字段存在就是连接正确
验证schema/DTB/probe默认值可以猜硬件

9.4.4 总线父子关系与依赖

设备节点的父子关系表达控制器与从设备的拓扑,依赖关系则决定 probe 是否可以开始。一个 SPI 子设备的驱动不应自己假定 SPI 控制器时钟已开;它应该通过资源和框架获得依赖,并在延迟探测时区分‘依赖尚未 ready’与‘身份不匹配’。

9.4.5 overlay、多实例与板级变体

overlay 适合表达板卡变体,但多层 overlay 可能覆盖同一属性、重复注册地址或改变 pinctrl。多实例要有稳定的实例标识、独立资源和并发所有权;不要用一个全局设备指针和一个模糊默认值把两块器件混成一块。

图 4.1 · 设备描述与配置:让硬件成为数据的判断路径每一格都要有自己的条件。
描述节点/属性
绑定资源/兼容
探测依赖/顺序
验证DTB/实机

从左到右是思考顺序,不是把代码、配置、协议和目标机现象压缩成一个“通过”。

9.4.6 binding schema 与静态校验

binding schema 的价值是把必填字段、枚举、数组长度、单位和互斥关系在构建期检查。即使当前驱动只用轮询,也应描述实际存在的中断资源;绑定描述硬件,不应随着某个驱动暂时不支持某功能而删掉事实。

9.4.7 probe 顺序、延迟探测与默认值

probe 失败日志要区分缺资源、依赖 busy、设备 NACK、身份不匹配和配置非法。默认值只能用于明确安全且有文档依据的字段,不能拿一个常见时钟、地址或 IRQ 猜测硬件。晚到的 regulator、reset 或总线控制器应触发可观察的 deferred 状态而不是静默跳过。

9.4.8 本节交付:硬件配置卡

硬件配置卡包括 source、generated_artifact、compatible、parent_bus、reg、irq、clock、reset、power、pinctrl、dma、required、defaults、schema_version、board_revision 和 probe_evidence。它让 Linux/Zephyr 的描述方式可以迁移,但不抹平板卡真实差异。

CHECKPOINT 9.4把“驱动怎样从设备树或配置文件得到资源,又怎样证明这些资源真的完整?”写成一份可复查记录

请围绕“设备描述与配置:让硬件成为数据”写出对象、资源/输入条件、状态或协议机制、输出和完成语义、一个时间/安全约束、一个失败路径和对应证据。没有目标板时,运行主机路径并把未执行字段标为未执行;拥有目标机时,也要补齐芯片、板卡、总线、供电、工具链、仪器、配置和观察条件。不要用一个 API 返回值、一帧成功报文、一个 mock 结果或一张截图覆盖没有做过的验证。

合格记录要让没有现场上下文的读者知道先查什么、先运行什么、看到什么才算通过,以及哪一句结论仍然不能推出。未知项和停止条件不是扣分项,而是交付的一部分。

05 · SIMULATION AND FAULTS

模拟与故障注入:在没有目标机时验证边界

主机模拟不是目标机的替身,而是把协议、状态机、超时、错误和恢复从寄存器依赖中拆出来。用可注入的总线、寄存器、时钟和时间源,可以在读者没有硬件时重复运行故障矩阵;再用明确的 HIL/目标机清单说明哪些事实仍需现场测量。

看懂把硬件依赖替换成可控假件。
说清用确定性时间和故障矩阵测试驱动。
开始做写清主机证据不能推出的目标机事实。
官方基线STM32CubeG0课程设备支持、启动代码与示例的锁定来源;链接指向固定 tag。锁定:v1.6.3 · 核对日期:2026-09-05

9.5.1 依赖注入与最小接口

依赖注入不是把硬件函数随便替换成 lambda,而是定义驱动真正需要的最小能力:读写寄存器、提交事务、取时间、等待事件、记录日志和报告电源状态。真实 HAL 和 fake 实现都遵守同一接口,测试才可以判断驱动策略,而不是判断一个特制 mock 的内部捷径。

9.5.2 伪寄存器与副作用模型

伪寄存器模型应记录地址、宽度、写入顺序和副作用。例如写 1 清除、读后弹出、锁定位、写保护和状态自动变化都要显式建模;如果 fake 把所有寄存器都当成普通字典,主机测试会给出虚假的安全感。

PYTHON · VIRTUAL DEVICE
class FakeDevice: def __init__(self, responses): self.responses = iter(responses) self.now = 0 def transfer(self, request): response = next(self.responses) if response == "TIMEOUT": self.now += 100 return response

确定性假件让超时、重试和恢复可重复;它不模拟真实电平或器件模拟行为。

C · FAULT VECTOR
typedef struct { const char *id; int expected_error; bool retryable; bool enters_fault; } fault_case_t; static const fault_case_t cases[] = { {"nack", ERR_NACK, true, false}, {"bad_crc", ERR_INTEGRITY, false, true} };

故障矩阵应覆盖阶段、状态、重试和停止条件,而不是只记录返回码。

9.5.3 确定性虚拟时间

虚拟时间让测试把 now、deadline、backoff 和器件转换完成作为可控事件推进。相比真实 sleep,它能在毫秒级时间窗内重放长时间故障,并精确断言‘超时前再试一次、超时后进入 fault’。时间源本身也是依赖,不能在函数内偷偷调用系统时钟。

表 5 · 模拟与故障注入:在没有目标机时验证边界的证据判断表
层次应留下的证据不能直接推出
假件总线/寄存器/时间mock 返回成功就够
故障阶段/重试/状态只测 happy path
迁移HIL/波形/电气主机通过即目标通过

9.5.4 故障矩阵:忙、NACK、坏帧与掉电

故障矩阵应按阶段列出:bind 身份不匹配、reset 不完成、configure 被拒绝、transfer NACK、response 坏帧、timeout、DMA 覆盖、power loss 和 recovery 失败。每格要写预期错误、是否重试、设备状态和必须留下的日志,避免只测一个异常返回。

9.5.5 属性、变形与边界测试

属性测试检查规则而非固定样例,例如编码后再解码得到同一消息、长度变化不会越界、增加一次可忽略的空操作不改变状态、序号重复不会增加副作用。变形测试特别适合协议和配置,因为它可以一次覆盖大量边界,但仍要保留失败输入供重放。

图 5.1 · 模拟与故障注入:在没有目标机时验证边界的判断路径每一格都要有自己的条件。
替换总线/寄存器
控制时间/故障
观察trace/断言
迁移HIL/现场

从左到右是思考顺序,不是把代码、配置、协议和目标机现象压缩成一个“通过”。

9.5.6 trace、计数器和最小日志

trace 至少包含单调 tick、设备实例、状态前后、操作、请求序号、输入摘要、错误阶段、attempt、deadline 和恢复动作。日志要有长度上限和敏感字段策略;主机测试的 trace 可作为目标机字段模板,但不能假装它包含真实电气波形。

9.5.7 HIL、目标机和主机证据边界

HIL 或目标机测试负责真实寄存器副作用、总线电平、IRQ/DMA 竞争、器件转换时间、上电和温度。主机测试通过只证明软件模型中的断言;迁移时要把同一个输入、预期输出、失败动作和证据字段带到实机,而不是重新写一套不可比的演示。

9.5.8 本节交付:故障矩阵卡

故障矩阵卡包括 dependency_contract、virtual_clock、register_side_effects、fault_id、input_vector、expected_phase、retry、state_after、trace_ref、host_result、target_required 和 stop_condition。它使‘没有板也能开始’与‘没有板不能声称完成’同时成立。

CHECKPOINT 9.5把“没有开发板,怎样证明驱动没有把正常路径写死?”写成一份可复查记录

请围绕“模拟与故障注入:在没有目标机时验证边界”写出对象、资源/输入条件、状态或协议机制、输出和完成语义、一个时间/安全约束、一个失败路径和对应证据。没有目标板时,运行主机路径并把未执行字段标为未执行;拥有目标机时,也要补齐芯片、板卡、总线、供电、工具链、仪器、配置和观察条件。不要用一个 API 返回值、一帧成功报文、一个 mock 结果或一张截图覆盖没有做过的验证。

合格记录要让没有现场上下文的读者知道先查什么、先运行什么、看到什么才算通过,以及哪一句结论仍然不能推出。未知项和停止条件不是扣分项,而是交付的一部分。

06 · DRIVER DELIVERY

驱动交付:从接口到可回滚产品

驱动交付是可重建、可诊断、可升级和可回滚的证据包:源码、配置、接口、构建版本、测试向量、日志、故障矩阵、目标机条件、已知限制和恢复动作必须一起出现。最后一个函数返回成功不是交付完成;读者要能知道运行什么、看到什么、何时停止以及结论不能越过哪里。

看懂列出可重建的交付物。
说清把接口、测试和版本绑定起来。
开始做写出升级、回滚和停止条件。
官方基线STM32CubeG0课程设备支持、启动代码与示例的锁定来源;链接指向固定 tag。锁定:v1.6.3 · 核对日期:2026-09-05

9.6.1 最小可重建交付包

最小交付包不是‘把源码压缩一下’,而是让没有上下文的读者在干净目录完成构建或主机测试,并知道每个结果依赖的版本和输入。清单应覆盖源文件、头文件、配置、协议向量、测试、脚本、工具版本、许可证、示例日志和未验证字段;任何靠开发者电脑上的隐藏文件才能工作的步骤都不算可重建。

9.6.2 接口文档与示例输入

接口文档要写前置条件、后置条件、单位、生命周期、并发、超时、错误阶段和所有权,并给出一个成功、一个边界和一个失败调用序列。示例输入不能只展示最短 happy path;它应包含截断、重复、忙、范围错误或版本不匹配,让读者看到合同如何保护系统。

SHELL · DELIVERY MANIFEST
sha256sum src/* examples/* host_tests/* python run_all_tests.py # record toolchain, config hash, target result and unverified scope

清单、命令、哈希和未验证范围一起交付;一个 ELF 不能替代重建证据。

PYTHON · REPLAY
def replay(records, handler): results = [] for record in records: results.append(handler(record["request"], record["fault"])) return results

主机重放能复核状态和协议规则;目标机仍需补充资源、时序、电气和恢复证据。

9.6.3 构建版本、哈希与配置快照

源码提交、配置快照、编译器版本、链接脚本、依赖锁、构建命令、ELF/map 和哈希共同组成 provenance。二进制名称和时间戳不能代替提交哈希;如果配置由设备树或生成器产生,还要保存源文件、生成版本和生成输出,才能解释一次升级为何改变资源。

表 6 · 驱动交付:从接口到可回滚产品的证据判断表
层次应留下的证据不能直接推出
重建源码/配置/工具一个二进制足够
证据测试/日志/哈希覆盖率代表安全
演进兼容/回滚/停止升级失败再重试

9.6.4 主机测试、目标测试与覆盖边界

主机测试验证纯软件模型、解析器、状态机和故障策略;目标测试验证真实寄存器、总线、IRQ/DMA、电气和器件行为。报告应分栏并列出覆盖边界。主机 100% 分支覆盖不等于目标机安全,目标机一次通过也不能替代长期、异常和回滚证据。

9.6.5 升级、迁移与兼容矩阵

兼容矩阵把旧固件、新驱动、协议版本、配置版本和能力组合列出来,说明支持、降级、拒绝或迁移脚本。字段改名、单位改变、默认值改变和错误码重用都可能是破坏性变化;保留一个短暂的双读/双写或显式版本迁移,通常比静默猜测更可靠。

图 6.1 · 驱动交付:从接口到可回滚产品的判断路径每一格都要有自己的条件。
重建源码/配置
验证测试/证据
演进版本/兼容
恢复回滚/停止

从左到右是思考顺序,不是把代码、配置、协议和目标机现象压缩成一个“通过”。

9.6.6 回滚、复位和不可逆操作

更新、复位和回滚路径要把不可逆动作单独标出:写入 Flash、启动电机、修改电源、清除配对或提交计数器都不能靠无限重试。失败时先进入安全态、保存原因和版本,再执行有界的恢复;更新过程被切断后,仍应有可验证的旧镜像或恢复模式。

9.6.7 安全、权限与敏感日志

协议令牌、密钥、个人数据、设备序列号和内部地址不应原样写入公开日志。调试信息要区分开发与生产级别,错误摘要可以哈希或截断,但必须保留定位所需的阶段和计数。权限边界也属于驱动合同:谁能配置、谁能读 raw、谁能触发 reset,都要明确。

9.6.8 评审、维护和已知限制

维护记录要包含已知限制、未验证假设、目标机差异、开放问题、复现命令和下一次只改一个变量的建议。‘在我的板上正常’不是可迁移结论;好的发布说明会写明支持的芯片/修订/配置范围、停止条件和不承诺的性能或安全属性。

9.6.9 本节交付:驱动发布卡

驱动发布卡包括 manifest、source_hash、config_hash、toolchain、api_version、protocol_version、test_commands、host_result、target_result、compatibility、migration、rollback、security_scope、known_limits、stop_conditions 和 next_owner。它把本章的分层、合同、协议、配置和模拟证据汇聚成可以交接的产品。

CHECKPOINT 9.6把“怎样把一个能工作的驱动交给下一位读者,而不是只留下源码?”写成一份可复查记录

请围绕“驱动交付:从接口到可回滚产品”写出对象、资源/输入条件、状态或协议机制、输出和完成语义、一个时间/安全约束、一个失败路径和对应证据。没有目标板时,运行主机路径并把未执行字段标为未执行;拥有目标机时,也要补齐芯片、板卡、总线、供电、工具链、仪器、配置和观察条件。不要用一个 API 返回值、一帧成功报文、一个 mock 结果或一张截图覆盖没有做过的验证。

合格记录要让没有现场上下文的读者知道先查什么、先运行什么、看到什么才算通过,以及哪一句结论仍然不能推出。未知项和停止条件不是扣分项,而是交付的一部分。

资料卡(访问日期:2026-09-04):ST HAL/LL、CMSIS-Driver、Zephyr 设备模型/Devicetree 和 Linux 设备模型/绑定规范展示不同生态的分层与配置入口。通用框架不能替代当前芯片、板卡、器件、总线、电气、供电和恢复条件。