diff --git "a/2026/\347\254\2544\347\273\204/\347\216\213\347\220\263/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/README.md" "b/2026/\347\254\2544\347\273\204/\347\216\213\347\220\263/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/README.md" new file mode 100644 index 0000000000000000000000000000000000000000..58a55646efec536eb481a9363fdded07aff94c32 --- /dev/null +++ "b/2026/\347\254\2544\347\273\204/\347\216\213\347\220\263/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/README.md" @@ -0,0 +1,234 @@ +# PIN设备的对接 +PIN设备(GPIO)的对接,是将芯片厂商提供的、各不相同的GPIO硬件操作库,通过一个统一的接口包装起来 + + + +# 设计虚拟设备驱动框架层 `test_dev` + +## 一、设计思路 + +### 1.1 架构分层 + +整体架构: + +| 层级 | 职责 | 对应代码 | +|------|------|----------| +| **应用层** | 调用标准 API 访问设备,不感知底层实现 | `test_dev_app()`、`test_dev_ref()` | +| **设备管理层** | RT-Thread 内核提供统一设备模型 | `rt_device_find/open/read/write/control` | +| **驱动层** | 实现具体的设备操作逻辑 | `test_dev_init/open/close/read/write/control` | + +```c +// 私有数据结构:设备对象作为首成员 +struct test_dev +{ + struct rt_device parent; /* 设备对象(必须第一成员) */ + rt_uint8_t buffer[TEST_DEV_BUF_SIZE]; /* 环形缓冲区 */ + rt_uint16_t rd_pos; /* 读指针 */ + rt_uint16_t wr_pos; /* 写指针 */ + rt_uint32_t open_count; /* 驱动层打开引用计数 */ + rt_uint32_t read_bytes; /* 累计读字节数 */ + rt_uint32_t write_bytes; /* 累计写字节数 */ + rt_bool_t initialized; /* 初始化标志 */ + struct rt_mutex lock; /* 互斥锁 */ +}; +``` + + +--- + +### 1.2 环形缓冲区(FIFO) + +```c +// 环形缓冲辅助函数(代码片段) +static rt_bool_t test_dev_is_empty(struct test_dev *d) { + return (d->rd_pos == d->wr_pos); +} +static rt_bool_t test_dev_is_full(struct test_dev *d) { + return (((d->wr_pos + 1) % TEST_DEV_BUF_SIZE) == d->rd_pos); +} +``` + +**核心机制**: +- **判空**:`rd_pos == wr_pos` → 缓冲区为空 +- **判满**:`(wr_pos + 1) % SIZE == rd_pos` → 缓冲区满(留一格空位区分空/满) +- **可读量**:`(wr_pos - rd_pos + SIZE) % SIZE` +- **可写量**:`SIZE - 1 - 可读量` +- **回绕处理**:读写操作分两段拷贝,自动处理指针到达缓冲区末尾时跳回头部 + +--- + +### 1.3 并发安全设计 + +在多线程 RTOS 环境中,缓冲区和计数是共享资源,必须加以保护。 + +```c +// 所有临界区操作均加锁(代码片段) +rt_mutex_take(&d->lock, RT_WAITING_FOREVER); +// ... 操作 buffer / rd_pos / wr_pos / open_count ... +rt_mutex_release(&d->lock); +``` + +使用 `rt_mutex` 互斥量保护所有共享资源,确保并发读写场景下数据不会错乱。 + +--- + +### 1.4 引用计数与不对称关闭语义 + +解决多线程共享设备时的资源生命周期管理问题。 + +```c +// 驱动 open:每次被调用都累加计数(代码片段) +d->open_count++; + +// 驱动 close:仅在系统引用计数归零时被调用一次(代码片段) +if (d->open_count > 0) d->open_count--; +if (d->open_count == 0) { + /* 真正关闭:复位缓冲区 */ + memset(d->buffer, 0, sizeof(d->buffer)); + d->rd_pos = 0; + d->wr_pos = 0; +} +``` + +**两层计数**: +| 计数类型 | 维护者 | 变化时机 | 作用 | +|----------|--------|----------|------| +| 系统引用计数 (`ref_count`) | RT-Thread 内核 | `open` +1,`close` -1 | 控制驱动 `close` 是否执行 | +| 驱动私有计数 (`open_count`) | 驱动层 | 驱动 `open` 被调用时 +1 | 追踪真实打开次数,用于最终清理 | + +**不对称语义**: +- 驱动 `open` 被调用的次数 = 应用调用 `rt_device_open` 的次数 +- 驱动 `close` 仅在系统引用计数归零时被调用 **一次** +- 避免了单一线程关闭设备导致其他线程无法访问的问题 + +--- + +### 1.5 打开模式校验 + +```c +// 模式校验(代码片段) +if ((req_mode == RT_DEVICE_OFLAG_WRONLY) && + !(dev_cap & (RT_DEVICE_FLAG_WRONLY | RT_DEVICE_FLAG_RDWR))) { + return -RT_EINVAL; +} +``` + +当应用请求的打开模式(只读/只写/读写)与设备能力不匹配时,驱动拒绝打开,返回 `-RT_EINVAL`,增强了驱动可靠性。 + +--- + +### 1.6 自定义控制命令 + +```c +#define TEST_DEV_CTRL_CLEAR 0x01 /* 清空内部缓冲区 */ +#define TEST_DEV_CTRL_GET_STATUS 0x02 /* 查询设备状态 */ +``` + +支持清空缓冲区和查询设备运行状态,便于调试和功能验证。 + +--- + +## 二、运行结果分析 + +### 2.1 设备注册 + +``` +[test_dev] registered successfully (direct function pointers) +msh >list_device +device type ref count +-------- -------------------- ---------- +test_dev Character Device 0 +uart1 Character Device 2 +pin Pin Device 0 +``` + +**分析**:`INIT_DEVICE_EXPORT` 在系统启动时自动注册了 `test_dev`。`list_device` 确认设备已加入设备管理器,引用计数为 0(尚未被打开)。 + +--- + +### 2.2 基础功能测试:`test_dev_app` + +#### ① 打开设备与初始状态 + +``` +[test_dev] init ok, ring buffer size=128 (usable=127) +[test_dev] open success, oflag=0x3, open_count=1 +[test_dev] --- STATUS: init=1 empty=1 full=0 rd=0 wr=0 readable=0 writable=127 open_count=1 read_total=0 write_total=0 +``` + +**分析**: +- `init` 初始化环形缓冲区、互斥锁,置 `initialized = 1`。 +- `oflag=0x3` 对应 `RT_DEVICE_OFLAG_RDWR`(读写模式)。 +- `open_count=1`:驱动层引用计数 +1。 +- `writable=127`:缓冲区容量 128,可用空间为 127(留一格区分空/满)。 + +#### ② 写入并读回 + +``` +[test_dev] write 16 bytes, wr_pos=16 +[test_dev] write 16 bytes +[test_dev] read 16 bytes, rd_pos=16 +[test_dev] read back: Hello RT-Thread! +``` + +**分析**:写入 `"Hello RT-Thread!"` 长度 16,写指针从 0→16。读回 16 字节,读指针从 0→16。数据完整读回,FIFO 顺序正确。 + +#### ③ 环形回绕测试 + +``` +[test_dev] write 127 bytes, wr_pos=15 +[test_dev] write 200 request -> actual stored 127 bytes (wrap test) +[test_dev] --- STATUS: init=1 empty=0 full=1 rd=16 wr=15 readable=127 writable=0 open_count=1 read_total=16 write_total=143 +``` + +**分析**: +- 当前 `rd=16, wr=16`(缓冲区为空)。 +- 尝试写入 200 字节,驱动自动截断为 127 字节(缓冲区满)。 +- `wr_pos` 从 16 开始,写入 127 字节后变为 15,发生**环形回绕**(`(16+127)%128=15`)。 +- 状态显示 `full=1`,`readable=127`,`writable=0`,缓冲区完全占满。 + +#### ④ 清空缓冲区与关闭设备 + +``` +[test_dev] buffer cleared +[test_dev] *** device really closed (open_count=0), buffer reset *** +[test_dev] test_dev_app done +``` + +**分析**:`TEST_DEV_CTRL_CLEAR` 清空缓冲区。`rt_device_close` 时 `open_count` 从 1→0,触发“真正关闭”,复位缓冲区。 + +--- + +### 2.3 引用计数专项测试:`test_dev_ref` + +#### ① 连续打开 3 次 + +``` +[test_dev] open success, oflag=0x3, open_count=1 +[test_dev] open success, oflag=0x3, open_count=2 +[test_dev] open success, oflag=0x3, open_count=3 +[test_dev] --- after open x3 --- +[test_dev] --- STATUS: init=1 empty=1 full=0 rd=0 wr=0 readable=0 writable=127 open_count=3 read_total=16 write_total=143 +``` + +**分析**:每次 `rt_device_open` 都进入驱动 `open`,`open_count` 从 1 累加到 3。证明驱动 `open` 调用次数 = 应用 `open` 次数。 + +#### ② 连续关闭 3 次 + +``` +[test_dev] close #1 (ref 3->2, driver close NOT called) +[test_dev] close #2 (ref 2->1, driver close NOT called) +[test_dev] close called, open_count=2 (still referenced) +[test_dev] close #3 (ref 1->0, driver close called ONCE, open_count 3->2) +[test_dev] --- STATUS: init=1 empty=1 full=0 rd=0 wr=0 readable=0 writable=127 open_count=2 read_total=16 write_total=143 +``` + + +**结论**:驱动 `close` 仅在系统引用计数归零时执行一次,完美验证了“不对称关闭”语义。最终 `open_count=2`,缓冲区未被复位(因为 `open_count` 未归零),说明驱动正确区分了“关闭”与“真正释放”。 + +--- +### 运行效果 + +![alt text](image.png) + +![alt text](image-1.png) \ No newline at end of file diff --git "a/2026/\347\254\2544\347\273\204/\347\216\213\347\220\263/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/image-1.png" "b/2026/\347\254\2544\347\273\204/\347\216\213\347\220\263/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/image-1.png" new file mode 100644 index 0000000000000000000000000000000000000000..f76b7b68b63ab759d506dde7277db7df54672c93 Binary files /dev/null and "b/2026/\347\254\2544\347\273\204/\347\216\213\347\220\263/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/image-1.png" differ diff --git "a/2026/\347\254\2544\347\273\204/\347\216\213\347\220\263/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/image.png" "b/2026/\347\254\2544\347\273\204/\347\216\213\347\220\263/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/image.png" new file mode 100644 index 0000000000000000000000000000000000000000..9df38b82c626074fad76fe9d97916f4ba86b1e8e Binary files /dev/null and "b/2026/\347\254\2544\347\273\204/\347\216\213\347\220\263/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/image.png" differ diff --git "a/2026/\347\254\2544\347\273\204/\347\216\213\347\220\263/\347\254\224\350\256\260/\351\251\261\345\212\250\347\254\224\350\256\260/\351\251\261\345\212\250\347\254\224\350\256\260.md" "b/2026/\347\254\2544\347\273\204/\347\216\213\347\220\263/\347\254\224\350\256\260/\351\251\261\345\212\250\347\254\224\350\256\260/\351\251\261\345\212\250\347\254\224\350\256\260.md" new file mode 100644 index 0000000000000000000000000000000000000000..2dcc3541c8663e029e8eb91b88bfe7cd4297d1a7 --- /dev/null +++ "b/2026/\347\254\2544\347\273\204/\347\216\213\347\220\263/\347\254\224\350\256\260/\351\251\261\345\212\250\347\254\224\350\256\260/\351\251\261\345\212\250\347\254\224\350\256\260.md" @@ -0,0 +1,103 @@ +# RT-Thread设备驱动框架与IO外设开发 +RT-Thread设备驱动框架的分层架构与API调用机制,通过GPIO、I2C及SPI的实操演示,阐述了从底层驱动到上层应用的完整对接流程及代码复用策略。 + +> “驱动”是把芯片寄存器或厂商 HAL 操作封装成系统和应用都能使用的接口。Linux 中常见的“设备文件 + `open/read/write/ioctl`”思想,在 RT-Thread 中对应“设备对象 + `rt_device_*` API”,但 RT-Thread 通常没有完整的文件系统也不要求使用进程。 + +### 预备概念: +- **线程上下文**:线程中可以睡眠、申请资源并调用大多数系统 API;中断服务函数(ISR)运行在中断上下文,必须短小,不能随意睡眠。需要延后处理时,应使用信号量、事件、消息队列或工作线程通知线程处理。 +- **调度**:RT-Thread 按优先级和时间片在就绪线程之间切换。高优先级线程长期不阻塞会影响低优先级线程,因此驱动读写通常应设置超时并及时让出 CPU。 +- **Linux 类比**:Linux 的进程/线程也有调度和阻塞;Linux 驱动常在中断上半部记录事件、下半部处理耗时工作,RT-Thread 可用“中断回调 + 工作线程”实现类似分工。 +- **常见数据流**:硬件产生事件 → ISR 清除硬件标志并通知 → 驱动回调/设备层 → 应用线程读取数据。通知只表示“有数据/事件”,实际数据仍应放在缓冲区或消息队列中。 +- **I2C 基础**:I2C 使用两根信号线,SCL 是时钟线、SDA 是数据线,通常由一个主机控制时钟,多个从设备共享总线。每个从设备用 7 位地址区分,SDA/SCL 为开漏结构并需要上拉电阻;一次传输包含起始信号、地址、读写位、数据、应答(ACK)和停止信号。它只需要少量引脚,适合连接温度传感器、EEPROM 等低速外设,但速度和距离通常不如 SPI。 +- **SPI 基础**:SPI 通常使用 SCK(时钟)、MOSI(主机到从机)、MISO(从机到主机)和 CS(片选)四类信号。它没有设备地址,主机通过拉低某个设备的 CS 选择通信对象;多个设备可以共用 SCK/MOSI/MISO,但每个设备一般需要独立 CS。SPI 可全双工、高速传输,常用于 Flash、显示屏和 ADC;通信前必须匹配从设备要求的 CPOL/CPHA 模式、时钟频率、数据位宽和位序。 +- **如何选总线**:需要少量引脚、多个低速传感器共享线路时优先考虑 I2C;需要高速连续传输、能接受额外 CS 引脚时选择 SPI。两者都需要先确认电平电压、最大频率和时序,再进行驱动配置。 +- +## 一、设备驱动框架架构与分层设计 +针对嵌入式开发中不同厂家API差异导致的代码复用率低问题,提出了分层抽象的设计理念 +#### 1. 分层架构与抽象优势 +- **硬件抽象层设计**:通过将SPI、I2C等设备抽象为一层,上层应用使用统一API调用,底层驱动负责对接具体硬件,实现驱动与应用的分离。 +- **代码复用与迁移**:该设计使得上层应用代码可在不同MCU间无缝迁移,仅需适配底层驱动,大幅降低学习成本并减少碎片化开发。 +- **架构演进**:RT-Thread设备框架从最初的SPI统一API演进为统一的IO设备管理层,通过 `open`、`write`、`read`、`close` 等标准接口操作硬件。 +#### 2. 设备分类与核心结构 +- **设备类型划分**:设备分为字符设备(如串口、键盘,支持字节流顺序读写)和块设备(如硬盘、Flash,支持随机寻址读写)。 +- **核心数据结构**:`rt_device` 结构体包含设备类型、参数、打开标志及操作函数集(ops),通过继承机制实现面向对象编程。 +- **设备管理机制**:设备通过 `rt_device_register` 注册到IO管理系统的链表中,应用层通过 `rt_device_find` 查找并打开设备。 + +应用通常不直接访问寄存器,而是执行“查找 → 打开 → 读写/控制 → 关闭”: +```c +rt_device_t dev = rt_device_find("uart1"); +rt_device_open(dev, RT_DEVICE_FLAG_INT_RX); +rt_device_read(dev, 0, buf, sizeof(buf)); +rt_device_control(dev, RT_DEVICE_CTRL_CONFIG, &cfg); +rt_device_close(dev); +``` +`rt_device_t` 是设备句柄(指针),不是设备本身;句柄失效或名称拼写错误时要检查返回值,避免空指针访问。 +## 二、IO设备管理与API调用流程 +设备从创建到调用的完整生命周期和回调机制 +#### 1. 设备注册与生命周期管理 +- **创建与注册流程**:通过 `rt_device_create` 创建设备句柄,再调用 `rt_device_register` 将其挂载到系统链表,可通过 `list_device` 命令查看。 +- **初始化与销毁**:`rt_device_init` 用于初始化设备,`rt_device_close` 用于关闭设备,系统通过引用计数管理设备的打开次数。 +> 设备句柄是一个指向设备控制块(Device Control Block)的指针,这个指针指向了设备在内存中的地址,会作为`rt_device_create`的返回值返回,并由调用者(通常为驱动代码)保存在一个自定义变量中。 +> 设备控制块的大小是`struct rt_device`结构体大小和`attach_size`用户自定义数据大小之和 +> 设备控制块包含了设备的所有信息,如类型、名称、打开技术及一组操作硬件的函数指针等 + +`ops` 中的函数指针类似 Linux 字符设备驱动的 `file_operations`:框架负责统一入口,具体驱动负责实现 `init/open/read/write/control`。因此排查问题时可沿着“应用 API → `rt_device` 包装函数 → `ops` → 芯片 HAL/寄存器”逐层跟踪。 +#### 2. 回调机制与中断处理 +- **中断回调设置**:通过 `rt_device_set_rx_indicate` 和 `rt_device_set_tx_complete` 设置接收和发送回调函数,用于在中断或DMA模式下通知上层应用。 +- **回调链传递**:硬件中断触发底层回调,底层回调调用RT-Thread框架层的回调函数,最终通知上层应用。 + +回调函数中只做必要的“记账”和通知,例如把字节写入环形缓冲区、释放信号量;不要在回调中打印大量日志、执行阻塞式 I2C/SPI 或调用 `rt_thread_delay`。 +### 三、GPIO外设开发与实操演示 +通过按键中断实验,展示GPIO设备驱动的具体实现与调试技巧 +#### 1. GPIO设备驱动实现 +- **引脚模式配置**:支持输入(上拉/下拉)、输出、开漏输出等多种模式,通过 `rt_pin_mode` 配置。 +- **中断回调注册**:使用 `rt_pin_attach_irq` 绑定引脚中断回调函数,支持上升沿、下降沿、双边沿等多种触发模式。 +- **电平读写操作**:通过 `rt_pin_write` 设置电平,`rt_pin_read` 读取电平。 + +示例(按键低电平按下): +```c +rt_pin_mode(KEY_PIN, PIN_MODE_INPUT_PULLUP); +rt_pin_attach_irq(KEY_PIN, PIN_IRQ_MODE_FALLING, key_isr, RT_NULL); +rt_pin_irq_enable(KEY_PIN, PIN_IRQ_ENABLE); +``` +中断里发送一个事件,在线程中等待事件并消抖: +```c +/* ISR:只通知 */ +rt_event_send(&key_event, KEY_PRESS); +/* 线程:等待后延时确认电平 */ +rt_uint32_t set; +rt_event_recv(&key_event, KEY_PRESS, RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR, + rt_tick_from_millisecond(100), &set); +rt_thread_mdelay(20); +if (rt_pin_read(KEY_PIN) == PIN_LOW) handle_key(); +``` +#### 2. 实操问题与调试分析 +- **按键消抖问题**:实验中发现按键触发两次中断,是机械抖动导致,建议使用软件包(如button库)或延时方式进行消抖处理。 +- **调试与源码分析**:通过单步调试分析了GPIO驱动从上层API调用到底层STM32 HAL库的完整调用栈,验证了Ops函数指针的正确赋值。 +### 四、I2C与SPI总线驱动开发 +针对复杂总线设备,可以使用总线-设备模型 +#### 1. I2C总线通信机制 +IIC(Inter-Integrated Circuit)协议,也称I2C总线,是一种串行通信协议,通常用于连接低速外设。只需要两条数据线,分别是串行线SDA和串行时钟线SCL,适用基于芯片的通信,例如连接传感器、存储器或数字信号处理器等。 +- **消息结构体传输**:I2C通信通过构建 `struct rt_i2c_msg` 结构体进行,包含设备地址、读写标志、缓冲区指针及长度。 +- **总线锁死恢复**:针对I2C总线锁死问题,可通过产生9个时钟脉冲(SCL)的方式恢复总线。 +- **地址格式说明**:I2C设备地址通常为7位,传输时会左移一位并在末尾补上读写位(0写,1读)。 + +初学者最容易混淆的是地址:代码中的 `addr` 通常填写 **7 位地址**(如 `0x3C`),不要先手动左移;底层驱动会根据 `RT_I2C_WR/RT_I2C_RD` 生成最后一位读写位。若设备无响应,先确认地址、上拉电阻和电平,再检查波特率。 +#### 2. SPI总线设备挂载 +- **总线-设备模型**:SPI采用总线-设备模型,先注册SPI总线控制器,再将具体设备(如Flash)挂载到总线上。 +- **设备命名规则**:设备命名遵循 `spixy` 格式,x为总线号,y为设备序号(如spi10表示总线1上的0号设备)。 +- **配置参数**:SPI配置包括时钟频率、数据宽度(8位/16位)、传输模式(CPOL/CPHA)、数据位序(MSB/LSB)等。 + +SPI 没有 I2C 的地址,片选(CS)决定当前通信设备;同一总线挂多个设备时,每个设备通常有独立 CS。CPOL/CPHA 必须与芯片手册一致,模式错误常表现为读到全 `0x00` 或 `0xFF`。 +### 五、开发环境配置与工程差异 +对比基于芯片(BSP)和基于SDK(CubeMX)两种开发模式的差异: +#### 1. 两种开发模式对比 +- **基于芯片(BSP)模式**:适用于多种芯片适配,需要通过 `board.h` 和 `board.c` 手动配置引脚和时钟,流程相对繁琐。 +- **基于SDK(CubeMX)模式**:适用于特定芯片型号,可直接使用CubeMX生成的配置文件,配置流程自动化程度高。 +#### 2. 硬件I2C支持现状 +- **软件模拟与硬件支持**:当前部分芯片支持包仅支持软件模拟I2C(Soft I2C),若需使用硬件I2C,需手动更新驱动或使用主线版本。 + +Linux 用户可把 BSP 理解为“板级设备树/板级初始化”的简化版本:BSP 确定芯片、时钟、引脚和外设实例,驱动再把实例注册为 RT-Thread 设备。改动 `board.h/board.c` 后要重新编译并确认 Kconfig 中对应驱动选项已打开。 + + +