Arduino 小白的 DIY 空气质量检测仪(5)- OLED显示模块、按钮模块

news2025/1/7 13:21:09

最终章

这一章把剩下的OLED显示模块、按钮模块分享一下,当前这个离线无存储的版本,基本告一段落。

如果后续能进化成🈶存储、联网版本,就再开一个小系列分享一下。

逐个分析

display.h

#include <Arduino.h>
#include <Wire.h>

// OLED 0.96 库
#include <ssd1306.h>

// OLED 0.96
// 接口:GND->GND、VDD->VCC(5V)、SCK->SCK/A5、SDA->SDA/A4
// 协议:I2C
// 地址:0x3C

namespace SSD_1306 {
unsigned int width = 128;
unsigned int lineHeight = 8;
unsigned int charMax = 24;
}

namespace Display {

struct _OLED {
  void init() {
    // 初始化OLED
    Wire.begin();
    ssd1306_128x64_i2c_init();
    ssd1306_setFixedFont(ssd1306xled_font6x8);
    ssd1306_clearScreen();
  }

  void printRaw(unsigned int left, unsigned int top, char* str, unsigned int style = STYLE_NORMAL) {
    ssd1306_printFixed(left, top, str, style);
  }

  void printNRaw(unsigned int left, unsigned int top, char* str, unsigned int style = STYLE_NORMAL) {
    ssd1306_printFixedN(left, top, str, style, 1);
  }

  void print(char* str, unsigned int left, unsigned int top) {
    printRaw(left, SSD_1306::lineHeight * top, str);
  }

  void printRight(char* str, int top) {
    uint16_t left = getLeft(str);
    print(str, left, top);
  }

  void drawBuffer(unsigned int left, unsigned int top, uint8_t* buffer) {
    ssd1306_drawBuffer(left, top, 3, 8, buffer);
  }

  void drawLine(unsigned int x1, unsigned int y1, unsigned int x2, unsigned int y2) {
    ssd1306_drawLine(x1, y1, x2, y2);
  }

  void clearBlock(unsigned int left, unsigned int top, unsigned int w, unsigned int h) {
    ssd1306_clearBlock(left, top, w, h);
  }

  void clearBlockCenter(unsigned int left, unsigned int right, unsigned int top) {
    clearBlock(left, top, SSD_1306::width - left - right, SSD_1306::lineHeight);
  }

  unsigned int getTextSize(char* str) {
    return ssd1306_getTextSize(str, 0);
  }

  unsigned int getLeft(char* str) {
    int w = getTextSize(str);
    return SSD_1306::width - w;
  }

  void clearScreen() {
    ssd1306_clearScreen();
  }
} OLED;

}

通讯方式是 I2C

这个 OLED 模块,分辨率只有 128x64,一行文字占 8 个像素的高度,一行大概可以容纳 24 个字母。

支持这个模块的库很多,有的依赖了别的库、有的带开屏广告、有的。。。最后我选了 ssd1306.h 感觉比较顺手,不过它的 API 的命名比较简单粗暴。

初始化、字体设置、清屏 很好理解,而绘制文字 ssd1306_printFixed 和 ssd1306_printFixedN 的区别,也只是 ssd1306_printFixedN 多一个放大倍数的参数输入,1 就是放大一倍。设计理念是基于 8 像素这个行高的。

比较重要的是,绘制有个特点,内容更新是需要考虑“清空”的,而这个“清空”多数时候是局部的,例如:

假如,第一秒数值是 1234,显示如下:

1234

第二秒数值是 234,如果不进行“清空”,显示将如下:

2344

改变的字母“234”区域更新了,但是原来未改变的“4”依然显示,因此,是需要清空“4”这个区域的,才能变成:

234

此库提供一个相应的方法:

void ssd1306_clearBlock(uint8_t x, uint8_t y, uint8_t w, uint8_t h){}

x、y 是开始位置,w、h 是处理范围。

