【智能家居项目】裸机版本——项目介绍 | 输入子系统(按键) | 单元测试

🐱作者:一只大喵咪1201
🐱专栏:《智能家居项目》
🔥格言:你只管努力,剩下的交给时间!
请添加图片描述

目录

  • 🏀项目简介
  • 🏀输入子系统(按键)
    • ⚽应用层
    • ⚽设备层
    • ⚽ 内核层抽象层
    • ⚽芯片抽象层
    • ⚽硬件操作
  • 🏀按键单元测试
    • ⚽串口
    • ⚽测试
  • 🏀源码
  • 🏀总结

在这个专栏中,本喵要实现一个智能家居的小项目,先基于HAL库实现裸机版本,之后再实现一个RTOS版本,为了无缝实现从裸机到RTOS的移植以及维护,本喵会使用面向对象的思想,将整个项目分层来实现,构建一种编程架构。

本项目重点:

  • 设计出优秀的程序框架:容易扩展、容易维护。
  • 具体:
  • 把项目拆分为各个子系统。
  • 使用面向对象的思想,把子系统抽象为结构体。
  • 编写函数时,有一定的封装细节,看函数名就知道怎么用,不需要深入函数内部看它的实现。

🏀项目简介

图
如上图,使用百问网的STM32F103ZET6开发板,实现:

  • 开发板启动后,自动连接家里的路由器,在OLED上显示出IP。
  • 手机上启动微信小程序,输入开发板OLED上显示的IP,连接开发板。
  • 在微信小程序里,点击图标控制开发板的LED、风扇。

图
如上图所示,在程序设计过程中,分为几个层次:

  • 第1层:软件系统,就是整个系统、整个程序。
  • 第2层:分解为子系统,比如我们可以拆分为:输入子系统、显示子系统、业务系统。
  • 第3层:分解为类,在C语言里没有类,可以使用结构体来描述子系统。
  • 第4层:分解成子程序,实现那些结构体中的属性和方法(结构体中有函数指针)。

图

如上图所示,在本项目中,可以分为6个子系统:

  • 设备子系统:比如实现LED控制、风扇控制。
  • 显示子系统:在OLED上显示信息。
  • 输入子系统:可以接收按键数据、网络数据。
  • 网络子系统:负责网络连接、数据收发。
  • 字体子系统:获得字符的字库。
  • 业务子系统:起综合作用,根据输入值(网络数据),控制设备。

其中业务子系统包含其余5个子系统,可以看作是上层,并且同样也可以看作一个子系统。

🏀输入子系统(按键)

图
首先来实现输入子系统,它可以接收来自按键,网络,标准输入等设备的数据,然后供上层业务子系统去使用。整个输入子系统划分为五个层次实现,这里本喵仅实现按键一个输入设备。

⚽应用层

  • 对于传递的"数据数据",我们把它称为"输入事件"。

图

如上图,在input_system.h输入子系统头文件中定义输入事件结构体,用来描述发生的输入事件,无论是按键输入还是网络以及标准输入,都会创建一个这样的结构体对象,但是INPUT_EVENT_TYPE不同,只有根据该成员变量的值才可以确定发生了哪种输入,通过其他成员变量可以获取到需要的事件属性,比如发生事件,按键编号,以及字符串数据等等。

输入事件类型有多种,在这个项目中并不会用到触摸屏输入,本喵这样写是为了表明拓展维护的方便性,在输入子系统层面,需要增加输入事件类型,以及描述输入事件的结构体InputEvent中增加触摸屏触摸的位置。


接下来就是输入事件的来源了,从框图中看到有按键输入,网络输入,标准输入,以后甚至可以扩展更多的输入来源,这些输入来源产生输入事件。

  • "输入事件"由"输入设备"产生。

图
如上图,在input_system.h输入子系统头文件中定义输入设备结构体,用来描述输入设备,每一个设备都会创建一个这样的结构体对象,其中包含设备的名称,获取输入事件,初始化设备,去初始化设备等方法,以及下一个设备节点的指针。

每一个设备都自带获取输入事件的方法,也就是获取InputEvent对象的函数,站在输入子系统的层面,它并不关心该方法是如何实现的,只在需要获取输入数据的时候直接调取该方法即可。

包括初始化和去初始化也是设备自带的方法,上层只需要直接调用即可,至于去初始化是在不需要某个设备的时候,将其配置恢复到初始化状态,从系统中抹除该设备。

