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

🐱作者:一只大喵咪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.xdnf.cn/news/146183.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

flink选择slot

flink选择slot 在这个类里修改 package org.apache.flink.runtime.resourcemanager.slotmanager.SlotManagerImpl; findMatchingSlot(resourceProfile):找到满足要求的slot(负责从哪个taskmanager中获取slot)对应上图第8,9&…

IDEA 使用

目录 Git.gitignore 不上传取消idea自动 add file to git撤销commit的内容本地已经有一个开发完成的项目,这个时候想要上传到仓库中 Git .gitignore 不上传 在项目根目录下创建 .gitignore 文件夹,并添加内容: .gitignore取消idea自动 add…

【OpenCV-Torch-dlib-ubuntu】Vm虚拟机linux环境摄像头调用方法与dilb模型探究

前言 随着金秋时节的来临,国庆和中秋的双重喜庆汇聚成一片温暖的节日氛围。在这个美好的时刻,我们有幸共同迎来一次长达8天的假期,为心灵充电,为身体放松,为未来充实自己。今年的国庆不仅仅是家国团聚的时刻&#xff…

STL容器

C STL STL实现原理及其实现 STL(Standard Template Library,标准模板库),提供了六大组件,可以相互之间组合套用,这六大组件分别是:容器(Containers),算法&a…

1.6.C++项目:仿muduo库实现并发服务器之channel模块的设计

项目完整版在: 文章目录 一、channel模块:事件管理Channel类实现二、提供的功能三、实现思想(一)功能(二)意义(三)功能设计 四、代码(一)框架(二…

凉鞋的 Godot 笔记 103. 检视器 :节点的微观编辑和查看

在上一篇,笔者简单介绍了场景与节点的增删改查,如下所示: 在这一篇,我们接着往下学习。 我们知道在场景窗口,可以对节点进行增删改查。 在 Godot 引擎使用过程中,场景窗口的使用频率是非常高的。 但是场景窗口只能编…

macOS Sonoma 14 正式版(23A344)发布,附黑/白苹果镜像下载地址

系统介绍(系统下载地址:http://www.imacosx.cn/115300.html) 黑果魏叔9 月 27日消息,苹果今日向 Mac 电脑用户推送了 macOS Sonoma 14 正式版(23A344)。 macOS 14正式版系统发布:全新功能与改…

ChatGPT Prompting开发实战(十二)

一、如何开发prompts实现个性化的对话方式 通过设置“system”和“user”等roles,可以实现个性化的对话方式,并且可以结合参数“temperature”的设定来差异化LLM的输出内容。在此基础上,通过构建一个餐馆订餐对话机器人来具体演示对话过程。…

Arcgis克里金插值报错:ERROR 010079: 无法估算半变异函数。 执行(Kriging)失败。

Arcgis克里金插值报错:ERROR 010079: 无法估算半变异函数。 执行(Kriging)失败。 问题描述: 原因: shape文件的问题,此图可以看出,待插值的点有好几个都超出了地理范围之外,这个不知道是坐标系配准的问…

JAVA设计模式-代理模式

一.概念 在软件开发中,也有一种设计模式可以提供与代购网站类似的功能。由于某些原因,客户端不想或不能直接访问一个对象,此时可以通过一个称之为“代理”的第三者来实现间接访问,该方案对应的设计模式被称为代理模式。 ​ 代理模…

数据结构——堆(C语言)

本篇会解决一下几个问题: 1.堆是什么? 2.如何形成一个堆? 3.堆的应用场景 堆是什么? 堆总是一颗完全二叉树堆的某个节点总是不大于或不小于父亲节点 如图,在小堆中,父亲节点总是小于孩子节点的。 如图&a…

GO 比较两个对象是否相同

本文主要是来聊一聊关于 Golang 中的深度比较 DeepEqual 因为最近发现身边的小伙伴写 2 个或者多个 map 比较的时候,都是自己去实现去比较每一个结构,每一个节点的 key 和 value 是不是都相等,且根据不同的数据结构,都要去实现一…

【Python】基于OpenCV人脸追踪、手势识别控制的求实之路FPS游戏操作

【Python】基于OpenCV人脸追踪、手势识别控制的求实之路FPS游戏操作 文章目录 手势识别人脸追踪键盘控制整体代码附录:列表的赋值类型和py打包列表赋值BUG复现代码改进优化总结 py打包 视频: 基于OpenCV人脸追踪、手势识别控制的求实之路FPS游戏操作 手…

Everything+cpolar搭建在线资料库,实现随时随地访问

Everythingcpolar搭建在线资料库,实现随时随地访问 文章目录 Everythingcpolar搭建在线资料库,实现随时随地访问前言1.软件安装完成后,打开Everything2.登录cpolar官网 设置空白数据隧道3.将空白数据隧道与本地Everything软件结合起来总结 前…

Python日期的加减等操作

嗨喽,大家好呀~这里是爱看美女的茜茜呐 日期输出格式化 所有日期、时间的api都在datetime模块内。 👇 👇 👇 更多精彩机密、教程,尽在下方,赶紧点击了解吧~ python源码、视频教程、插件安装教程、资料我都…

scala基础入门

一、Scala安装 下载网址:Install | The Scala Programming Language ideal安装 (1)下载安装Scala plugins (2)统一JDK环境,统一为8 (3)加载Scala (4)创建工…

ARM底层汇编基础指令

汇编语言的组成 伪操作 不参与程序执行,但是用于告诉编译器程序怎么编译.text .global .end .if .else .endif .data 汇编指令 编译器将一条汇编指令编译成一条机器码,在内存里一条指令占4字节内存,一条指令可以实现一个特定的功能 伪指令 不…

Hudi第二章:集成Spark(二)

系列文章目录 Hudi第一章:编译安装 Hudi第二章:集成Spark Hudi第二章:集成Spark(二) 文章目录 系列文章目录前言一、IDEA1.环境准备2.代码编写1.插入数据2.查询数据3.更新数据4.指定时间点查询5.增量查询6.删除数据7.覆盖数据 二、DeltaStre…

设计模式8、装饰者模式 Decorator

解释说明:动态地给一个对象增加一些额外的职责。就扩展功能而言,装饰模式提供了一种比使用子类更加灵活的替代方案 抽象构件(Component):定义一个抽象接口以规范准备收附加责任的对象 具体构件(ConcreteCom…

ELK整合springboot(第二课)

一、创建一个springboot的项目 pom文件如下&#xff1a; <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLo…