物联网项目开发实战-第3章-自动浇花项目迭代3

本节我们在迭代二的基础上使用四位数码管和OLED显示屏显示相关交互信息。

3.4迭代三:通过人机界面了解当前状态

在前面的两个迭代里,我们通过串口输出了一些程序运行的信息,但查看串口输出信息需要使用Arduino串口监视器或其他第三方串口查看软件,这对于产品的最终用户来说,是非常不友好的。可以通过增加人机交互设备来显示这些信息。

常见的人机交互设备有:7段式数码管、OLED显示屏、TFT LCD显示屏(可支持触摸输入)等。

3.4.1迭代目标

本节我们在迭代二的基础上使用四位数码管和OLED显示屏显示相关交互信息,具体要求如:

1.当土壤湿度传感器未插入土壤中时(传感器数值为0),数码管的第1位显示”E1”;

2.当土壤湿度传感器正常工作时,数码管的第3、4位显示湿度值;

3.所有串口输出的内容,在OLED显示屏滚动式显示;

3.4.2需求分析

本节的目标主要是信息输出,不涉及操作,按要求实现即可。

3.4.3知识点

知识点3-7:字符串

在Arduino编程中,字符串是一种非常重要的数据类型,它可以用来存储和处理文本信息。

一、字符串的定义与初始化

有两种方式来定义字符串:使用字符数组和使用String类

1.字符数组

字符数组是传统的存储字符串的方式,例如:

const char firstString[] = “Hello,Arduino!”;

这里定义了一个名为firstString的字符数组。

2.String类

Arduino提供了String类,使用起来更加便利,例如:

String str = “Hello,Arduino!”;

二、字符串的常用操作

1.连接字符串

在Arduino中,可以使用”+”运算符来连接字符串,例如:

String str1 = ”Hello”;
String str2 = “, world!”;
String str3 = str1 + str2; // “Hello, world!”;

2.获取字符串长度

String类的length()函数用来获取字符串长度,例如:

String str = “Hello, Arduino!”;
int len = str.length(); // 15

3.访问字符串的字符

可以通过索上来访问字符串的单个字符,例如:

String str = “Hello, Arduino!”;
char first = str[0]; //获取第1个字符,即’H’

注意:字符串的索引从0开始;索引值必须在有效范围内,否则程序会崩溃!

4.格式化输出

很多时候需要按照一定的格式输出到字符串中,像Serial.printf那样,Arduino里提供了sprintf函数用于格式化输出到字符串,例如:

int num1 = 25;
int num2 = 30;
String str;
sprintf(str, “数值:%d, %d”, num1, num2);

在上面的格式化字符串中,用%来定义要格式化的字符,可以使用%d, %s等,更多格式请参阅Arduino编程手册。另外格式化字符数量与后面的参数数量要一致。

三、字符串转换

1.字符串与数字间的转换,例如:

int num=123;
String str = String(num); //将数字转为字符串
String str2 = “1234”;
int num2 = str2.toInt(); //将字符符转为整型数字
String str3 = “123.4”;
float num3 = str3.toFloat(); //将字符串转为浮点数字

注意:转数字时,请确保字符中内容是正确的,否则将出现异常。

2.字符串与字符数组的转换,例如:

char chs[] = “Hello, Arduino”;
String strs = String(chs); //将字符数组转为字符串
String strs2 = “Hello, world!”;
const char *chs2 = strs2.c_str();

在调用第三方库时,有些只接受C标准的字符串,即以’\0’结尾的字符串。

知识点3-8:四位数码管

四位数码管由四个独立的七段数码管组成,每个数码管可以显示0-9的数字。它们通常有共阴或共阳两种类型,共阴数码管的阴极连接在一起,共阳数码管的阳极连接在一起,如下图所示:

物联网项目开发实战-第3章-自动浇花项目迭代3

四位数码管引脚较多,一般搭配驱动芯片来使用以节省I/O端口,常见的驱动芯片有74HC595、TM1650、TM1637等,本书套件使用TM1650驱动的四位数码管模块。

四位数码管模块(TM1650驱动)

物联网项目开发实战-第3章-自动浇花项目迭代3

TM1650是一种带键盘扫描接口的LED驱动控制专用电路,只需要通过二个引脚(SCL和SDA)与MCU通讯就可以完成数码管的驱动,可节省MCU引脚资源。

安装TM1650库

在本书资源包中找到:doc\四位数码管\TM1650-1.1.0.zip文件,通过“导入库”->“添加.zip库”方式安装,安装后查看验证。