为了管理多个设备,本喵将其放在一个链表中,所以还有一个pNext指针指向下一个设备节点。

  • 在输入子系统层面,并不关心获取输入事件函数是如何实现的,而且该函数的实现涉及到了硬件底层,所以并不在子系统层面实现。

图
如上图,在input_system.c源文件中,创建一个全局链表,用来让输入子系统管理输入设备,并且实现注册输入设备,增加输入设备,初始化所有输入设备等函数。

注册输入设备的本质就是将新增加的输入设备节点插入到链表中,让输入子系统能够通过操作链表来维护使用输入设备。

增加输入设备也是输入子系统要处理的事情,在增加输入设备函数中再调用增加具体输入设备的函数,需要增加多少输入设备,就将对应设备的增加函数放进去。

初始化所有输入设备的时候,只需要变量链表中的设备节点,调用每个设备节点自带的初始化化函数即可。

  • 这三个函数要在input_system.h中声明。

无论是一个输入设备还是多个输入设备,所产生的数据并不只一个,但是使用者只有输入子系统一个,为了防止数据丢失,所以这些数据也需要维护起来,这里使用环缓冲区列来维护,主要有输入事件产生,就将相应的InputEvent对象放入环形缓冲区中,子系统只需要从环形缓冲区读取数据就可以,不用关心数据是怎么来的。
图
如上图所示,环形缓冲区本质上也是一个数组,就拿写来说,当这个数组被写满后ring_buffer[7] = data,就通过取模运算pW = (7 + 1) % 8 = 0重新从数组的起始位置开始写数据,读也是类似的道理。

  • pR是向环形缓冲区读数据时的下标。
  • pW是向环形缓冲区写数据时的下标。

通过pR是否等于pW来判断环形缓冲区中是否有数据,没有数据就相等,有数据就不相等,同样通过pW是否等于pR来判断环形缓冲区是否写满数据,相等就写满了,不相等就没写满。

图
如上图所示,定义环形缓冲区结构体,通过维护pWpR来维护环状,以及从存放输入事件的buffer中读写事件。

输入子系统还需要提供读写数据的方法:

图
如上图所示,创建一个全局的环形缓冲区对象,由于是静态全局变量,且没有初始化,所以编译器会用0去初始化,并放在未初始化数据段,读写事件都是在操作这个全局的环形缓冲区。


图
此时,输入子系统已经具有了上图所示结构以及对应的操作方法,输入子系统的层就完成了,到目前位置丝毫没有提及到和STM32F103ZE开发板有关的内容,连一句相关的代码也没有,实现了应用层和硬件的解耦。


⚽设备层

此时输入子系统中的上层部分已经完成了,还需要处理输入子系统设备层,这里本喵仅实现按键输入设备:

图
如上图所示,在gpio_key.h中定义了两个按键的编号,之后直接使用即可。

图
如上图所示,在gpio_key.c中实例化出一个按键对象,并进行初始化,赋值设备名,初始化函数等,还要提供一个增加按键设备的函数AddInputDeviceGPIOKey供应用层在初始化所有设备时候调用。

  • 对于裸机程序,事件获取方法不用注册到设备队列中,而是在后面中断函数中调用。

图
此时,已经实现了按键的设备层,包括按键设备的实例化,按键设备的初始化方法,以及增加按键设备的方法。

⚽ 内核层抽象层

本喵想让这个系统支持多个系统,包括裸机,FreeRTOS,RT-Thread,甚至是Linux,这里将裸机也看作是一种内核。

不同内核下的数据来源:

  • 裸机:数据来自中断,在中断中解析数据并放入环形缓冲区。
  • RTOS:创建任务,在任务中解析数据并放入环形缓冲区。

内核抽象层中,根据不同的内核对按键进行初始化,本喵这里仅实现裸机的按键初始化:

图

如上图,初始化按键的时候,调用KAL_GPIOKeyInit,在函数内部再调用不同内核对按键的初始化函数,对于裸机则调用芯片层的CAL_GPIOKinit函数进行初始化,如果是RTOS,则仅需要将该函数改成对应的初始化函数即可。

  • 设备抽象层调用的是该层的KAL_GPIOKeyInit,根本不关心具体的实现逻辑。