问题来了,我如何知道从哪个像素开始“清空”呢?那将需要另外一个 API:

lcduint_t ssd1306_getTextSize(const char *text, lcduint_t *height){}

它可以通过字符串的内容,计算字符串所需占用的宽高,这里返回值就是宽,输入的第二参数是高(本项目只需要宽,高都以默认 8 像素计算)。

在本项目中,一行将显示 2 个传感器数值,也就是说需要左右各自对齐贴边:

在这里插入图片描述

这里“清空”的区域就要考虑左右两个数值的字符串宽度了,就是说,每次更新数值的时候,需要“清空”的区域大概是:

在这里插入图片描述

举个例子,本项目中,最后一行显示,最外层的方法是:

// arduino-air-monitor.ino

void process(bool display) {
// ...略

  if (display) {
    // ...略
    Display::OLED.clearBlockCenter(printCO2(Module::CO2.getValue(), 7, false), printHum(Module::Humidity.getValue(), 7, true), 7);
  }
}

CO2 的显示方法 printCO2:

unsigned int printCO2(unsigned int value, unsigned int row, bool isRight) {
  char str[SSD_1306::charMax] = "";
  strcat(str, "CO2:");

  char numStr[SSD_1306::charMax] = "";
  itoa(value, numStr, 10);
  strcat(str, numStr);
  strcat(str, "ppm");

  if (isRight) {
    Display::OLED.printRight(str, row);
  } else {
    Display::OLED.print(str, 0, row);
  }

  Serial.println(str);

  return Display::OLED.getTextSize(str);
}

湿度的显示方法 printHum:

unsigned int printHum(float value, unsigned int row, bool isRight) {
  char str[SSD_1306::charMax] = "";
  strcat(str, "Hum:");

  char numStr[SSD_1306::charMax] = "";
  dtostrf(value, 1, 1, numStr);
  strcat(str, numStr);
  strcat(str, "%");

  if (isRight) {
    Display::OLED.printRight(str, row);
  } else {
    Display::OLED.print(str, 0, row);
  }

  Serial.println(str);

  return Display::OLED.getTextSize(str);
}

这里设计思路,是每个数值的显示方法,最后都会返回字符串占用的宽度,用于计算中间“清空”区域。

  void clearBlockCenter(unsigned int left, unsigned int right, unsigned int top) {
    clearBlock(left, top, SSD_1306::width - left - right, SSD_1306::lineHeight);
  }
  void clearBlock(unsigned int left, unsigned int top, unsigned int w, unsigned int h) {
    ssd1306_clearBlock(left, top, w, h);
  }

关于靠右显示,也是利用 ssd1306_getTextSize,用 128 显示宽度减去字符串的宽度,就是靠右显示的起始位置了。

最后,说说 2 个要自己实现的显示字符:“立方”和“度”,是不支持此类特殊字符的:

在这里插入图片描述

这个时候,就需要利用此库绘制位图:

unsigned int printPower3(unsigned int left, unsigned int top) {
  // 绘制立方"³"符号
  // 位图方向:从左往右、从下往上
  // 0 0 0
  // 0 0 0
  // 0 0 0
  // 1 1 1
  // 0 0 1
  // 1 1 1
  // 0 0 1
  // 1 1 1
  // 第一列 00010101 -> 0x15
  // 第二列 00010101 -> 0x15
  // 第三列 00011111 -> 0x1F
  // js转换示例:parseInt('00011111',2) -> (31).toString(16) -> 1f
  uint8_t buffer[3] = { 0x15, 0x15, 0x1F };
  Display::OLED.drawBuffer(left, top, buffer);

  return 4;
}

