Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
RT-Thread一般讨论
【2024-RSOC】RT-Thread设备驱动框架与案例【Day4】
发布于 2024-07-25 23:23:28 浏览:312
订阅该版
[tocm] # 【2024-RSOC】RT-Thread设备驱动框架与案例【Day4】 **打开ulog组件过程** ``` menuconfig.exe >RT-Thread Components ---> >Utilities ---> >[*] Enable ulog ---> >[*] Enable ISR log. pkgs --update scons --target=vsc ``` ## RT-Thread I/O 设备模型 ### I/O 设备模型框架 RT-Thread 提供了一套简单的 I/O 设备模型框架,如下图所示,它位于硬件和应用程序之间,共分成三层,从上到下分别是 I/O 设备管理层、设备驱动框架层、设备驱动层。 ![day4io-dev.png](https://oss-club.rt-thread.org/uploads/20240725/5722a196503e198c48012a39f6354c8d.png) 在RT-Thread中,应用程序通过I/O设备管理接口与底层硬件进行交互。 1. **I/O 设备管理层**:提供了一组标准接口,使得应用程序可以透明地访问底层硬件设备。这一层封装了具体的设备驱动细节,使得应用程序无需关心驱动的具体实现,从而降低了代码的耦合性和提高了系统的可维护性。 2. **设备驱动框架层**:对同类硬件设备进行了抽象,将通用部分标准化,而将特定于制造商的部分留作驱动程序实现。这一层确保了不同厂家的同类硬件设备可以以一致的方式被管理和使用。 3. **设备驱动层**:直接与硬件交互的程序,实现了对硬件设备的控制和数据传输。驱动程序通过设备模型创建设备实例,并将其注册到I/O设备管理器中。 ![day4io-dev-ex.png](https://oss-club.rt-thread.org/uploads/20240725/5648b908334bbb7c7125d813f842f8b1.png.webp) 图中设备驱动框架层有很多 RT-Thread 写好的类,图中只列出2类,其他类用 “xxx” 来表示,这些省略的类及其管理接口可以在 RT-Thread 源码 components/drivers 目录下找寻,比如该目录下可以找到serial/i2c/spi/sensor/can 等等相关目录。 横向看是分层思想,纵向看是各类派生继承关系。从下到上不断抽象、屏蔽下层差异,体现了面向对象的抽象的思想。子类受到父类的接口约束,子类各自实现父类提供的统一接口,又体现了面向接口编程的思想。 ### 设备驱动流程 设备驱动层的实现遵循以下步骤: 1. **设备实例创建**:根据设备模型定义,创建具备硬件访问能力的设备实例。 2. **设备注册**:使用`rt_device_register()`接口将设备实例注册到I/O设备管理器中。对于简单的设备,可以直接注册,无需经过设备驱动框架层。 3. **设备查找**:应用程序使用`rt_device_find()`接口来查找所需的设备。 4. **设备访问**:通过I/O设备管理接口,应用程序可以访问硬件设备,执行读写操作、控制命令等。 ![day4io-call.png](https://oss-club.rt-thread.org/uploads/20240725/4325ead0a144794832cc2b65745a6d92.png) ## RT-Thread SPI设备驱动 ### SPI 协议工作原理 #### SPI简介 SPI(Serial Peripheral Interface,串行外设接口)是一种**高速、全双工、同步通信总线**,广泛应用于短距离通信场景中。SPI系统由一个主设备和一个或多个从设备组成,其中主设备负责提供时钟信号并发起数据交换,而从设备则根据主设备的时钟信号进行数据的接收和发送。 **SPI通信特点**: - **全双工**:SPI支持同时的数据发送和接收,即数据可以在两个方向上同时流动。 - **同步通信**:数据的发送和接收严格同步于时钟信号,确保了通信的稳定性和可靠性。 - **单主多从**:SPI系统中仅有一个主设备,它可以与一个或多个从设备进行通信。 - **片选信号**:每个从设备都有一个独立的片选信号线,用于选择特定的从设备进行通信。 SPI通信通常使用以下四根信号线: - **MOSI**(Master Out Slave In):主设备输出 / 从设备输入数据线。 - **MISO**(Master In Slave Out):主设备输入 / 从设备输出数据线。 - **SCLK**(Serial Clock):时钟信号线,由主设备提供。 - **CS**(Chip Select):从设备选择信号线,用于选择特定的从设备进行通信。 ![day4spi1.png](https://oss-club.rt-thread.org/uploads/20240725/ca6d2c86286c4173db4fa94a48125c32.png) #### SPI 工作流程 数据传输通常会包含一次数据交换。当主节点向从节点发送数据时,从节点也会向主节点发送数据。为此,主节点的内部移位寄存器和从节点被设置成环形。 ![day4CA_SPIBus03.gif](https://oss-club.rt-thread.org/uploads/20240725/67b1b7e03b3ceee6d1380acd74ae65bd.gif) 在数据交换之前,主节点和从节点使其内部移位寄存器加载存储器数据。产生时钟信号时,主节点会通过 MOSI 线同步输出其移位寄存器。同时,从节点在 SIMO 处从主节点读取第一位,并将其存储到存储器中,然后通过 SOMI 输出 **MSB**。主节点会在 MISO 处读取从节点的第一位,并将其存储到存储器中以待稍后处理。整个过程将一直持续,直至交换完所有数据位,然后主节点使时钟空闲并通过 /SS 禁用从节点。 SPI只有主模式和从模式之分,没有读和写的说法,外设的写操作和读操作是同步完成的。如果只进行写操作,主机只需忽略接收到的字节;反之,若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输。也就是说,你发一个数据必然会收到一个数据;你要收一个数据必须也要先发一个数据。 SPI主机和从机都有一个串行移位寄存器,主机通过向它的SPI串行寄存器写入一个字节来发起一次传输。 1. **选择从设备**:主设备通过拉低从设备的片选信号线(CS)来选择与之通信的从设备。 2. **数据加载**:主设备和从设备将待发送的数据加载到各自的移位寄存器中。 3. **时钟同步**:主设备产生时钟信号(SCLK),用于同步数据的发送和接收。 4. **数据交换**: - **主设备发送数据**:主设备通过MOSI(Master Out Slave In)线将数据一位一位地发送给从设备。 - **从设备接收数据**:从设备通过MISO(Master In Slave Out)线将数据一位一位地发送给主设备,同时接收来自MOSI线的数据。 - **数据移动**:主设备和从设备的移位寄存器在每个时钟周期移动一位数据,直到所有的数据位都被交换完毕。 5. **结束通信**:主设备释放从设备的片选信号线(CS),结束此次通信。 ![day4CA_SPIBus05.gif](https://oss-club.rt-thread.org/uploads/20240725/d06c6d076443f8de2d15cac6eab05d3e.gif) #### SPI通信的四种模式 SPI(Serial Peripheral Interface)通信支持四种不同的工作模式,这些模式定义了时钟信号的极性和相位。时钟极性(CPOL)和时钟相位(CPHA)的选择允许主设备和从设备之间实现精确的数据同步。以下是SPI通信的四种模式: **时钟极性(Clock Polarity ,CPOL)**用来决定在总线空闲时,同步时钟(SCLK)信号线上的电位是高电平还是低电平。 - **CPOL=0**:时钟信号在逻辑0(低电平)处空闲,有效状态为SCLK处于高电平时。 - **CPOL=1**:时钟信号在逻辑1(高电平)处空闲,有效状态为SCLK处于低电平时。 **时钟相位(Clock Phase ,CPHA)**用来决定何时进行信号采样。 - **CPHA=0**,在时钟的第一个跳变沿(上升沿或下降沿)进行数据采样,在第2个边沿发送数据(数据线上电平变化) ![day4cpha0.png](https://oss-club.rt-thread.org/uploads/20240725/3f7c98ab4892ede080eed06776364e02.png.webp) - **CPHA=1**,在时钟的第二个跳变沿(上升沿或下降沿)进行数据采样,在第1个边沿发送数据 ![day4cpha1.png](https://oss-club.rt-thread.org/uploads/20240725/c74e0cda5c44d256d340cb4692efb7fc.png.webp) **SPI模式** 1. **Mode 0 (CPOL=0, CPHA=0)**: - 时钟信号在逻辑0(低电平)处空闲。 - 数据在时钟信号的上升沿被采样,在下降沿发生变化。 2. **Mode 1 (CPOL=0, CPHA=1)**: - 时钟信号在逻辑0(低电平)处空闲。 - 数据在时钟信号的下降沿被采样,在上升沿发生变化。 3. **Mode 2 (CPOL=1, CPHA=0)**: - 时钟信号在逻辑1(高电平)处空闲。 - 数据在时钟信号的下降沿被采样,在上升沿发生变化。 4. **Mode 3 (CPOL=1, CPHA=1)**: - 时钟信号在逻辑1(高电平)处空闲。 - 数据在时钟信号的上升沿被采样,在下降沿发生变化。 CPOL 和 CPHA,能够允许时钟信号实现 180 度相移且数据延迟半个时钟周期。 ![day4CA_SPIBus04.gif](https://oss-club.rt-thread.org/uploads/20240725/d845b729dd4ab2ef017a373805dc499c.gif) **它们的区别是定义了在时钟脉冲的哪条边沿转换(toggles)输出信号,哪条边沿采样输入信号,还有时钟脉冲的稳定电平值(就是时钟信号无效时是高还是低)。每种模式由一对参数刻画,它们称为时钟极(clock polarity)CPOL与时钟期(clock phase)CPHA。** ### RT-Thread SPI设备 [官方文档中心](https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/device/spi/spi?id=%e6%8c%82%e8%bd%bd-spi-%e8%ae%be%e5%a4%87) 基本上官方的关于SPI设备注册和配置使用的API等都说的十分清楚了,因为时间有限,暂时就先不重述了。 ## I2C 总线设备 ### I2C 总线工作原理 #### I2C 总线简介 **I²C**(**Inter-Integrated Circuit**)字面上的意思是**集成电路之间**,它其实是**I²C Bus**简称,它是一种**串行**、**半双工**的通信总线,使用**多主从架构**,由飞利浦公司在1980年代为了让主板、嵌入式系统用以连接低速周边设备而发展。 I²C只使用两条双向漏极开路(Open Drain)线,其中一条线为传输数据的串行资料线(SDA, Serial DAta line),另一条线是启动或停止传输以及发送时钟序列的串行主频(SCL, Serial CLock line)线,这两条线上都有上拉电阻。I²C允许相当大的工作电压范围,但典型的电压准位为+3.3V或+5v。 是一种**多主控总线**,即可以在总线上放置任意多主节点。此外,在**停止比特(STOP)**发出后,一个主节点也可以成为从节点,反之亦然。 | 模式 | 速度 | | ------------------------------ | ------- | | 标准模式(Standard Mode) | 100kb/s | | 快速模式(Fast Mode) | 400kb/s | | 增强快速模式(Fast Mode Plus) | 1Mb/s | | 高速模式(High Speed Mode) | 3.4Mb/s | | 极速模式(Ultra-FastMode) | 5Mb/s | I2C具有如下特点: ①只需要两条总线;串行数据线(SDA)和串行时钟线(SCL)。 ②连接到总线的每个设备都是可通过唯一地址进行软件寻址的,并且始终存在简单的控制器/目标关系;控制器可以作为控制器发送器或控制器接收器运行。 ③这是一种真正的多控制器总线,包括冲突检测和仲裁,以防止两个或更多控制器同时启动数据传输时出现数据损坏。 ④面向8位的串行双向数据传输速率在标准模式下最高可达100 kbit/s,在快速模式下最高可达400 kbit/s,在快速增强模式下最高可达1 Mbit/s,在高速模式下最高可达3.4 Mbit/s。 ⑤串行、面向8位、单向数据传输,在超快速模式下最高可达5 Mbit/s。 ⑥片内滤波可抑制总线数据线上的尖峰信号,以保持数据完整性。 ⑦可以连接到同一总线的IC数量仅受最大总线电容的限制。在某些条件下(如简化SCL时钟频率、增加输出驱动力、增加缓冲器件、改进上拉电阻等),可以允许更大的电容。 ⑧极低的电流消耗,高抗扰度,宽电源电压范围,宽工作温度范围。 ⑨硬件的最简化,给芯片设计师减轻了节省输出引脚的压力,给芯片应用商带来了成本降低、空间减小、测试方便、易于升级等诸多好处,为芯片应用工程师的产品开发带来灵活多样的选择方案、方便快捷的调试手段、开发周期的缩短、开发效率的提高等好处。 #### 总线结构 IIC使用两根信号线进行通信,要求两根线都使用 开漏输出接上拉电阻 的配置,以此实现总线上所有节点SDA、SCL信号的 线与 逻辑关系。 Rp电阻的取值有一定的要求,太小会导致灌入电流过大,使’低’数据不稳定,甚至损坏端口;太大会导致信号上升缓慢,使得数据传输出错。在不同应用场景及供电电压下有不同的取值要求。 ![day4iic硬件结构.png](https://oss-club.rt-thread.org/uploads/20240725/4332c376ea60dcf3ccac1accc6d3d69c.png) 漏极开路/集电极开路的缺点是对于一个距离长的数据线,信号传输速率得不到有效保证。更长的走线对于输出驱动器表现为更大的容性负载,等效容性负载C和信号线的上拉电阻R构成RC振荡器。RC越大,意味着反射和振荡越强,从而影响总线的信号完整性。这也是I2C规范对总线电容值约束在400pf以内的原因。高速模式对信号完整性的要求更高,协议有定义相关SDA/SCL处理办法,以保证在数据线够长、速率够高时,信号完整性也可以得到满足。 #### 工作时序 IIC 的数据读取动作都在 **SCL为高** 时产生,SCL为低时是数据改变的时期,无论SDA如何变化都不影响读取。所以,传输数据的过程中,当SCL为高时,数据应当保持稳定,避免数据的采集出错。 ![day4iic数据有效性.png](https://oss-club.rt-thread.org/uploads/20240725/73d28c2588a11901d91d6e30f10c3ca3.png) **开始信号(START/S):** SCL为高时,SDA从高到低的跳变产生开始信号 **结束信号(STOP/P) :** SCL为高时,SDA从低到高的跳变产生结束信号 ![day4iic开始结束信号.png](https://oss-club.rt-thread.org/uploads/20240725/443945e81aeabfcafc1f6475117e92ca.png) I2C总线通信时每个字节为8位长度,数据传送时,先传送最高位,后传送低位,发送器发送完一个字节数据后接收器必须发送1位应答位来回应发送器,即一帧共有9位。I2C每次发送数据必须是8位,MSB固定,先发高位,再发低位。 ![day4iic字节格式.png](https://oss-club.rt-thread.org/uploads/20240725/8248bb1e983cb90e9054b40de0c8171c.png.webp) 协议规定数据传输过程必须包含应答(ACK)。接收器通过应答告知发送的字节已被成功接收,之后发送器可以进行下一个字节的传输。主机产生数据传输过程中的所有时钟,包括用于应答的第9个时钟。发送器在应答时钟周期内释放对SDA总线的控制,这样接收器可以通过将SDA线拉低告知发送器:数据已被成功接收。 **应答信号**分为两种: - 当第9位(应答位)为低电平时,为 ACK(Acknowledge)信号 - 第9位(应答位)为高电平时,为 NACK(Not Acknowledge)信号 主机发送数据,从机接收时,ACK信号由从机发出。当在SCL第9位时钟高电平信号期间,如果SDA仍然保持高电平,则主机可以直接产生STOP条件终止以后的传输或者继续RESTART开始一个新的传输。 从机发送数据,主机读取数据时,ACK信号由主机给出。主机响应ACK表示还需要再接收数据,而当主机接收完想要的数据后,通过发送NACK告诉从机读取数据结束、释放总线。随后主机发送STOP命令,将总线释放,结束读操作。 ![day4iic主机NACK.png](https://oss-club.rt-thread.org/uploads/20240725/5d37c4203ac8780f20fbd49d3d8da021.png.webp) 一个完整的7-bit通信过程 ![day4iic一个完整的7-bit通信过程.png](https://oss-club.rt-thread.org/uploads/20240725/0e7ad804ea0dcd9869ec803b1c25b7a4.png.webp) 主机对从机发送数据时,主机对从机发送一个开始字节,然后即可一直发送数据。以示例来讲解,其第一帧数据为要操作的寄存器地址,所以为:“[1-Byte]开始字节(写) + [1-Byte]寄存器地址 + [1-Byte]寄存器数据”。 ![day4iic方向不变.png](https://oss-club.rt-thread.org/uploads/20240725/49606faf118b18e346f3633b893c7ca7.png.webp) 主机对向从机读取数据时,方式同发送数据有所不同,要多一次通信过程。 主机需要先向从机发送一次信号,告诉从机”我要读取数据“,然后重开一次通信,等待从机主动返回数据。以示例来讲解,发送 “[1-Byte]开始字节(写) + [1-Byte]要读取的寄存器的地址”,之后结束通信,或者重开始,来进入到第二次通信中,先发送 [1-Byte]开始字节(读),然后等待读取从机发送过来的 [1-Byte]数据 即可。 ![day4iic主机读方向改变.png](https://oss-club.rt-thread.org/uploads/20240725/c1f2d057cbda92f889a15718461b657d.png) #### 通信过程 当总线空闲时,SDA 和 SCL 都处于高电平状态,当主机要和某个从机通讯时,会先发送一个开始条件,然后发送从机地址和读写控制位,接下来传输数据(主机发送或者接收数据),数据传输结束时主机会发送停止条件。传输的每个字节为8位,高位在前,低位在后。 1. **起始条件**:当SCL(时钟线)保持高电平时,SDA(数据线)从高电平变为低电平,表示通信开始。 2. **地址传输**:主设备发送7位从设备地址,后跟一位读写标志位(R/W)。该标志位指示后续操作是读(1)还是写(0)。 3. **应答(ACK)**:如果从设备位于总线上,它将发送一个ACK(低电平)作为应答。主设备检测到ACK后,根据读写标志位进入相应的模式(发送或接收)。 4. **数据传输**: - **写操作**:如果主设备要向从设备写数据,它将发送一个字节,随后从设备发送ACK作为应答。此过程重复进行,直到所有数据传输完毕。 - **读操作**:如果主设备要从从设备读取数据,它将不断接收字节,并发送ACK作为应答,除了最后一个字节,主设备发送非ACK(高电平)来表示数据接收完毕。 5. **停止条件**:当SCL保持高电平时,SDA从低电平变为高电平,表示通信结束。 - 从机地址发送完后可能会发送一些指令,依从机而定,然后开始传输数据,由主机或者从机发送,每个数据为 8 位,数据的字节数没有限制。 - 在一次通信过程中,主机可能需要和不同的从机传输数据或者需要切换读写操作时,主机可以再发送一个开始条件。 - 第一个字节和最后的停止信号一定是主机发给从机,但中间就不一定了。 #### 时钟同步与仲裁 时钟同步是通过I2C总线上的SCL之间的线“与”(wire-AND)来完成的,即如果有多个主机同时产生时钟,那么只有所有master都发送高电平时,SCL上才表现为高电平,否则SCL都表现为低电平。 I²C总线允许多个主设备竞争总线控制权。在多个主设备尝试启动通信时,仲裁机制确保只有一个主设备能够控制总线,以避免数据冲突。总线仲裁与时钟同步类似,当所有主机在SDA上都写1时,SDA的数据才是1,只要有一个主机写0,那此时SDA上的数据就是0。 - **竞争条件**:当两个或更多主设备试图在同一时间控制总线时,就会发生竞争条件。 - **仲裁过程**: - **数据位发送**:每个主设备开始发送地址或其他数据。 - **线与效应**:由于I²C总线采用开放式集电极或漏极开路连接,因此SDA线呈现为一个大的下拉电阻网络。如果一个主设备发送逻辑1而另一个主设备发送逻辑0,则总线上的实际电压将是0V,即逻辑0。 - **位比较**:每个主设备都会检查SDA线上的电压,如果发现发送的数据位与线上的数据位不符,则该主设备退出竞争。 - **仲裁获胜**:第一个发送较低数据位的主设备将赢得仲裁,并继续控制总线。 多个主机仲裁时,因为线“与”特性,谁低谁能强制SDA为低,也就是跟自己匹配,所以先高(高电平1)的那个就会仲裁失败。 ### RT-Thread IIC 总线设备 [官方文档中心](https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/device/i2c/i2c) 基本上官方的关于[访问 I2C 总线设备](https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/device/i2c/i2c?id=访问-i2c-总线设备)和配置使用的API等都说的十分清楚了,因为时间有限,暂时就先不重述了。 ## 结合实现RT-Thread的设备驱动框架实现一个自己的驱动 首先在`rt-thread\bsp\stm32\stm32f407-rt-spark\dist\project\libraries\HAL_Drivers\drivers\drv_a_test.o`新建一个自己要实现的驱动文件 ```c #include
#include
#include "board.h" #ifdef BSP_USING_TEST #define DRV_DEBUG #define LOG_TAG "drv.test" #include
static rt_err_t dev_test_init(rt_device_t dev) { LOG_I("test dev init"); return RT_EOK; } static rt_err_t dev_test_open(rt_device_t dev, rt_uint16_t oflag) { LOG_I("test dev open, flag = %d", oflag); return RT_EOK; } static rt_err_t dev_test_close(rt_device_t dev) { LOG_I("test dev close"); return RT_EOK; } static rt_ssize_t dev_test_read(rt_device_t dev, rt_off_t pos, void *buffer, rt_size_t size) { LOG_I("test dev read, pos = %d, size = %d",pos,size); return RT_EOK; } static rt_ssize_t dev_test_write(rt_device_t dev, rt_off_t pos, const void *buffer, rt_size_t size) { LOG_I("test dev write, pos = %d, size = %d",pos,size); return RT_EOK; } static rt_err_t dev_test_control(rt_device_t dev, int cmd, void *args) { LOG_I("test dev control, cmd = %d",cmd); return RT_EOK; } int rt_drv_test_init() { rt_device_t test_dev = rt_device_create(RT_Device_Class_Char,0); if(!test_dev) { LOG_E("test dev creat failed."); return -RT_ERROR; } test_dev->init = dev_test_init; test_dev->open = dev_test_open; test_dev->close = dev_test_close; test_dev->read = dev_test_read; test_dev->write = dev_test_write; test_dev->control = dev_test_control; if(rt_device_register(test_dev,"test_dev",RT_DEVICE_FLAG_RDWR)!=RT_EOK) { LOG_E("test dev register failed. check the name."); return -RT_ERROR; } return RT_EOK; } INIT_BOARD_EXPORT(rt_drv_test_init); #endif ``` 这段代码实现了一个简单的 RT-Thread 设备驱动程序,用于演示如何创建一个虚拟设备,并为其定义基本的操作接口。这个驱动程序包括了初始化、打开、关闭、读写和控制等基本功能。 其主要就是定义了下面几个**设备操作函数**: - **初始化函数** (`dev_test_init`):初始化设备,通常用于执行一些必要的设置,如配置寄存器等。在这个例子中,它只是记录一条初始化的日志。 - **打开函数** (`dev_test_open`):打开设备时调用,记录打开操作及其标志参数。 - **关闭函数** (`dev_test_close`):关闭设备时调用,记录关闭操作。 - **读取函数** (`dev_test_read`):从设备读取数据时调用,记录读取位置和大小,并返回成功状态。 - **写入函数** (`dev_test_write`):向设备写入数据时调用,记录写入位置和大小,并返回成功状态。 - **控制函数** (`dev_test_control`):处理设备控制命令时调用,记录命令编号,并返回成功状态。 其代码原型如下所示: ```c /** * operations set for device object */ struct rt_device_ops { /* common device interface */ rt_err_t (*init) (rt_device_t dev); rt_err_t (*open) (rt_device_t dev, rt_uint16_t oflag); rt_err_t (*close) (rt_device_t dev); rt_ssize_t (*read) (rt_device_t dev, rt_off_t pos, void *buffer, rt_size_t size); rt_ssize_t (*write) (rt_device_t dev, rt_off_t pos, const void *buffer, rt_size_t size); rt_err_t (*control)(rt_device_t dev, int cmd, void *args); }; ``` 同时定义了宏 `BSP_USING_TEST` 来控制是否编译这部分代码,并通过menuconfig和scons工具控制是否编译。 然后我们可以在`rt-thread\bsp\stm32\stm32f407-rt-spark\dist\project\libraries\HAL_Drivers\drivers\SConscript`这个文件下添加刚刚我们预设的宏以及文件名部分代码,用于控制scons工具是否将源文件编译。 ```c if GetDepend(['BSP_USING_TEST']): src += ['drv_a_test.c'] ``` 接下来在`rt-thread\bsp\stm32\stm32f407-rt-spark\dist\project\board\Kconfig`中添加menuconfig工具的config代码,这样就可以在工具中选择是否编译了。 ``` menu "Board extended module Drivers" config BSP_USING_TEST bool "Enable test driver" default n ``` 当在menuconfig中打开定义,就可以在应用层使用这个驱动了。 ```c #include
#include
#include
#include
#define LOG_TAG "drv.test" #define LOG_LVL LOG_LVL_DBG #include
static int dev_test_app(void) { rt_device_t test_dev; test_dev = rt_device_find("test_dev"); if(test_dev == RT_NULL) { LOG_E("can't find test_dev."); return -RT_ERROR; } rt_device_open(test_dev, RT_DEVICE_OFLAG_RDWR); rt_device_control(test_dev, RT_DEVICE_CTRL_CONFIG, RT_NULL); rt_device_write(test_dev, 100, RT_NULL, 1024); rt_device_read(test_dev, 20, RT_NULL, 128); rt_device_close(test_dev); return RT_EOK; } MSH_CMD_EXPORT(dev_test_app, dev_test_app); ``` ## 结合ath10、按键、多线程、中断、信号量实现的一个小案例 ```c #include
#include
#include
#include
#include "aht10.h" #define LOG_TAG "pin_irq_example" #define LOG_LVL LOG_LVL_DBG #include
static float humidity, temperature; static aht10_device_t dev; static int count; //新建一个线程专门用于读取传感器温湿度数据并将数据分别放入两个消息队列中 static rt_thread_t thread1 = RT_NULL; static rt_thread_t thread2 = RT_NULL; static rt_mq_t mq1 = RT_NULL; static rt_mq_t mq2 = RT_NULL; /* 定义一个线程入口 */ static void thread1_entry(void *parameter) { while (1) { /* 读取湿度 */ humidity = aht10_read_humidity(dev); rt_mq_send(mq1, &humidity, sizeof(humidity)); rt_thread_mdelay(500); } } static void thread2_entry(void *parameter) { while (1) { /* 读取温度 */ temperature = aht10_read_temperature(dev); rt_mq_send(mq2, &temperature, sizeof(temperature)); rt_thread_mdelay(500); } } /* 总线名称 */ static const char *i2c_bus_name = "i2c3"; // - 按键,4个,KEY_UP(兼具唤醒功能,PIN:PC5),KEY_DOWN(PIN:PC1),KEY_LEFT(PIN:PC0),KEY_RIGHT(PIN:PC4) #define KEY_PIN_L GET_PIN(C, 0) // PC0: KEY Left --> KEY #define KEY_PIN_R GET_PIN(C, 4) // PC4: KEY Right --> KEY #define KEY_PIN_U GET_PIN(C, 5) #define KEY_PIN_D GET_PIN(C, 1) void key_left_irq_handler(void *parameter) { /* 从队列中读取湿度并打印 */ rt_mq_recv(mq1, &humidity, sizeof(humidity), RT_WAITING_NO); LOG_D("humidity : %d.%d %%", (int)humidity, (int)(humidity * 10) % 10); int key_level = rt_pin_read(KEY_PIN_L); LOG_I("key left irq handler, key level: %d", key_level); } void key_right_irq_handler(void *parameter) { /* 从队列中读取温度并打印 */ rt_mq_recv(mq2, &temperature, sizeof(temperature), RT_WAITING_NO); LOG_D("temperature: %d.%d", (int)temperature, (int)(temperature * 10) % 10); int key_level = rt_pin_read(KEY_PIN_R); LOG_I("key right irq handler, key level: %d", key_level); } void key_up_irq_handler(void *parameter) { int key_level = rt_pin_read(KEY_PIN_U); LOG_I("key up irq handler, key level: %d", key_level); } void key_down_irq_handler(void *parameter) { int key_level = rt_pin_read(KEY_PIN_D); LOG_I("key down irq handler, key level: %d", key_level); } static int irq_pin_example(void) { count = 0; /* 等待传感器正常工作 */ rt_thread_mdelay(2000); /* 初始化 aht10 */ dev = aht10_init(i2c_bus_name); if (dev == RT_NULL) { LOG_E(" The sensor initializes failure"); return 0; } // 初始化按键引脚 rt_pin_mode(KEY_PIN_L, PIN_MODE_INPUT_PULLUP); rt_pin_mode(KEY_PIN_R, PIN_MODE_INPUT_PULLUP); rt_pin_mode(KEY_PIN_U, PIN_MODE_INPUT_PULLUP); rt_pin_mode(KEY_PIN_D, PIN_MODE_INPUT_PULLUP); // 注册按键中断 rt_pin_attach_irq(KEY_PIN_L, PIN_IRQ_MODE_FALLING, key_left_irq_handler, RT_NULL); rt_pin_attach_irq(KEY_PIN_R, PIN_IRQ_MODE_FALLING, key_right_irq_handler, RT_NULL); rt_pin_attach_irq(KEY_PIN_U, PIN_IRQ_MODE_FALLING, key_up_irq_handler, RT_NULL); rt_pin_attach_irq(KEY_PIN_D, PIN_IRQ_MODE_FALLING, key_down_irq_handler, RT_NULL); // 使能中断 rt_pin_irq_enable(KEY_PIN_L,PIN_IRQ_ENABLE); rt_pin_irq_enable(KEY_PIN_R,PIN_IRQ_ENABLE); rt_pin_irq_enable(KEY_PIN_U,PIN_IRQ_ENABLE); rt_pin_irq_enable(KEY_PIN_D,PIN_IRQ_ENABLE); // 初始化消息队列 mq1 = rt_mq_create("mq1", 10, sizeof(humidity), RT_IPC_FLAG_FIFO); mq2 = rt_mq_create("mq2", 10, sizeof(temperature), RT_IPC_FLAG_FIFO); // 创建线程 thread1 = rt_thread_create("thread1", thread1_entry, RT_NULL, 1024, 25, 10); if (thread1 != RT_NULL) { rt_thread_startup(thread1); } thread2 = rt_thread_create("thread2", thread2_entry, RT_NULL, 1024, 25, 10); if (thread2 != RT_NULL) { rt_thread_startup(thread2); } return RT_EOK; } /* 导出到 msh 命令列表中 */ MSH_CMD_EXPORT(irq_pin_example, irq pin example); ``` 该程序创建了两个线程来读取一个 AHT10 温湿度传感器的数据,并通过消息队列传递这些数据。此外,程序还为四个按键注册了中断处理函数,以便用户可以通过按键来读取消息队列中的数据并打印出来。 一开始是想要直接在中断中读取传感器数据的,结果读取传感器数据的函数中存在互斥锁,程序断言失败,于是加了单独的线程用于读取传感器数据并送入消息队列中,避开了这种限制。 下面是一些现象,当按下按键后即可打印出相关传感器数据。 ``` irq_pin_example msh >irq_pin_example msh >[13120] D/pin_irq_example: temperature: 26.8 [13120] I/pin_irq_example: key right irq handler, key level: 0 [15593] D/pin_irq_example: humidity : 72.7 % [15593] I/pin_irq_example: key left irq handler, key level: 0 [16533] D/pin_irq_example: humidity : 73.9 % [16533] I/pin_irq_example: key left irq handler, key level: 0 [17269] D/pin_irq_example: humidity : 73.9 % [17269] I/pin_irq_example: key left irq handler, key level: 0 [17447] D/pin_irq_example: humidity : 74.0 % [17447] I/pin_irq_example: key left irq handler, key level: 1 [17447] D/pin_irq_example: humidity : 73.9 % ```
0
条评论
默认排序
按发布时间排序
登录
注册新账号
关于作者
luyism
用舍由时,行藏在我
文章
4
回答
0
被采纳
0
关注TA
发私信
相关文章
1
有关动态模块加载的一篇论文
2
最近的调程序总结
3
晕掉了,这么久都不见layer2的踪影啊
4
继续K9ii的历程
5
[GUI相关] FreeType 2
6
[GUI相关]嵌入式系统中文输入法的设计
7
20081101 RT-Thread开发者聚会总结
8
嵌入式系统基础
9
linux2.4.19在at91rm9200 上的寄存器设置
10
[转]基于嵌入式Linux的通用触摸屏校准程序
推荐文章
1
RT-Thread应用项目汇总
2
玩转RT-Thread系列教程
3
国产MCU移植系列教程汇总,欢迎查看!
4
机器人操作系统 (ROS2) 和 RT-Thread 通信
5
五分钟玩转RT-Thread新社区
6
【技术三千问】之《玩转ART-Pi》,看这篇就够了!干货汇总
7
关于STM32H7开发板上使用SDIO接口驱动SD卡挂载文件系统的问题总结
8
STM32的“GPU”——DMA2D实例详解
9
RT-Thread隐藏的宝藏之completion
10
【ART-PI】RT-Thread 开启RTC 与 Alarm组件
热门标签
RT-Thread Studio
串口
Env
LWIP
SPI
AT
Bootloader
Hardfault
CAN总线
FinSH
ART-Pi
USB
DMA
文件系统
RT-Thread
SCons
RT-Thread Nano
线程
MQTT
STM32
RTC
FAL
rt-smart
ESP8266
I2C_IIC
WIZnet_W5500
ota在线升级
UART
PWM
cubemx
freemodbus
flash
packages_软件包
BSP
潘多拉开发板_Pandora
定时器
ADC
GD32
flashDB
socket
中断
Debug
编译报错
msh
SFUD
keil_MDK
rt_mq_消息队列_msg_queue
at_device
ulog
C++_cpp
本月问答贡献
踩姑娘的小蘑菇
7
个答案
3
次被采纳
张世争
8
个答案
2
次被采纳
rv666
5
个答案
2
次被采纳
用户名由3_15位
11
个答案
1
次被采纳
KunYi
6
个答案
1
次被采纳
本月文章贡献
程序员阿伟
6
篇文章
2
次点赞
hhart
3
篇文章
4
次点赞
大龄码农
1
篇文章
2
次点赞
ThinkCode
1
篇文章
1
次点赞
Betrayer
1
篇文章
1
次点赞
回到
顶部
发布
问题
投诉
建议
回到
底部