在描述输入事件的结构体InputEvent中有一个time成员变量用来记录事件发生的事件,而这个时间在不同的内核中表现方式不同,所以在内核抽象层需要实现获取时间的函数。

图
如上图,在使用的时候,直接调用内核抽象层的KAL_GetTime获取时间即可,在该函数内部,根据具体的获取方式调用对应的函数。

如本喵使用的STM32F103ZET6是通过滴答定时器来获取时间的,需要获取芯片中寄存器的值,所以要调用CAL_GetTime从芯片获取时间。

对于Linux,它在系统内部会记录着时间,此时就可以直接返回时间,不用再向下调用。

图

此时,内核抽象层也实现了,设备层会调用内核抽象层的初始化函数。

⚽芯片抽象层

项目的最终实现需要依托具体的芯片,本喵用的STM32F103ZET6是支持HAL库的,但是也有一些芯片并没有HAL库,需要用它自己的库来操作,所以在这一层要实现对不同类型芯片的支持。

图

如上图,在芯片抽象层会调用CAL_GPIOKeyInit来初始化按键,在函数内部根据不同的芯片再调用它对应的初始化函数,如ST芯片就调用KEY_GPIO_ReInit


同样,不同芯片获取时间的方式也不同,这里也要实现针对不同芯片获取时间的方式:

tu
如上图所示,从芯片寄存器中获取时间的时候,对于ST芯片,调用HAL_GetTick获取即可,对于其他芯片,放入对应的获取方式即可。

图

此时芯片抽象层也实现了,内核抽象层会调用该层的CAL_GPIOKeyInit初始化按键。

⚽硬件操作

本喵使用的是STM32F103ZET6芯片,使用CubeMXHAL库进行按键初始化,在初始化的时候,要在中断函数中进行输入数据的读取,并放入环形缓冲区中。

图
如上图,在driver_key.h中进行一些芯片的资源定义,方便后面使用。

图

如上图,使用HAL库对按键进行初始化,在按键中断函数中处理输入事件InputEvent并且放入到环形队列中

图
此时,具体芯片的硬件配置也设置好了,输入子系统中按键设备就完全写好了。


图
如上图,现在整个代码结构是这样,其中智能家居项目部分全部放在了smartdevice文件夹中,包含输入子系统的应用层,设备抽象层,内核抽象层,芯片抽象层。

其余部分是通过CubeMX进行的基本外设配置,整个输入子系统中,只有在硬件操作的时候会用到这里的配置,其余四层都是独立的,不存在耦合。

🏀按键单元测试

⚽串口

为了观察按键按下后的现象,使用串口将发生的输入事件InputEvent打印出来,此时串口配置并不属于我们实现的输入子系统,只是一个调试工具,直接使用HAL库配置就可以。

图
如上图所示是串口的头文件,只包含串口的使能和失能函数声明。

图

如上图所示是串口的具体配置函数,这里同样需要一个环形缓冲区,这里本喵就不展示它的实现了,后面本喵会放源码。

在调用EnableDebugIRQ打开串口后,在向串口发送数据的时候,直接调用printf即可,因为printf底层会调用fputc函数,所以需要在这里将fput重定向,使得printf符合我们的要求。

fputc中,先将发送完成标志清0,然后调用HAL库的中断发送函数发送一个字节,当发送完成标志位为0时就一直等待,说明没有发送完成。这个字节发送完成以后,会进入串口的发送中断回调函数,在中断函数中将发送标志位置1,让fputc退出循环等待。printf发送多个字节就调用多次fputc

在获取串口发送来的数据时,直接调用scanf即可,因为scanf底层会调用fgetc函数,所以也需要重定向fgetc函数,使得scanf符合我们的要求。

当串口上有数据到来时,会发生串口中断,通过判断SR寄存器的第五位确定是接收到了数据,并且将接收到的数据放入到环形缓冲区中。fgetc直接从环形缓冲区中读取数据。

  • 为了像在PC端一样使用标准库中的printfscanf,必须重新实现fputcfgetc函数,让终端变成串口,符合我们的要求。

⚽测试

为了看我们设计的输入子系统是否正确,需要专门写一个单元测试函数来测试一下:

图
如上图所示,将按键设备添加到输入子系统中,然后进行初始化,在while(1)循环中读取输入事件,并通过串口打印输入事件的信息。

main函数中调用该测试函数,通过串口调试助手查看打印信息:

