ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

RT-Thread SPI驱动框架深度解析:从总线到设备,掌握嵌入式驱动开发核心

RT-Thread SPI驱动框架深度解析:从总线到设备,掌握嵌入式驱动开发核心 1. 为什么一定要拆开SPI驱动框架从一次具体需求说起前几篇笔记一直在RT-Thread的设备模型里打转I2C框架、UART框架都过了一遍到了SPI这里我一开始其实是有点想跳过的。原因很简单SPI协议本身不复杂无非就是四根线——CLK、MOSI、MISO、CS时序上比I2C还更直观没有地址概念谁拉低CS谁就是主机当前要通信的对象。我当时觉得这种外设协议直接用HAL库的HAL_SPI_TransmitReceive一把梭不就行了为什么要去理解RT-Thread那套驱动框架直到我接了第一个项目板子上挂了一个SPI接口的Flash芯片一个SPI接口的LCD屏后面还打算挂一个SPI转CAN的模块。三颗芯片挂在同一条SPI总线上各自工作频率不同模式不同片选引脚下发到GPIO其中一个还要求支持DMA传输。这时候直接用HAL库一把梭的想法就崩了——如果应用层直接操作硬件那么每个外设都得重复写一遍SPI初始化代码切换设备时还得手动切换频率和模式LCD刷新过程中CPU被SPI忙等卡死DMA的事件回调散落各处代码很快成一锅粥。RT-Thread的SPI驱动框架解决的核心问题就是把SPI总线和SPI设备这两个概念彻底分开。总线负责管理时钟、模式、数据的底层搬运设备负责表达我是谁、我挂在哪个CS上、我需要什么参数。应用层对外设发起读写请求时不必关心当前挂的是Flash还是LCD。这跟Linux的SPI子系统设计思路同源但RT-Thread在嵌入式场景下做了大量简化代码量可控非常适合单芯片多外设的裸机升级场景。这篇笔记适合正在用RT-Thread做驱动开发、想把SPI外设驱动从能用提升到结构清晰的读者。我会从框架分层、核心数据结构、设备注册链路、数据传输链路、片选管理和STM32平台适配这几个维度把源码关键路径逐段拆开配合我实际调板时踩过的坑一起讲尽量让读完的人能直接上手写自己的SPI设备驱动而不是看到struct rt_spi_device就头皮发麻。2. 框架整体分层与核心结构体的翻译2.1 四层架构从应用到底层的职责切分看RT-Thread的SPI框架源码主要文件就集中在components/drivers/spi/目录下spi_core.c、spi_dev.c、spi_msd.c这个是多片选设备的支持后面细说。整个框架从上到下可以切成四层每一层的接口边界非常清楚第一层是应用层也就是我们写的业务代码通过rt_device_read/write或者rt_spi_transfer之类的API发起数据搬运完全不感知硬件细节。第二层是设备层由spi_dev.c实现。它把每一个挂在总线上的SPI外设抽象成一个rt_spi_device对象注册到RT-Thread的设备管理器中用户可以通过rt_device_find按名字找到它再通过标准设备接口操作。第三层是总线层由spi_core.c实现。它维护一条SPI总线上所有设备的信息负责总线上设备的配置频率/模式/数据宽度切换和在同一时刻只允许一个设备占用总线的互斥访问。第四层是驱动层也就是BSP里具体的drv_spi.c。这一层把RT-Thread的框架请求翻译成STM32 HAL库的调用或者直接操作寄存器。框架本身不关心底层芯片怎么实现时序它只定义了一组操作接口即struct rt_spi_ops底层驱动必须实现这些接口。理解这四层之后写驱动时最困难的反而不是协议本身而是哪一步应该由应用做哪一步由框架做哪一步必须我来做的边界感。有个通用判断标准凡是多设备共享的东西总线时钟、模式切换、CS选择策略尽量交给框架凡是某个设备独有细节私有数据、自定义片选逻辑、DMA中断服务放到设备驱动里。2.2 四个关键结构体总线、设备、配置、消息在源码里struct rt_spi_bus和struct rt_spi_device的定义结构清晰但初次接触的人容易混淆它们之间的关系。我习惯用一个比喻来理解SPI总线就像一条公路SPI设备就是公路边上的各个工厂大门CS就是每个工厂的门卫室。先看总线和设备结构体struct rt_spi_bus { struct rt_device parent; /* 继承标准设备对象 */ rt_uint8_t mode; /* 总线当前工作模式 */ const struct rt_spi_ops *ops; /* 底层驱动操作接口 */ struct rt_mutex lock; /* 总线互斥锁 */ struct rt_spi_device *owner; /* 当前占用总线的设备 */ }; struct rt_spi_device { struct rt_device parent; /* 继承标准设备对象 */ struct rt_spi_bus *bus; /* 所属总线 */ struct rt_spi_configuration config; /* 该设备的SPI配置 */ void *user_data; /* 设备私有数据 */ };struct rt_spi_bus代表一条物理SPI总线对应STM32的SPI1、SPI2或SPI3。关键在lock和owner这两个字段同一时刻只有owner指向的设备能占用总线其他设备想用必须等锁释放。而struct rt_spi_device代表挂在这条总线上的某个外设它的config字段保存该设备自己的SPI参数user_data则通常用来保存硬件片选的GPIO信息或自定义数据。再来看配置结构体和消息结构体struct rt_spi_configuration { rt_uint8_t mode; /* 模式时钟极性和相位 */ rt_uint8_t data_width; /* 数据宽度通常8位 */ rt_uint16_t reserved; /* 保留 */ rt_uint32_t max_hz; /* 最大通信频率 */ }; struct rt_spi_message { const void *send_buf; /* 发送缓冲 */ void *recv_buf; /* 接收缓冲 */ rt_size_t length; /* 传输长度 */ struct rt_spi_message *next; /* 下一跳消息指针 */ rt_uint32_t cs_take; /* 传输前是否拉低CS */ rt_uint32_t cs_release; /* 传输后是否释放CS */ };send_buf为RT_NULL表示只接收MOSI不发送数据recv_buf为RT_NULL表示只发送MISO数据丢弃两者都为非NULL就是标准的全双工读写。next指针把多个消息串成一个链表一次呼叫可以连续传输多段数据中间由CS信号来区分是连续还是断开。这个机制是我觉得整个框架设计里最巧妙的地方后面会单独讲。3. 设备注册链路全拆解从总线注册到设备挂载3.1 总线的注册rt_spi_bus_register每个SPI控制器驱动先初始化硬件然后调rt_spi_bus_register把总线对象挂到内核中。以STM32为例驱动层需要先定义一个struct rt_spi_bus实例填充ops接口然后调用注册函数rt_err_t rt_spi_bus_register(struct rt_spi_bus *bus, const char *name, const struct rt_spi_ops *ops);以BSP中的drv_spi.c为例实际代码大致是static struct rt_spi_bus spi1_bus; static const struct rt_spi_ops stm32_spi_ops { .configure spi_configure, .xfer spi_xfer, }; int stm32_spi_register(void) { /* 配置GPIO复用、SPI外设时钟等 */ ... return rt_spi_bus_register(spi1_bus, spi1, stm32_spi_ops); }注册成功后系统中就存在一个名为spi1的总线对象。注意这里bus-parent的设备类型是RT_Device_Class_SPIBUS如果用rt_device_find(spi1)也能找到它但一般不直接操作这个对象而是拿它来挂载设备。由于RT-Thread的BSP对同一款芯片可能支持多个SPI控制器实际代码中会用数组管理总线再通过适配层把HAL库的句柄对应上。我第一次用CubeMX生成完初始化代码后直接在驱动层调HAL库初始化发现框架虽然注册成功了但一传输就死机最后定位到是GPIO复用配置被CubeMX重复初始化覆盖了——这种问题需要你确认BSP里SPI引脚是框架自己配置还是交给CubeMXRT-Thread的HAL库适配层一般在board.c里通过宏控制切忌两边同时开否则引脚状态会被来回撕扯。3.2 设备的挂载rt_spi_bus_attach_device总线注册好了接下来要把具体外设挂到总线上。RT-Thread提供了两个重要的注册接口名字接近但行为差异不小rt_err_t rt_spi_bus_attach_device(struct rt_spi_device *device, const char *name, const char *bus_name, void *user_data); rt_err_t rt_spi_device_register(struct rt_spi_device *device, const char *name, const char *bus_name, void *user_data);rt_spi_bus_attach_device是旧版接口它把device挂到bus_name指定的总线上并且尝试从user_data中读取一个struct rt_spi_configuration指针初始化设备参数。在较新的RT-Thread版本5.0中rt_spi_device_register逐步取代了它参数形式上更接近其他设备驱动。二者核心逻辑都是根据名字找到总线把设备的bus字段指向该总线设置设备名然后挂到设备管理器。这里有个初学者常踩的坑设备名不一定非得是spi10这种带总线编号的名字你可以自己定义比如flash0、lcd只要系统内唯一就行。应用层通过rt_device_find查找设备时用的是这个名字。在BSP提供的drv_spi.c里通常还会配合使用rt_hw_spi_device_attach函数一次性完成CS引脚的GPIO初始化和设备挂载。这块在不同BSP中实现差异很大有的BSP直接在函数里硬编码了CS引脚有的则是在初始化表格里配置。用之前务必打开BSP源码确认。3.3 配置是如何生效的rt_spi_configure设备挂载后还不具备通信条件——你必须先用rt_spi_configure设置工作参数rt_err_t rt_spi_configure(struct rt_spi_device *device, struct rt_spi_configuration *cfg);配置参数包括mode、data_width、max_hz三个关键项。mode是SPI Mode 0~3的组合实质上是CPOL和CPHA两个位SPI ModeCPOL时钟极性CPHA时钟相位适用场景Mode 00空闲低电平0第一个边沿采样绝大多数Flash、LCDMode 10空闲低电平1第二个边沿采样部分传感器Mode 21空闲高电平0第一个边沿采样部分外设Mode 31空闲高电平1第二个边沿采样少数外设这个参数从外设数据手册的时序图直接读出来判断方法是看手册里SCLK空闲时是高还是低数据在哪个边沿被采样。我早期吃过一次亏拿到一个国产Flash芯片手册时序图画得模糊我按Mode 0去配读ID读出来全是0xFF后来用示波器抓MISO才发现器件其实在SCLK的下降沿读数据实际是Mode 3。调试SPI不通先用逻辑分析仪抓CS/CLK/MOSI/MISO四根线对照手册时序图先确认模式再去查代码这个顺序不能反。调用rt_spi_configure后框架会把配置写入device-config然后通过总线ops-configure提交给底层驱动执行。底层驱动根据新参数重算分频系数、设置CR1寄存器。注意rt_spi_configure并不会自动获得总线锁如果在多线程环境下调用配置需要自己保证互斥通常建议在设备初始化阶段一次性完成配置后续不要再频繁修改。4. 数据搬运的完整链路从API到底层寄存器4.1 同步传输接口的使用习惯RT-Thread SPI框架对外提供的主要接口是rt_spi_transfer和rt_spi_transfer_message二者都是同步阻塞式API底层驱动执行完了才返回rt_size_t rt_spi_transfer(struct rt_spi_device *device, const void *send_buf, void *recv_buf, rt_size_t length); rt_err_t rt_spi_transfer_message(struct rt_spi_device *device, struct rt_spi_message *message);rt_spi_transfer内部自动构造了一个rt_spi_message对象CS默认是一整次传输前拉低、传输完释放。适合最简单的一次性读写。rt_spi_transfer_message则更灵活你传进去的是一个消息链表头框架会串行处理链表中的每一个消息你可以自由控制每段消息的CS拉起和释放时机这是实现连续传输但分段处理的关键。需要注意rt_spi_transfer要求send_buf和recv_buf至少有一个非空而且默认按8位数据宽度处理。如果你的设备数据宽度不是8位或者想要DMA传输需要自己构造消息并通过rt_spi_transfer_message完成。另外rt_spi_transfer底层一定会调用rt_spi_take_bus和rt_spi_release_bus所以它是线程安全的但rt_spi_transfer_message不会自己获取总线锁调用前需要手动rt_spi_take_bus否则多线程同时操作同一总线上不同设备会出数据错乱。这个差异官方文档里写得不明显但是非常关键我写业务代码时就因为没注意到这一点两个线程一个读LCD一个写Flash互相抢总线最后轮到LCD执行时MISO上回来的全是Flash的回读数据。4.2 消息链与片选协同next指针的价值struct rt_spi_message里的next指针是理解整个框架灵活性的钥匙。它允许你把多个不连续的数据段打包成一条消息链一次性提交给驱动执行。典型场景是LCD驱动先发送命令字节再发送数据块中间不想释放CS想保持连续选中状态。此时可以构造两个消息第一个消息send_buf指向命令字节cs_take1cs_release0第二个消息send_buf指向数据块cs_take0cs_release1并把next指向第一个消息。框架会在传输完第一个消息后自动检查next继续传输下一个消息期间CS保持拉低状态。这里还有个细节cs_take和cs_release并不是在框架层直接操作GPIO的而是通过ops-xfer回调往底层传标志位。底层驱动通常用软件片选也就是直接控制GPIO输出电平来实现具体片选管理方式将在下一章细聊。实际项目里CS拉低的时机必须发生在第一个CLK脉冲之前CS拉高的时机必须在最后一个CLK脉冲结束后这两个时间点如果控制不好很多时序敏感外设会拒绝通信。软件片选中你在spi_xfer函数里用GPIO控制CS的话一定要操作顺序正确先拉低CS再使能SPI外设或开始数据传输。4.3 底层驱动如何执行ops-xfer的实现范式从框架层往下走真正的数据搬运发生在底层驱动的xfer回调中。RT-Thread里底层驱动的xfer回调签名如下rt_uint32_t (*xfer)(struct rt_spi_device *device, struct rt_spi_message *message);STM32 BSP里对应的实现思路大致是根据message-cs_take判断是否需要拉低CS引脚如果message-send_buf为空把发送寄存器填充为0xFF或者0x00视外设而定如果message-recv_buf为空把接收到的数据丢掉根据长度循环调用HAL库的HAL_SPI_TransmitReceive阻塞模式或者配置DMA中断模式根据message-cs_release判断是否需要拉高CS引脚返回实际传输的字节数。阻塞模式实现简单但会占着CPU空转等SPI外设完成这在长数据传输例如从Flash读几千字节时非常浪费。RT-Thread针对这种场景支持把SPI的DMA传输配置成中断方式但框架本身仍然是同步等待的——底层回调里启动DMA然后挂起当前线程DMA完成中断里唤醒线程。这个机制在spi_core.c里没有显式体现是各BSP自己实现的所以不同BSP对DMA的支持成熟度差别很大。STM32的BSP比较成熟配置rtconfig.h里的BSP_USING_SPI_DMA即可启用DMA但是要注意DMA中断优先级必须高于当前线程抢占优先级否则中断里唤醒线程不生效。4.4 从应用视角看完整调用序列把上面几段串起来一个典型调用流程是这样的/* 应用程序里 */ struct rt_spi_device *flash_dev; flash_dev (struct rt_spi_device *)rt_device_find(flash0); rt_spi_configure(flash_dev, flash_cfg); /* 1. 获取总线锁 */ rt_spi_take_bus(flash_dev); /* 2. 拉低CS */ rt_spi_take_cs(flash_dev); /* 3. 执行实际传输 */ rt_spi_transfer(flash_dev, send_buf, recv_buf, length); /* 4. 释放CS注意如果这是消息链的一部分可能不释放 */ rt_spi_release_cs(flash_dev); /* 5. 释放总线锁 */ rt_spi_release_bus(flash_dev);如果不手动调用rt_spi_take_bus和rt_spi_take_cs只调rt_spi_transfer框架内部也会帮你加上这些步骤效果等价。但是手动控制的好处是能在同一总线上连续发多条消息而只占用一次总线锁适合做批量操作。注意rt_spi_transfer_message在使用时如果你在消息里设置了cs_take和cs_release通常就不需要再手动调rt_spi_take_cs/release_cs了否则片选逻辑会乱套。5. 片选管理的两条路线硬件片选与软件片选5.1 软件片选的实现与代价在RT-Thread的SPI框架里片选并非常规意义上的总线内建功能它更像一个由设备驱动自己控制的旁路信号。STM32的SPI外设支持硬件NSS管理由SPI控制器在通信开始时自动拉低、结束时自动拉高但这在多数实际项目中反而不推荐。原因有三个STM32的硬件NSS引脚是固定的但很多板子上CS引脚接到其他GPIO硬件NSS用不上某些SPI外设要求CS在整个多段传输期间保持低电平硬件NSS对消息链的中间状态不好控制硬件NSS在SPI外设从模式切换为主模式时容易产生异常电平毛刺我见过一次因为NSS引脚配置不当导致Flash偶尔写错地址的灵异问题排查了整整两天。所以RT-Thread生态里最常见的是软件片选CS引脚配置为普通GPIO输出在ops-xfer回调里手动控制。设备驱动通过user_data保存CS引脚编号注册设备时传给框架。以STM32 BSP为例它的rt_hw_spi_device_attach函数里会解析CS引脚参数初始化GPIO并把这个引脚信息存到设备对象里。软件片选的代价是CS动作和SPI数据时钟之间并非硬件同步虽然实际时序通常是满足要求的CPU写GPIO寄存器很快但在极端高速率下可能引入几纳秒的偏差。如果你的外设对CS建立时间极其敏感可能需要额外加延时。另外软件片选要求CPU在传输开始和结束时及时执行GPIO操作如果这段时间CPU被更高优先级中断抢占CS的低电平窗口可能异常拉长。总而言之嵌入式常规场景下软件片选是性价比最高的方案。5.2 多设备共享总线的片选切换当一条总线上挂了多个设备时软件片选的切换顺序至关重要。RT-Thread的rt_spi_take_cs/rt_spi_release_cs操作的是当前owner设备的CS引脚底层的xfer回调会根据当前消息是否为cs_take来决定拉低哪个引脚。这里有个不容易察觉的细节总线锁是bus级别不是device级别的。也就是说rt_spi_take_bus拿到的是整条总线的访问权此时总线上只能有一个owner设备被选中——这正是设置bus-owner字段的意义。实际使用中如果两个设备片选引脚同时被拉低SPI主机发出的时钟信号会被两个从设备同时采样从设备可能互相干扰。因此多设备共享总线的代码可以遵循这个固定顺序rt_spi_take_bus(dev1); rt_spi_take_cs(dev1); rt_spi_transfer(dev1, cmd, buf, len); rt_spi_release_cs(dev1); rt_spi_release_bus(dev1);每次操作完立即释放避免一个设备占用太久导致另一个设备的实时性受损。如果你在ISR里也要访问SPI务必注意rt_spi_transfer内部可能触发线程调度中断里调用需要极度谨慎。一般来说SPI传输需要时间又涉及锁天生不适合放在中断上下文里执行最好在中断里只是置个标志位把实际SPI读写放到线程里统一执行。5.3 RT-Thread 5.x中多片选设备支持新版RT-Thread针对一个SPI从设备有多个片选的需求引入了spi_msd.c实现对多片选设备的支持。比如一颗Flash芯片内部可以分为多个逻辑分区每个分区拥有独立的CS但在操作时要保证要么都不选要么一次性选多个。这样的场景比较边缘普通项目用不到对大多数使用者来说理解上述软件片选逻辑加上消息链机制已经足够了。读源码时看到spi_msd.c可以先跳过不用被名字吓到。6. STM32平台上的落地HAL适配与实测踩坑6.1 drv_spi.c的核心实现逻辑大多数基于STM32的RT-Thread BSP里SPI驱动文件叫drv_spi.c。它的核心是两组函数一组是spi_configure负责把RT-Thread配置翻译成HAL库的SPI_InitTypeDef另一组是spi_xfer负责消息的实际传输。配置示例大致是static rt_err_t spi_configure(struct rt_spi_device *device, struct rt_spi_configuration *cfg) { SPI_HandleTypeDef *handle device-user_data; handle-Init.Mode SPI_MODE_MASTER; handle-Init.Direction SPI_DIRECTION_2LINES; handle-Init.DataSize SPI_DATASIZE_8BIT; handle-Init.CLKPolarity (cfg-mode RT_SPI_CPOL) ? SPI_POLARITY_HIGH : SPI_POLARITY_LOW; handle-Init.CLKPhase (cfg-mode RT_SPI_CPHA) ? SPI_PHASE_2EDGE : SPI_PHASE_1EDGE; handle-Init.NSS SPI_NSS_SOFT; handle-Init.BaudRatePrescaler calculate_prescaler(handle-Init.BaudRatePrescaler, cfg-max_hz); ... }然后通过HAL_SPI_Init应用参数。这个映射关系很直观需要注意的地方是calculate_prescaler需要根据cfg-max_hz计算预分频系数RT-Thread框架并不保证cfg-max_hz恰好能被HAL库支持的某个分频档位整除很多BSP里直接硬编码了一个预设值或者只粗略判断频率范围再映射到分频档位。例如408MHz的SPI时钟你要求8MHz预分频系数选择SPI_BAUDRATEPRESCALER_64实际速率是6.375MHz不够8MHz但满足不超过max_hz的要求。如果你对速率有精确要求得自己确认能接受实际频率误差。6.2 我踩过的三个大坑坑一SPI速率配置在传输过程中被意外覆盖RT-Thread里大部分BSP在spi_configure里都会关闭SPI外设再重新初始化因此每次配置都会触发一次SPI硬件的重新初始化可能产生单次毛刺并影响正在进行中的传输。如果某个设备驱动每次传输前都调用rt_spi_configure大批量读写时性能骤降。我是在做一个Flash读写测试时发现的通过逻辑分析仪观察CS波形正常情况应该是一个完整低电平窗口结果每次传输前CS都抖动了一下。原因是底层配置文件里设备驱动的初始化函数在传输前调用了rt_spi_configure而配置里带了rt_spi_release_bus加rt_spi_take_bus的隐含操作导致片选被短暂释放再拉起。解决方式很简单只初始化时配置一次之后稳定传输不要每次搬数据都重新喂配置。坑二DMA模式下CS释放时机不对启用SPI DMA后HAL_SPI_TransmitReceive_DMA是异步的函数返回时DMA还在搬运数据。如果代码在调用DMA后立刻把CS拉高那么后半段数据完全不会被设备收到。因此spi_xfer在DMA模式下不能偷懒必须等待DMA传输完成信号才能释放CS。STM32 BSP一般在传输结尾加一个信号量等待但不同版本写法差别很大。我自己遇到的是用旧版本BSP的DMA传输Flash读数据读出来的前几个字节对、后面全是0xFF抓波形发现CS在传输中途就拉高了。升级到新版本BSP后修复。如果你用DMA传输时发现数据尾部丢失先查CS的释放时序。坑三总线频率和GPIO速度配置不合理这是一个完全被忽略的问题。SPI的SCLK最大速率并不仅仅是SPI外设分频器能给出的频率还取决于GPIO的输出速度等级。STM32的GPIO输出速度一般有Low/Medium/High/Very High四档如果GPIO配置成Low2MHz而SPI时钟跑到20MHz那么波形上升沿和下降沿会被拉得很缓甚至产生尖峰高速下误码率极高。用RT-Thread BSP时很多驱动初始化代码里GPIO速度默认配成GPIO_SPEED_FREQ_VERY_HIGH10MHz级这通常没问题但有些移植场景下被改成Low档后忘了改回来。表现症状很迷低速通信正常提高速率就随机出错且没有一个统一的报错点。排查时先用逻辑分析仪看SCLK波形如果上升沿明显不够陡峭检查GPIO配置。6.3 设备驱动怎么写比较顺手基于以上框架我现在写SPI外设驱动的基本套路是这样的在board.h或设备配置表格里定义CS引脚和总线名使用BSP提供的rt_hw_spi_device_attach或手动调用rt_spi_device_register挂载设备在设备初始化函数里调用rt_spi_configure设置模式、宽度、频率写read/write回调函数注册到标准设备接口或者直接封装成业务函数调用rt_spi_transfer如果需要DMA把DMA初始化放在设备初始化阶段中断里只做信号量释放。实践下来最理想的状态是应用层不知道底层是SPI还是别的什么只知道我发命令、收数据这些操作通过标准设备接口统一进行。这样以后换总线、换芯片只需改设备注册配置和底层驱动应用代码完全不动。7. 和Linux SPI框架的对照RT-Thread做了哪些取舍7.1 分层思想同源但复杂度差距明显Linux SPI子系统的结构是spi_controller代表控制器spi_device代表从设备spi_message和spi_transfer代表一次传输请求底层还有spi_master队列、spi_driver匹配机制等。如果接触过Linux再看RT-Thread的SPI框架一眼就能看出设计思想上是一脉相承的。二者最大的不同在于Linux的消息队列是异步的、可以批量排队由内核线程调度执行驱动通过complete等待完成而RT-Thread的传输API默认就是同步阻塞的直接在当前线程上下文完成整个搬移过程没有内核线程介入。这个差异带来的直接后果是RT-Thread的SPI传输会阻塞当前任务你没法在一个线程里发起传输后立刻去干别的。对于大多数MCU应用场景这反而是好事——代码逻辑清晰简单不会出现异步完成回调把调用栈搞得很深的问题。对于Linux那种需要云端多路并行、高性能传输的场景同步模型反而受限制。但注意RT-Thread其实也可以通过DMA信号量在底层模拟异步效果只是API层面看还是同步的这对使用者友好。7.2 消息结构的差异next指针 vs transfer链Linux SPI里一次spi_message内部用transfer_list链表串起多个spi_transfer框架会按顺序执行并保证在最后一个transfer完成后才调用complete。RT-Thread的rt_spi_message类似的但把链表指针直接放在每个消息头部的next字段在处理多段消息时直接用循环迭代没有额外封装一个容器结构。这个简化让代码更加直白调试时也更容易打印每一段消息的属性和数据状态。我个人觉得RT-Thread的模型在MCU上更合理因为MCU场景下消息数量通常很少不需要Linux那样的通用性和可扩展性。7.3 设备模型与配置管理的差异Linux通过device tree描述SPI设备、片选、频率等参数驱动通过spi_get_device_id或of_get_property获取RT-Thread在MCU上更常用的是通过注册时的user_data指针传递硬件信息或者直接在BSP的stm32_spi_attach表格里面写死。这本质上是两种场景的取舍Linux面对的是千奇百怪的板级配置和热插拔设备必须靠运行时解析MCU项目板级电路固定方案简单直接把信息放到代码里编译进去就够了。作为一个做嵌入式应用的人我反而觉得RT-Thread这种方式更可控至少不会出现设备树里引脚配置错误导致整个系统启动失败还找不到原因的情况。8. 写在最后关于框架学习的个人建议花了不少篇幅把SPI框架的关键路径拆了一遍。回看整个分析过程我的最大体会是学习一个驱动框架不能光看API手册也不能一上来就扎进源码的每个角落。正确路径是先搞清楚它主要解决什么问题然后抓住几条关键链路注册、配置、传输、片选沿着代码往下走最后回到自己的板子上做实验验证理解。单说SPI驱动框架其实没有玄学核心就是三层设备层管个性总线层管共享驱动层管硬件翻译。你把这三个管理清楚了后续不管换什么芯片、挂什么外设无非就是套模板。真正难的不是理解框架本身而是理解你的外设——它需要什么模式、要不要连续传输、能不能接受DMA中断延迟这些信息看一眼数据手册和示波器波形远比抠框架代码更能解决实际问题。最后分享一个我自己调试SPI的黄金流程先确认发送端波形是否正确MOSI、SCLK正常再确认CS时序是否符合外设要求接着确认接收端MISO是否有响应最后再逐步提高频率。如果第一步就抓不到波形别急着怀疑框架先查GPIO复用和时钟使能那才是SPI不通的最大概率根源。
返回列表