当前位置: 首页 > article >正文

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

最终章

这一章把剩下的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.kler.cn/a/468178.html

相关文章:

  • vulnhub靶场-potato(至获取shell)
  • github开源链游详细搭建文档
  • 深入解析-正则表达式
  • springboot适配mybatis+guassdb与Mysql兼容性问题处理
  • 李宏毅机器学习笔记-Transformer
  • pdf预览兼容问题- chrome浏览器105及一下预览不了
  • 微信小程序校园自助点餐系统实战:从设计到实现
  • CSS系列(50)-- View Transitions详解 系列总结
  • 应用Docker快速实现 JMeter + InfluxDB + Grafana 监控方案
  • 虚拟机图像界面打不开了
  • NLP初识
  • leetcode中简单题的算法思想
  • 计算机网络•自顶向下方法:网络安全、RSA算法
  • react报错解决
  • 1、pycharm、python下载与安装
  • 服务器信息整理:用途、操作系统安装日期、设备序列化、IP、MAC地址、BIOS时间、系统
  • 什么是Kafka的重平衡机制?
  • 小红书怎么看ip所属地?小红书ip属地为什么可以变
  • 基于Spring Boot的健康饮食管理系统
  • 开发培训:慧集通(DataLinkX)iPaaS集成平台-基于接口的组件开发
  • WebSocket 基础入门:协议原理与实现
  • Appllo学习
  • MySQL 索引分类及区别与特点
  • OkHttp接口自动化之断言
  • 基于Spring Boot的智能笔记的开发与应用
  • 自动化文件监控与分类压缩:实现高效文件管理