物联网项目开发实战-第3章-自动浇花项目迭代3

实例9:使用四位数码管

本实例使用四位数码模块(CLK引脚接SCL、DIO引脚接SDA)

#include <TM1650.h>

TM1650 tm1650; // 实例化

void setup() {
  Serial.begin(115200);

  Wire.begin();
  tm1650.init();  // 初始化
}

void loop() {
  tm1650.displayOff();
  tm1650.displayString("____");
  tm1650.setBrightness(TM1650_MIN_BRIGHT);
  tm1650.displayOn();
  Serial.println("显示:____");
  delay(1000);

  char line[] = "1234";
  tm1650.displayString(line);
  Serial.println("显示:1234");
  tm1650.setBrightnessGradually(TM1650_MAX_BRIGHT);
  delay(2000);
  tm1650.setBrightnessGradually(TM1650_MIN_BRIGHT);
  tm1650.displayOff();
  delay(1000);
  
  tm1650.displayString("abcd");
  Serial.println("显示:abcd");
  tm1650.displayOn();
  delay(2000);
  
  Serial.println("滚动显示:1234567890abcdefghjlnop");
  if (tm1650.displayRunning("1234567890abcdefghjlnop")) {
    while (tm1650.displayRunningShift()) delay(500);
  }
  delay(2000);
  
  Serial.println("亮度交替调节(5次)");
  for (int i = 0; i<5; i++) {
    tm1650.setBrightness(1);
    delay(200);
    tm1650.setBrightness(7);
    delay(200);
  }

  Serial.println("逐个闪烁显示:小数点(5次)");
  for (int i = 0; i<5; i++) {
    for (int j = 0; j<4; j++) {
      tm1650.setDot(j,true);
      delay(200);
    }
    for (int j = 0; j<4; j++) {
      tm1650.setDot(j,false);
      delay(200);
    }
  }

  String chars = "ABCD";
  Serial.println("在指定位显示字符");
  for (int i=0; i<chars.length();i++) {
    tm1650.clear();
    byte b = ((byte) chars[i]) & 0b01111111;
    tm1650.setPosition(i,  TM1650_CDigits[b]);
    Serial.println("在指定位显示字符");
    delay(2000);
  }

  String numbers = "5678";
  Serial.println("在指定位显示数据");
  for (int i=0; i<numbers.length(); i++) {
    tm1650.clear();
    byte b = ((byte) numbers[i]) & 0b01111111;
    tm1650.setPosition(i,  TM1650_CDigits[b]);
    Serial.println("在指定位显示字符");
    delay(2000);
  }
} 

关于TM1650库的更多信息,请访问以下网站:

https://github.com/arkhipenko/TM1650

知识点3-9:OLED显示屏

OLED一种低功耗、高显示效果的显示技术,广泛应用于小型电子设备中。不同于传统的液晶显示器,OLED不需要背光源,每个像素都能独立发光,因此能实现更高的对比度和更广的色彩范围。

常见的OLED显示屏驱动芯片有SSD系列、SH系列、UC系列、ST系列等,本书套件使用SSD1306驱动的OLED显示屏模块。

0.96寸 OLED显示屏模块(SSD1306驱动)

物联网项目开发实战-第3章-自动浇花项目迭代3

SSD1306是一款常见的OLED显示屏驱动芯片,能够驱动多种类型的小型OLED显示屏,并且支持I2C和SPI通信协议,这使得它非常适合嵌入式应用。得益于其低功耗的特性,SSD1306在电池供电的设备中非常受欢迎,常用于智能设备、传感器数据展示等项目中。

实例10:使用SSD1306图形库驱动OLED显示屏

本实例使用OLED显示屏模块(SCL引脚接SCL、SDA引脚接SDA),使用第三方库SSD1306,库安装步骤如下:

1.点击菜单”工具”->”管理库...”;

2.输入”ESP32 SSD1306”进行筛选;

物联网项目开发实战-第3章-自动浇花项目迭代3 

3.找到”ESP8266 and ESP32 OLED driver SSD1306”进行安装,这里安装的是4.6.1版本。

代码:

#include <SSD1306Wire.h>    // 4.6.1 (ThingPulse)

SSD1306Wire display(/* address */ 0x3c, 
    /* sda */ 21, 
    /* scl */ 22); 

void setup() {
  Serial.begin(115200);

  display.init();
  display.flipScreenVertically(); // 调用屏幕方向
  display.setFont(ArialMT_Plain_16); // 设置字体
}

void loop() {
  display.clear();
  display.drawString(8, 0, "Hello,ESP32");
  display.drawString(8, 16, "Welcome!");
  display.drawString(8, 32, "line3.");
  display.drawString(8, 48, "line4");
  display.display();

  delay(1000);
}

此图形库包含三个英文字体库ArialMT_Plain_10、ArialMT_Plain_16、ArialMT_Plain_24,对应的像素高度分别是10、16、24,显示屏的分辨率是128x64,读者可行换算下各字体对应能显示的行数和每行字符数。

关于SSD1306库的更多信息,请访问以下网站:

https://github.com/ThingPulse/esp8266-oled-ssd1306

本例使用的驱动库只支持显示英文字符,要显示中文,目前最常用的就是U8g2图形库。

实例11:使用U8g2图形库驱动OLED显示屏

本实例使用OLED显示屏模块(SCL引脚接SCL、SDA引脚接SDA),使用第三方库U8g2,库安装步骤如下:

1.点击菜单”工具”->”管理库...”;

2.输入”U8g2”进行筛选;

物联网项目开发实战-第3章-自动浇花项目迭代3

3.找到”U8g2”进行安装,这里安装的是2.35.30版本。

代码:

#include <U8g2lib.h>
#include <ArrayList.h>

// 使用默认引脚(21,22)
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, 
  /* reset=*/ U8X8_PIN_NONE);   
// 使用其他引脚,用如下函数
//U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, 
//  /* clock=*/26, 
//  /* data=*/25, 
//  /* reset=*/ U8X8_PIN_NONE); 

ArrayList<String> strList; // 保存要滚动的字符行

void displayWelcome() {
  char *str = "正在启动...";
  u8g2.clearBuffer();
  /* 将字符串显示在屏幕中央 */
  u8g2.drawUTF8( u8g2.getDisplayWidth() / 2 - u8g2.getUTF8Width( str ) / 2, 
    u8g2.getDisplayHeight() / 2 + u8g2.getMaxCharHeight() / 2, 
    str );
  u8g2.sendBuffer();
}

uint8_t line=0;

void setup() {
  Serial.begin(115200);
  Serial.println("Init u8g2 ....");

  u8g2.setFont( u8g2_font_wqy14_t_gb2312 ); // 设置为中文字体

  u8g2.begin();
  u8g2.enableUTF8Print(); // 设置中文
  displayWelcome();     // 显示欢迎语
  delay(2000);

  strList.add("Hello,ESP32");
  strList.add("Welcome!");
  strList.add("这是第1行.");

  u8g2.clearBuffer();
  for (int i=0; i<strList.size(); i++) {
    u8g2.drawUTF8(0, 20*(i+1), strList.get(i).c_str());
  }
  u8g2.sendBuffer();
  delay(3000);

  line=2;
}

void loop() {
  strList.add("这是第"+ String(line) +"行.");
  if (strList.size() > 3) {
    strList.remove(0);
  }

  // 滚动显示
  u8g2.clearBuffer();
  for (int i=0; i<strList.size(); i++) {
    u8g2.drawUTF8(0, 20*(i+1), strList.get(i).c_str());
  }
  u8g2.sendBuffer();

  delay(2000);
  line++;
}

本实例使用典型的三步操作来实现图形化内容输出处理

Step1: 清除缓存 u8g2.clearBuffer();

Step2: 图形化操作 u8g2.drawXXX(...);

Step3: 缓存更新 u8g2.sendBuffer();

关于U8g2库的更多信息,请访问以下网站:

https://github.com/ThingPulse/esp8266-oled-ssd1306

知识点3-10:I2C通信协议

I2C(IIC)属于两线式串行总线,由飞利浦公司开发用于微控制器(MCU)和外围设备(从设备)进行通信的一种总线,属于一主多从(一个主设备(Master),多个从设备(Slave))的总线结构,总线上的每个设备都有一个特定的设备地址,以区分同一I2C总线上的其他设备,最多支持126个从设备。

物理I2C接口有两根双向线,串行时钟线(SCL)和串行数据线(SDA)组成,可用于发送和接收数据,但是通信都是由主设备发起,从设备被动响应,实现数据的传输。

物联网项目开发实战-第3章-自动浇花项目迭代3

实例12:查询I2C设备的地址

本实例通过向地址1到127发送起始信号,并根据返回信息得知设备是否有效的方式来查询设备I2C地址。

#include <Wire.h>

void setup() {
  Serial.begin(115200); // 启动串行通信
  
  Wire.begin(); // 加入I2C总线作为主设备
  Serial.println("I2C扫描开始...");
 
  byte error, address; // 变量用于I2C错误和地址
 
  for(address = 1; address < 127; address++ ) {
    Wire.beginTransmission(address); // 向当前地址发送起始信号
    error = Wire.endTransmission(); // 结束传输,并获取错误状态
 
    if (error == 0) {
      Serial.print("发现I2C设备,地址:0x");
      if (address<16) {
        Serial.print("0");
      }
      Serial.println(address,HEX);
    }
  }
  Serial.println("扫描结束.");
}
 
void loop() {
  // 什么也不做
}

3.4.4实现

本小节迭代目标只涉及信息输出,我们在迭代二的基础上重构和添加相关代码即可。

1.程序实现

#include <ArrayList.h>
#include <U8g2lib.h>
#include <TM1650.h>

const int RELAY_PIN = 12;
const int BUTTON_PIN = 13;
const int SOIL_MOISTURE_PIN = 35;

ArrayList<int> valList;
ArrayList<String> logList;

TM1650 tm1650; // 实例化

// 使用默认引脚(21,22)
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, 
  /* reset=*/ U8X8_PIN_NONE);   

boolean button_pressed = 0;

// 输出日志
void outputLog(String str) {
  Serial.println(str);

  logList.add(str);
  if (logList.size() > 3) {
    logList.remove(0);
  }

  // 滚动显示
  u8g2.clearBuffer();
  for (int i=0; i<logList.size(); i++) {
    u8g2.drawUTF8(0, 20*(i+1), logList.get(i).c_str());
  }
  u8g2.sendBuffer();
}

// 显示湿度
void showHumidity(uint8_t val) {
  tm1650.clear();
  if (val == 0) {
    byte b = ((byte) 'E') & 0b01111111;
    tm1650.setPosition(0,  TM1650_CDigits[b]);
  } else {
    String str = String(val);
    uint8_t pos=3;
    for (int i=str.length()-1;i>=0;i--) {
      byte b = ((byte) str[i]) & 0b01111111;
      tm1650.setPosition(pos,  TM1650_CDigits[b]);
      pos--;
    }
  }
  tm1650.displayOn();
}

// 浇水子程序
void watering(uint8_t seconds) {
  outputLog("浇水 "+ String(seconds) +"秒。");
  digitalWrite(RELAY_PIN, HIGH);
  vTaskDelay(seconds * 1000 / portTICK_PERIOD_MS);
  digitalWrite(RELAY_PIN, LOW);
}

boolean buttonClicked() {
  // 检测按钮是否已按下
  if (digitalRead(BUTTON_PIN) == LOW) { // 已按下
    delay(10); // 延时10ms,用于防止机械抖动
    if (digitalRead(BUTTON_PIN)==LOW) { // 再次读取
      button_pressed = 1;
      outputLog("按键已按下。");
    } 
  }

  // 检测按键是否已释放
  if (button_pressed==1 && digitalRead(BUTTON_PIN) == HIGH) { // 已释放
    delay(10); // 延时10ms,用于防止机械抖动
    if (button_pressed==1 && digitalRead(BUTTON_PIN) == HIGH) {
      button_pressed = 0;
      outputLog("按键已释放。");
      outputLog("按键单击。");
      return true;
    }
  }

  return false;
}

// 数据采集子程序
uint8_t collectData() {
  int originVal = analogRead(SOIL_MOISTURE_PIN);
  int sensorVal = 0;
  if (originVal > 0) {
    outputLog("温度原始值:"+String(originVal));
    // 将值映射到1-100
    sensorVal = map(originVal, 1, 4095, 100, 1);
  }
  return sensorVal; 
}

// 列表数值比较
int compareAll(uint8_t cmpValue) {
  for (int i=0; i<valList.size(); i++) {
    if (valList.get(i) > cmpValue) {
      // 只要有一个值高于阀值,即判定不满足。
      return 0;
    }
  }
  return 1;
}

// 数据采集任务
void collectTask(void *pvParam) {
  while(1) {
    uint8_t val = collectData();
    showHumidity(val);
    if (val>0) {
      outputLog("土壤湿度:" + String(val));
      valList.add(val);
      if (valList.size() > 5) {
        // 超过5条,移除最早的数据。
        valList.remove(0);
      }
      if (valList.size()==5 && compareAll(50)) {
        // 有5条数据,并且都小于阀值,则浇水
        watering(5);
      }
    }

    vTaskDelay(180000 / portTICK_PERIOD_MS); //3分钟
  }
}

// 按键任务
void buttonTask(void *pvParam) {
  while(1) {
    if (buttonClicked()) {
      // 浇水5秒
      watering(5);
    }

    vTaskDelay(1 / portTICK_PERIOD_MS);
  }
}

void setup() {
  Serial.begin(115200);

  pinMode(RELAY_PIN, OUTPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP); // 使用引脚内置上拉电阻
  pinMode(SOIL_MOISTURE_PIN, INPUT);

  u8g2.setFont( u8g2_font_wqy14_t_gb2312 ); // 设置为中文字体
  u8g2.begin();
  u8g2.enableUTF8Print(); // 设置中文
  outputLog("OLED模块启动。");

  Wire.begin();
  tm1650.init();  // 初始化
  outputLog("数码管初始化。");

  // 创建按键检测任务
  xTaskCreate(
    buttonTask,    // 任务函数
    "Button_Task",   // 任务名称
    10000,          // 堆栈大小(字节)
    NULL,           // 参数
    1,              // 优先级
    NULL           // 任务句柄
  );
  outputLog("创建按键任务。");

  // 创建数据采集任务
  xTaskCreate( collectTask, "Collect_Task", 10000, NULL, 1, NULL );
  outputLog("创建数据任务。");

  outputLog("系统已启动。");
}

void loop() {
  // ...
  vTaskDelay(10 / portTICK_PERIOD_MS);
}

程序合并了迭代二实现和本节知识点的相关代码,用showHumidity函数控制数码管显示,用outputLog函数输出日志给串口和OLED显示屏,其他没有特别的地方,就不详细解释了。

2.效果图

物联网项目开发实战-第3章-自动浇花项目迭代3

- 本文由用户 老张 发布,文中观点仅代表作者本人,不代表本站立场。
- 如需转载,请联系作者;如有侵权,请联系本站处理。

09-10   阅读(8)   评论(0)
 标签: 创客 ESP32 物联网 TM1650 四位数码管 SSD1306 OLED

涨知识
寄存器

寄存器的功能是存储二进制代码,它是由具有存储功能的触发器组合起来构成的。一个触发器可以存储1位二进制代码,故存放n位二进制代码的寄存器,需用n个触发器来构成。

评论:
相关文章
物联网项目开发实战-第3章-自动浇花项目迭代2

本节我们在迭代一的基础上增加采集土壤湿度数据,并根据湿度数据来决定是否自动进行浇水动作。


物联网项目开发实战-第3章-自动浇花项目迭代1

本节我们实现一个基本能工作的手动浇水装置,即通过按下按键来闭合继发器让小水泵进行浇水。


物联网项目开发实战-第2章-开发环境

本小节通过点亮LED和串口输出两个程序,来初步掌握ArduinoIDE、了解GPIO和串口使用、同时把开发环境与开发板的连接,上传程序的各环节跑通,


小鹏浇花套件单机版程序V1.0.0

本程序是小鹏物联网智能浇花套件的单机版程序(不连接物联网),供同学们参考。


ESP32 的中断机制和处理

本文介绍ESP32中的中断机制,以及如何通过GPIO中断实现按钮控制。重点讲解了如何设置中断服务例程、处理中断抖动问题,并提供了消除中断抖动的示例代码。


在ESP32上实现WEB交互界面

本文主要介绍在未联网(AP热点)情况下实现WEB交互界面的CSS和javascript库。


Arduino ESP32获取芯片、RAM信息

本文介绍如何使用Arduino-ESP32库中的API函数获取ESP32的芯片、RAM信息等,并提供了一个示例程序代码。


ESP32 FreeRTOS 双核使用

ESP32系列(包括ESP32-S3)搭载Xtensa双核处理器,默认情况下Arduino框架仅使用单核运行用户代码,通过多核编程,可以充分利用硬件资源来提升系统响应和性能。


ESP32 GPIO 矩阵和引脚多路复用

ESP32 芯片有34个物理GPIO管脚。每个GPIO管脚都可用作一个通用IO,或连接一个内部的外设信号。IO_MUX ¹、RTC IO MUX 和GPIO交换矩阵用于将信号从外设传输至GPIO管脚。


ESP32Encoder:高效的ESP32旋转编码器库

ESP32Encoder库是一个利用ESP32脉冲计数器硬件外设实现高效旋转编码器读取的软件库。