tu
如上图,将板子的串口和电脑连在一起后,通过串口调试助手可以看到,当按键1或者按键2按下后,会打印出发生的事件信息,包括事件类型,发生事件,按键编号,以及按键值,说明设计的输入子系统是成功的。

🏀源码

这部分代码是在OLED代码的基础上写的,包含源码以及串口调试工具,需要的小伙伴自取传送门。

🏀总结

这篇文章实现了智能家居项目中输入子系统中的按键设备,最重要的是介绍的代码框架和编程思想,之后的项目部分都会按照这个思路来扩展维护。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/145656.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

每日一练-Q1-大数加法-20231001

目录 1.题目描述 2.输入描述 3.示例提示 4.问题分析 5.通过代码 1.题目描述 大数一直是一个c语言的一个难题。 现在我们需要你手动模拟出大数加法过程。 请你给出两个大整数加法结果。 2.输入描述 第一行输入整数n&#xff0c;第二行输入整数m。 (1<number<1e100)…

在nodejs中如何防止ssrf攻击

在nodejs中如何防止ssrf攻击 什么是ssrf攻击 ssrf&#xff08;server-side request forgery&#xff09;是服务器端请求伪造&#xff0c;指攻击者能够从易受攻击的Web应用程序发送精心设计的请求的对其他网站进行攻击。(利用一个可发起网络请求的服务当作跳板来攻击其他服务)…

10月1日作业

汇编指令合集 用select实现服务器并发代码 #include<myhead.h> #define IP "192.168.0.106" #define PORT 8888int main(int argc, const char *argv[]) {//新建套接字文件int sfd socket(AF_INET, SOCK_STREAM, 0);if(sfd < 0){ERR_MSG("socket&quo…

多线程学习笔记(一)

文章目录 1 线程基础知识复习2 CompletableFuture1、Future和Callable接口2、FutureTask3、对Future的改进4、案例精讲——电商5、常用方法6、CompetableFutureWithThreadPool【重要】 3 锁1、乐观锁和悲观锁2、synchronized 8锁案例3、公平锁和非公平锁4、可重入锁5、死锁及排…

web:[RoarCTF 2019]Easy Calc