unsigned int printDeg(unsigned int left, unsigned int top) {
  // 绘制"°"符号
  // 位图方向:从左往右、从下往上
  // 0 0 0
  // 0 0 0
  // 0 0 0
  // 0 0 0
  // 0 0 0
  // 0 1 0
  // 1 0 1
  // 0 1 0
  uint8_t buffer[3] = { 0x02, 0x05, 0x02 };
  Display::OLED.drawBuffer(left, top, buffer);

  return 4;
}

请看注释,实际上就是:在格子中填0/1,1 就是代表像素的亮。

计算的逻辑,可以参考 printPower3 的注释:

在这里插入图片描述

上面说的“位图方向:从左往右、从下往上”,不是很严谨,其实这里只是使用该 API 得出的特点(我也不是很明白为何颠倒过来了,等哪位大神可以解答一下最好),位图按维基百科应该下面那样才符合直觉:
在这里插入图片描述

最后,我使用下来,发现使用 String 类型会出现各种无法解释的异常乱码,个人建议这里使用 C 风格的字符串。

上边基本上就是我遇到的一些比较值得注意的坑吧。

buttons.h

// buttons.h

#include <Arduino.h>

#define _Pin_Btn_1 12

namespace Buttons {

enum Status {
  Ready = 0,
  Down = 1,
  Up = 2
};

struct _Btn_1 {
  Status status = Ready;

  void init() {
    pinMode(_Pin_Btn_1, INPUT_PULLUP);
  }

  void loop() {
    if (status == Ready && digitalRead(_Pin_Btn_1) == LOW) {
      status = Down;
    }

    if (status != Ready && digitalRead(_Pin_Btn_1) == HIGH) {
      status = Up;
    }
  }

  bool getValue() {
    bool result = status == Up;

    if (result) {
      status = Ready;
    }
    return result;
  }
} Btn_1;

}

按网页开发的直觉,按钮不就是用 digitalRead 得到该按钮引脚如果低电平,就知道按了,就 if 一下去干一件事情就可以了吗?

实际上,在这里,“点击”是需要自己处理按钮的状态的,我抽象成 Ready 等待(HIGH)、Down 按下中(LOW)、Up 释放中(HIGH),可以看出来 Ready 和 Up 都是 HIGH,这应该如何区分?

该模块里面,我也定义了一个 loop 方法,就意味着要放在 程序入口 的 loop 方法中。

// arduino-air-monitor.ino

void loop() {
  Buttons::Btn_1.loop();

  bool clicked = Buttons::Btn_1.getValue();

  if (clicked) {
    oledDisplay = !oledDisplay;
  }

  // oledDisplay 就是通过按钮切换的一个 true/false 状态

  // ...略
}

流程图表达:

在这里插入图片描述

可以看出,只要按下不动,就会变成且持续是 Down 状态,放手释放的时候,就会变成且持续是 Up 状态。

那什么时候才会变回 Ready 进行下一次“点击”识别?是读取是否“点击”了的时候:

// buttons.h

  bool getValue() {
    bool result = status == Up;

    if (result) {
      status = Ready;
    }
    return result;
  }
// arduino-air-monitor.ino

bool clicked = Buttons::Btn_1.getValue();

这样子,就可以在持续不断的 loop 中,识别出“点击”的操作,毕竟“点击”是由“按下”和“释放”两个动作构成的,类似网页中 click 约等于 mousedown + mouseup。

好羡慕 PCB 设计、3D 打印 的大神们,如果拥有这两块的能力,做成真正的成品该多好呀~~~~

多多支持其它文章
https://blog.csdn.net/xachary2

又或者请我喝杯奶茶😍
vue3-zoom-drag

项目完整代码仓库在这
arduino-air-monitor

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

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

相关文章

基于机器视觉和Dijkstra算法的平面建筑群地图路线规划matlab仿真

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 1.程序功能描述 基于机器视觉和Dijkstra算法的平面建筑群地图路线规划matlab仿真&#xff0c;输入一张平面建筑群的地图&#xff0c;然后通过机器视觉识别地图里面的障碍物&#xff0c;然后通…

计算机的错误计算(二百零一)

摘要 用两个大模型计算 &#xff0c;结果保留 10位有效数字。实验表明&#xff0c;两个大模型的输出均只有1位正确数字&#xff1b;并它们几乎相同&#xff1a;仅最后1位数字不同。 例1. 计算 , 结果保留 10位有效数字。 下面是与一个数学解题器的对话。 以上为与一个数学解…

【Motion Builder】配置c++插件开发环境

目录 准备环境构建官方案例另行构建经验分享附录 准备环境 安装Motion Builder 2024并破解安装Qt 5.15.2 截止至2024年12月19日&#xff0c;Qt的在线安装器的默认页面是没有5.15.2版本的。你需要&#xff1a;在“选择组件”界面&#xff0c;选择“Archive”&#xff0c;点击“…

大学生入学审核系统的设计与实现(源码+数据库+文档)

亲测完美运行带论文&#xff1a;文末获取源码 文章目录 项目简介&#xff08;论文摘要&#xff09;运行视频包含的文件列表&#xff08;含论文&#xff09;后台运行截图 项目简介&#xff08;论文摘要&#xff09; 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理…

使用高云小蜜蜂GW1N-2实现MIPI到LVDS(DVP)转换案例分享

作者&#xff1a;Hello&#xff0c;Panda 大家晚上好&#xff0c;熊猫君又来了。 今天要分享的是一个简单的MIPI到LVDS&#xff08;DVP&#xff09;接口转换的案例。目的就是要把低成本FPGA的应用潜力充分利用起来。 一、应用背景 这个案例的应用背景是&#xff1a;现在还在…

单片机-独立按键矩阵按键实验

1、按键介绍 按键管脚两端距离长的表示默认是导通状态&#xff0c;距离短的默认是断开状态&#xff0c; 如果按键按下&#xff0c;初始导通状态变为断开&#xff0c;初始断开状态变为导通 我们开发板是采用软件消抖&#xff0c;一般来说一个简单的按键消抖就是先读取按键的状…

供应链系统设计-供应链中台系统设计(七)- 商品中心设计篇

概述 上篇文章我们大致讲了一些商品中心相关的概念&#xff0c;例如&#xff1a;SPU、SKU、Item等等&#xff0c;在这里我们来简单的回顾一下&#xff1a; 商品概念的分层与定义&#xff1a; SPU&#xff08;Standard Product Unit&#xff09;&#xff1a;代表产品系列或产品…

RAG(Retrieval-Augmented Generation,检索增强生成)流程

目录 一、知识文档的准备二、OCR转换三、分词处理四、创建向量数据库五、初始化语言聊天模型1.prompt2.检索链3.对话 完整代码 知识文档的准备&#xff1a;首先需要准备知识文档&#xff0c;这些文档可以是多种格式&#xff0c;如Word、TXT、PDF等。使用文档加载器或多模态模型…

mysql自定义安装

1、下载安装包 我是在windows上安装&#xff0c;所以选择“Mysql Installer for Windows” 2、安装mysql 双击“mysql-installer-community-8.0.40.0.msi”&#xff0c;开始启动安装 这里选择安装项&#xff0c;这里只选择了两项。workbench是图形化管理工具&#xff0c;比较吃…

Innodisk iSMART V6使用说明_SSD还能用多久?已经读写了多少次数?……

Innodisk iSMART是一款SSD健康数据读取软件。它能轻松获取大部分SSD内部寄存器中的健康数据&#xff0c;并以简洁的图形界面展示给用户。在程序界面的顶部&#xff0c;是页面标签&#xff0c;点击页面标签就能切换到相应的页面。页面标签的下面是磁盘选择栏。点击磁盘编号&…

JAVA:利用 Redis 实现每周热评的技术指南

