【智能家居项目】裸机版本——项目介绍 | 输入子系统(按键) | 单元测试
🐱作者:一只大喵咪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来判断环形缓冲区是否写满数据,相等就写满了,不相等就没写满。

如上图所示,定义环形缓冲区结构体,通过维护pW和pR来维护环状,以及从存放输入事件的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。
同样,不同芯片获取时间的方式也不同,这里也要实现针对不同芯片获取时间的方式:

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

此时芯片抽象层也实现了,内核抽象层会调用该层的CAL_GPIOKeyInit初始化按键。
⚽硬件操作
本喵使用的是STM32F103ZET6芯片,使用CubeMX和HAL库进行按键初始化,在初始化的时候,要在中断函数中进行输入数据的读取,并放入环形缓冲区中。

如上图,在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端一样使用标准库中的
printf和scanf,必须重新实现fputc和fgetc函数,让终端变成串口,符合我们的要求。
⚽测试
为了看我们设计的输入子系统是否正确,需要专门写一个单元测试函数来测试一下:

如上图所示,将按键设备添加到输入子系统中,然后进行初始化,在while(1)循环中读取输入事件,并通过串口打印输入事件的信息。
在main函数中调用该测试函数,通过串口调试助手查看打印信息:

如上图,将板子的串口和电脑连在一起后,通过串口调试助手可以看到,当按键1或者按键2按下后,会打印出发生的事件信息,包括事件类型,发生事件,按键编号,以及按键值,说明设计的输入子系统是成功的。
🏀源码
这部分代码是在OLED代码的基础上写的,包含源码以及串口调试工具,需要的小伙伴自取传送门。
🏀总结
这篇文章实现了智能家居项目中输入子系统中的按键设备,最重要的是介绍的代码框架和编程思想,之后的项目部分都会按照这个思路来扩展维护。
相关文章:
【智能家居项目】裸机版本——项目介绍 | 输入子系统(按键) | 单元测试
🐱作者:一只大喵咪1201 🐱专栏:《智能家居项目》 🔥格言:你只管努力,剩下的交给时间! 目录 🏀项目简介🏀输入子系统(按键)⚽应用层⚽设备层⚽ 内核层抽象层⚽…...
算法练习8——有序三元组中的最大值
LeetCode 100088 有序三元组中的最大值 I LeetCode 100086 有序三元组中的最大值 II 给你一个下标从 0 开始的整数数组 nums 。 请你从所有满足 i < j < k 的下标三元组 (i, j, k) 中,找出并返回下标三元组的最大值。如果所有满足条件的三元组的值都是负数&am…...
git创建
问: git remote add origin https://github.com//blog.git fatal: not a git repository (or any of the parent directories): .git 回答: 这个错误提示指出当前目录或其父目录中不存在.git文件夹,因此无法执行git相关操作。请确保你是在一个已经初始化为git仓库…...
yolov8 opencv模型部署(python版)
yolov8 opencv模型部署(python版) 使用opencv推理yolov8模型,以yolov8n为例子,一共几十行代码,没有废话,给出了注释,从今天起,少写一行代码,少掉一根头发。测试数据有需…...
Simulink仿真封装中的参数个对话框设置
目录 参数和对话框窗格 初始化窗格 文档窗格 为了更加直观和清晰的分析仿真,会将多个元件实现的一个功能封装在一起,通过参数对话框窗格,可以使用参数、显示和动作选项板中的对话框控制设计封装对话框。如图所示: 参数和对话框…...
【C++】class的设计与使用(十)重载iostream运算符
希望对某个类对象进行读写操作,直接cout<<类对象<<endl;或cin>>类对象;编译器会报错,所以我们必须提供一份重载的input/output运算符: 重载ostream运算符 ostream& operator<<(ostream &os, const Triangu…...
Java使用Scanner类实现用户输入与交互
概述: Scanner类是Java中的一个重要工具类,用于读取用户的输入。它提供了一系列的方法,可以方便地读取不同类型的数据,如整数、浮点数、字符串等。在本文中,我们将详细介绍Scanner类的使用方法,并通过两个…...
FFmpeg 命令:从入门到精通 | ffppeg 命令参数说明
FFmpeg 命令:从入门到精通 | ffmpeg 命令参数说明 FFmpeg 命令:从入门到精通 | ffmpeg 命令参数说明主要参数音频参数视频参数更多参考 FFmpeg 命令:从入门到精通 | ffmpeg 命令参数说明 本节主要介绍了 ffmpeg 命令的常用参数。 主要参数 …...
Chrome(谷歌浏览器)如何关闭搜索栏历史记录
目录 问题描述解决方法插件解决(亲测有效)自带设置解决步骤首先打开 地址 输入:chrome://flags关闭浏览器,重新打开Chrome 发现 已经正常 问题描述 Chrome是大家熟知的浏览器,但是搜索栏的历史记录如何自己一条条的删…...
基于Java的宠物医院管理系统设计与实现(源码+lw+部署文档+讲解等)
文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序(小蔡coding)有保障的售后福利 代码参考源码获取 前言 💗博主介绍:✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作…...
使用WPS自动化转换办公文档: 将Word, PowerPoint和Excel文件转换为PDF
🌷🍁 博主猫头虎 带您 Go to New World.✨🍁 🦄 博客首页——猫头虎的博客🎐 🐳《面试题大全专栏》 文章图文并茂🦕生动形象🦖简单易学!欢迎大家来踩踩~🌺 &a…...
对pyside6中的textedit进行自定义,实现按回车可以触发事件。
我的实现方法是,先用qt designer写好界面,如下图: 接着将其生成的ui文件编译成为py文件。 找到里面这几行代码: self.textEdit QTextEdit(self.centralwidget)self.textEdit.setObjectName(u"textEdit")self.textEdit…...
Spark SQL
Spark SQL 一、Spark SQL概述二、准备Spark SQL的编程环境三、Spark SQL程序编程的入口四、DataFrame的创建五、DataFrame的编程风格六、DataSet的创建和使用七、Spark SQL的函数操作 一、Spark SQL概述 Spark SQL属于Spark计算框架的一部分,是专门负责结构化数据的…...
初识多线程
一、多任务 现实中太多这样同时做多件事的例子了,例如一边吃饭一遍刷视频,看起来是多个任务都在做,其实本质上我们的大脑在同一时间依旧只做了一件事情。 二、普通方法调用和多线程 普通方法调用只有主线程一条执行路径 多线程多条执行路径…...
Linux用户、用户组和文件权限的管理与实践
目录 一、Linux用户、用户组和文件权限的基础概念与作用1.1 Linux用户的概念与作用1.2 Linux用户组的概念与作用1.3 Linux文件权限的概念与作用 二、Linux用户、用户组和文件权限的具体操作实践2.1 创建新用户:从零开始构建用户体系2.2 修改用户和用户组属性&#x…...
【CMU15-445 Part-14】Query Planning Optimization I
Part14-Query Planning & Optimization I SQL is Declarative,只告诉想要什么而不需要说怎么做。 IBM System R是第一个实现query optimizer查询优化器的系统 Heuristics / Rules 条件触发 静态规则,重写query来remove 低效或者愚蠢的东西…...
七、垃圾收集中级
JVM由浅入深系列 JVM由浅入深系列一、关于Java性能的误解二、Java性能概述三、了解JVM概述四、探索JVM架构五、垃圾收集基础六、HotSpot中的垃圾收集七、垃圾收集中级八、垃圾收集高级👋垃圾收集中级 ⚽️1. 权衡收集器插件 就 Java 平台而言,有一点可能初学者未必能马上意…...
el-menu 导航栏学习(1)
最简单的导航栏学习跳转实例效果: (1)index.js路由配置: import Vue from vue import Router from vue-router import NavMenuDemo from /components/NavMenuDemo import test1 from /components/test1 import test2 from /c…...
Axios请求封装
安装axios,在net文件下新建index.js,封装InternalPsot请求: function internalPost(url,data,header,success,failure,errordefaultError()){axios.post(url,data,{headers:header}).then(({data})>{if (data.code200){success(data.dat…...
Pikachu靶场——XXE 漏洞
文章目录 1. XXE1.1 查看系统文件内容1.2 查看PHP源代码1.3 查看开放端口1.4 探测内网主机 1. XXE 漏洞描述 XXE(XML External Entity)攻击是一种利用XML解析器漏洞的攻击。在这种攻击中,攻击者通过在XML文件中插入恶意实体来触发解析器加载…...
【Axure高保真原型】引导弹窗
今天和大家中分享引导弹窗的原型模板,载入页面后,会显示引导弹窗,适用于引导用户使用页面,点击完成后,会显示下一个引导弹窗,直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…...
<6>-MySQL表的增删查改
目录 一,create(创建表) 二,retrieve(查询表) 1,select列 2,where条件 三,update(更新表) 四,delete(删除表…...
Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)
文章目录 1.什么是Redis?2.为什么要使用redis作为mysql的缓存?3.什么是缓存雪崩、缓存穿透、缓存击穿?3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...
剑指offer20_链表中环的入口节点
链表中环的入口节点 给定一个链表,若其中包含环,则输出环的入口节点。 若其中不包含环,则输出null。 数据范围 节点 val 值取值范围 [ 1 , 1000 ] [1,1000] [1,1000]。 节点 val 值各不相同。 链表长度 [ 0 , 500 ] [0,500] [0,500]。 …...
【AI学习】三、AI算法中的向量
在人工智能(AI)算法中,向量(Vector)是一种将现实世界中的数据(如图像、文本、音频等)转化为计算机可处理的数值型特征表示的工具。它是连接人类认知(如语义、视觉特征)与…...
【配置 YOLOX 用于按目录分类的图片数据集】
现在的图标点选越来越多,如何一步解决,采用 YOLOX 目标检测模式则可以轻松解决 要在 YOLOX 中使用按目录分类的图片数据集(每个目录代表一个类别,目录下是该类别的所有图片),你需要进行以下配置步骤&#x…...
土地利用/土地覆盖遥感解译与基于CLUE模型未来变化情景预测;从基础到高级,涵盖ArcGIS数据处理、ENVI遥感解译与CLUE模型情景模拟等
🔍 土地利用/土地覆盖数据是生态、环境和气象等诸多领域模型的关键输入参数。通过遥感影像解译技术,可以精准获取历史或当前任何一个区域的土地利用/土地覆盖情况。这些数据不仅能够用于评估区域生态环境的变化趋势,还能有效评价重大生态工程…...
BCS 2025|百度副总裁陈洋:智能体在安全领域的应用实践
6月5日,2025全球数字经济大会数字安全主论坛暨北京网络安全大会在国家会议中心隆重开幕。百度副总裁陈洋受邀出席,并作《智能体在安全领域的应用实践》主题演讲,分享了在智能体在安全领域的突破性实践。他指出,百度通过将安全能力…...
MySQL 8.0 OCP 英文题库解析(十三)
Oracle 为庆祝 MySQL 30 周年,截止到 2025.07.31 之前。所有人均可以免费考取原价245美元的MySQL OCP 认证。 从今天开始,将英文题库免费公布出来,并进行解析,帮助大家在一个月之内轻松通过OCP认证。 本期公布试题111~120 试题1…...
微软PowerBI考试 PL300-在 Power BI 中清理、转换和加载数据
微软PowerBI考试 PL300-在 Power BI 中清理、转换和加载数据 Power Query 具有大量专门帮助您清理和准备数据以供分析的功能。 您将了解如何简化复杂模型、更改数据类型、重命名对象和透视数据。 您还将了解如何分析列,以便知晓哪些列包含有价值的数据,…...