题目 进入页面是一个计算器的页面 随便试了一下 查看源代码看看有什么有用的信息 访问一下这个calc.php 进行代码审计 <?php error_reporting(0); if(!isset($_GET[num])){show_source(__FILE__); }else{$str $_GET[num];$blacklist [ , \t, \r, \n,\, ", , \[, \]…

Win11下无法打开丛林之狐,提示未检测到DirectX 8.1

新装的win11系统&#xff0c;打开丛林之狐提示未检测到DirectX 8.1. 运行dxdiag检查DirectX版本&#xff1a; DX版本已经是12了&#xff1a; 最终参考了这篇文章解决了&#xff1a; 罪恶都市出现XX-directx version 8.1处理方法 - 知乎 控制面板 > 程序 > 启用或关闭Wi…

蜂蜜配送销售商城小程序的作用是什么

蜂蜜是农产品中重要的一个类目&#xff0c;其受众之广市场需求量大&#xff0c;但由于非人人必需品&#xff0c;因此传统线下门店经营也面临着痛点&#xff0c;线上入驻平台也有很多限制难以打造自有品牌&#xff0c;无法管理销售商品及会员、营销等&#xff0c;缺少自营渠道&a…

力扣 -- 718. 最长重复子数组

解题步骤&#xff1a; 参考代码&#xff1a; class Solution { public:int findLength(vector<int>& nums1, vector<int>& nums2) {int m nums1.size();int n nums2.size();//多开一行&#xff0c;多开一列vector<vector<int>> dp(m 1, ve…

【中秋国庆不断更】OpenHarmony多态样式stateStyles使用场景

Styles和Extend仅仅应用于静态页面的样式复用&#xff0c;stateStyles可以依据组件的内部状态的不同&#xff0c;快速设置不同样式。这就是我们本章要介绍的内容stateStyles&#xff08;又称为&#xff1a;多态样式&#xff09;。 概述 stateStyles是属性方法&#xff0c;可以根…

【数据库——MySQL】(13)过程式对象程序设计——存储函数、错误处理以及事务管理

目录 1. 存储函数2. 存储函数的应用3. 错误处理4. 抛出异常5. 事务处理6. 事务隔离级7. 应用实例参考书籍 1. 存储函数 要 创建 存储函数&#xff0c;需要用到 CREATE 语句&#xff1a; CREATE FUNCTION 存储函数名([参数名 类型, ...])RETURNS 类型[存储函数体]注意&#xff1…

【计算机网络】DNS原理介绍

文章目录 DNS提供的服务DNS的工作机理DNS查询过程DNS缓存 DNS记录和报文DNS记录DNS报文针对DNS服务的攻击 DNS提供的服务 DNS&#xff0c;即域名系统(Domain Name System) 提供的服务 一种实现从主机名到IP地址转换的目录服务&#xff0c;为Internet上的用户应用程序以及其他…

最快的包管理器--pnpm创建vue项目完整步骤

1.用npm全局安装pnpm npm install -g pnpm 2.在要创建vue项目的包下进入cmd&#xff0c;输入&#xff1a; pnpm create vue 3.输入项目名字&#xff0c;选择Router,Pinia,ESLint,Prettier之后点确定 4.cd到创建好的项目 &#xff0c;安装依赖 cd .\刚创建好的项目名称\ p…

Redis配置和优化

Redis配置和优化 一 、Redis介绍二、关系数据库和非关系数据库2.1、关系型数据库2.2、 非关系型数据库2.3、 非关系型数据库的产生背景2.4、 关系型数据库和非关系型数据库区别2.5、 总结 三、缓存概念3.1、系统缓存3.2、 缓存保存位置及分层结构3.2.1、DNS缓存3.2.2、 应用层缓…

笔试练习day01

目录 一、选择题 1、格式化输出 2、逻辑判断--短路原则 3、赋值语句 4、左移、异或 二、编程题 1、组队竞赛 2、删除公共字符 一、选择题 1、格式化输出 知识点&#xff1a;格式化输出&#xff0c;%m.ns 2、逻辑判断--短路原则 知识点&#xff1a; else和最近的if匹…

第一次作业题解

第一次作业题解 P5717 【深基3.习8】三角形分类 思路 考的是if()的使用,还要给三条边判断大小 判断优先级&#xff1a; 三角形&#xff1f;直角、钝角、锐角等腰等边 判断按题给顺序来 代码 #include <stdio.h> int main() {int a 0, b 0, c 0, x 0, y 0, z 0…

排序篇(二)----选择排序

排序篇(二)----选择排序 1.直接选择排序 基本思想&#xff1a; 每一次从待排序的数据元素中选出最小&#xff08;或最大&#xff09;的一个元素&#xff0c;存放在序列的起始位置&#xff0c;直到全部待排序的数据元素排完 。 直接选择排序: ​ 在元素集合array[i]–array[…

pytorch函数reshape()和view()的区别及张量连续性

目录 1.view() 2.reshape() 3.引用和副本&#xff1a; 4.区别 5.总结 在PyTorch中&#xff0c;tensor可以使用两种方法来改变其形状&#xff1a;view()和reshape()。这两种方法的作用是相当类似的&#xff0c;但是它们在实现上有一些细微的区别。 1.view() view()方法是…

C++指针常量,常量指针以及, 引用和指针的区别

const修饰指针有三种情况 1. const修饰指针 --- 常量指针 2. const修饰常量 --- 指针常量 3. const即修饰指针&#xff0c;又修饰常量 c int main() {int a 10;int b 10;//const修饰的是指针&#xff0c;常量指针&#xff0c;指针指向可以改&#xff0c;指针指向的值不…

分享10个必备的VS Code技巧和窍门,提高你的开发效率

目录 前言 1. 时间线视图&#xff1a;本地源代码控制 2. 自动保存&#xff1a;不再需要按Ctrl S 3. 使用命令面板进行任何操作 4、快速转到文件 5. 快速跳转指定行 6. 快速删除该行 7. 享受使用流畅的光标进行打字 8. 快速格式化代码 9. 使用多光标编辑功能节省时间…

数据计算-第15届蓝桥杯第一次STEMA测评Scratch真题精选

[导读]&#xff1a;超平老师的《Scratch蓝桥杯真题解析100讲》已经全部完成&#xff0c;后续会不定期解读蓝桥杯真题&#xff0c;这是Scratch蓝桥杯真题解析第154讲。 第15届蓝桥杯第1次STEMA测评已于2023年8月20日落下帷幕&#xff0c;编程题一共有6题&#xff0c;分别如下&a…