1、简述 在现代应用中&#xff0c;尤其是社交媒体和内容平台&#xff0c;展示热门评论是常见的功能。我们可以通过 Redis 的高性能和丰富的数据结构&#xff0c;轻松实现每周热评功能。本文将详细介绍如何利用 Redis 实现每周热评&#xff0c;并列出完整的实现代码。 2、需求分…

LSP介绍并实现语言服务

首发于Enaium的个人博客 LSP (Language Server Protocol) 介绍 前段时间我为Jimmer DTO实现了一个 LSP 的语言服务&#xff0c;这是我第一次实现 LSP&#xff0c;所以在这里我分享一下我实现LSP的经验。 首先来看一下效果&#xff0c;图片太多&#xff0c;我就放一部分&#…

【微软,模型规模】模型参数规模泄露:理解大型语言模型的参数量级

模型参数规模泄露&#xff1a;理解大型语言模型的参数量级 关键词&#xff1a; #大型语言模型 Large Language Model #参数规模 Parameter Scale #GPT-4o #GPT-4o-mini #Claude 3.5 Sonnet 具体实例与推演 近日&#xff0c;微软在一篇医学相关论文中意外泄露了OpenAI及Claud…

一文大白话讲清楚TCP连接的三次握手和断开连接的四次挥手的原理

文章目录 一文大白话讲清楚TCP连接的三次握手和断开连接的四次挥手的原理1.TCP建立连接需要3次握手1.1 先讲个你兄弟的故事1.2 TCP 3次握手1.2 TCP 3次握手8件事1.3 TCP握手能不能是两次 2. TCP 断开连接要4次挥手2.1 还回到你兄弟的故事上2.2 TCP 4次挥手2.2 TCP4次挥手4件事2…

解决npm报错:sill idealTree buildDeps

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl 报错信息 使用 npm 安装依赖时报错&#xff1a;sill idealTree buildDeps 解决方案 请按照以下步骤进行相关操作&#xff1a; 1、删除 C:\Users{账户}\ 文件夹中的 .npm…

Apache Celeborn 在B站的生产实践

背景介绍 Shuffle 演进 随着B站业务的飞速发展,数据规模呈指数级增长,计算集群也逐步从单机房扩展到多机房部署模式。多个业务线依托大数据平台驱动核心业务,大数据系统的高效性与稳定性成为公司业务发展的重要基石。如图1,目前在大数据基础架构下,我们主要采用 Spark、Fl…

SAP系统中的标准价、移动平均价是什么?有何区别?物料分类账的优点

文章目录 前言一、SAP系统中的价格控制二、移动平均价、标准价是什么&#xff1f;三、S价&#xff08;标准价&#xff09;的优势四、S价&#xff08;标准价&#xff09;的劣势五、V价&#xff08;移动平均价&#xff09;的优势六、V价&#xff08;移动平均价&#xff09;的劣势…

我的Java-Web进阶--SpringMVC

1.三层架构与MVC模式 三层架构 MVC模式 2.SpringMVC执行流程 3.SpringMVC的基本使用方法 1. 配置 1.1 Maven依赖 首先&#xff0c;在pom.xml文件中添加Spring MVC的依赖&#xff1a; <dependencies><!-- Spring MVC --><dependency><groupId>org.…

让css设置的更具有合理性

目录 一、合理性设置宽高 二、避免重叠情况&#xff0c;不要只设置最大宽 三、优先使用弹性布局特性 四、单词、数字换行处理 五、其他编码建议 平常写css时&#xff0c;除了遵循一些 顺序、简化、命名上的规范&#xff0c;让css具有合理性也是重要的一环。 最近的需求场…

【C++】深入理解C语言中的特殊字符处理与问题分析优化

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C 文章目录 &#x1f4af;前言&#x1f4af;题目&#xff1a;B2090 年龄与疾病输入格式输出格式输入输出样例 &#x1f4af;初始代码分析与问题排查问题点分析 &#x1f4af;修正后的代码与优化修正与优化要点 &#…