在前面的两个迭代里,我们通过串口输出了一些程序运行的信息,但查看串口输出信息需要使用Arduino串口监视器或其他第三方串口查看软件,这对于产品的最终用户来说,是非常不友好的。可以通过增加人机交互设备来显示这些信息。
常见的人机交互设备有:7段式数码管、OLED显示屏、TFT LCD显示屏(可支持触摸输入)等。
本节我们在迭代二的基础上使用四位数码管和OLED显示屏显示相关交互信息,具体要求如:
1.当土壤湿度传感器未插入土壤中时(传感器数值为0),数码管的第1位显示”E1”;
2.当土壤湿度传感器正常工作时,数码管的第3、4位显示湿度值;
3.所有串口输出的内容,在OLED显示屏滚动式显示;
本节的目标主要是信息输出,不涉及操作,按要求实现即可。
在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’结尾的字符串。
四位数码管由四个独立的七段数码管组成,每个数码管可以显示0-9的数字。它们通常有共阴或共阳两种类型,共阴数码管的阴极连接在一起,共阳数码管的阳极连接在一起,如下图所示:
四位数码管引脚较多,一般搭配驱动芯片来使用以节省I/O端口,常见的驱动芯片有74HC595、TM1650、TM1637等,本书套件使用TM1650驱动的四位数码管模块。
四位数码管模块(TM1650驱动)
TM1650是一种带键盘扫描接口的LED驱动控制专用电路,只需要通过二个引脚(SCL和SDA)与MCU通讯就可以完成数码管的驱动,可节省MCU引脚资源。
安装TM1650库
在本书资源包中找到:doc\四位数码管\TM1650-1.1.0.zip文件,通过“导入库”->“添加.zip库”方式安装,安装后查看验证。
本实例使用四位数码模块(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
OLED一种低功耗、高显示效果的显示技术,广泛应用于小型电子设备中。不同于传统的液晶显示器,OLED不需要背光源,每个像素都能独立发光,因此能实现更高的对比度和更广的色彩范围。
常见的OLED显示屏驱动芯片有SSD系列、SH系列、UC系列、ST系列等,本书套件使用SSD1306驱动的OLED显示屏模块。
0.96寸 OLED显示屏模块(SSD1306驱动)
SSD1306是一款常见的OLED显示屏驱动芯片,能够驱动多种类型的小型OLED显示屏,并且支持I2C和SPI通信协议,这使得它非常适合嵌入式应用。得益于其低功耗的特性,SSD1306在电池供电的设备中非常受欢迎,常用于智能设备、传感器数据展示等项目中。
本实例使用OLED显示屏模块(SCL引脚接SCL、SDA引脚接SDA),使用第三方库SSD1306,库安装步骤如下:
1.点击菜单”工具”->”管理库...”;
2.输入”ESP32 SSD1306”进行筛选;
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图形库。
本实例使用OLED显示屏模块(SCL引脚接SCL、SDA引脚接SDA),使用第三方库U8g2,库安装步骤如下:
1.点击菜单”工具”->”管理库...”;
2.输入”U8g2”进行筛选;
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
I2C(IIC)属于两线式串行总线,由飞利浦公司开发用于微控制器(MCU)和外围设备(从设备)进行通信的一种总线,属于一主多从(一个主设备(Master),多个从设备(Slave))的总线结构,总线上的每个设备都有一个特定的设备地址,以区分同一I2C总线上的其他设备,最多支持126个从设备。
物理I2C接口有两根双向线,串行时钟线(SCL)和串行数据线(SDA)组成,可用于发送和接收数据,但是通信都是由主设备发起,从设备被动响应,实现数据的传输。
本实例通过向地址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() {
// 什么也不做
}
本小节迭代目标只涉及信息输出,我们在迭代二的基础上重构和添加相关代码即可。
#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显示屏,其他没有特别的地方,就不详细解释了。
寄存器的功能是存储二进制代码,它是由具有存储功能的触发器组合起来构成的。一个触发器可以存储1位二进制代码,故存放n位二进制代码的寄存器,需用n个触发器来构成。
本节我们在迭代一的基础上增加采集土壤湿度数据,并根据湿度数据来决定是否自动进行浇水动作。
本节我们实现一个基本能工作的手动浇水装置,即通过按下按键来闭合继发器让小水泵进行浇水。
本小节通过点亮LED和串口输出两个程序,来初步掌握ArduinoIDE、了解GPIO和串口使用、同时把开发环境与开发板的连接,上传程序的各环节跑通,
本程序是小鹏物联网智能浇花套件的单机版程序(不连接物联网),供同学们参考。
本文介绍ESP32中的中断机制,以及如何通过GPIO中断实现按钮控制。重点讲解了如何设置中断服务例程、处理中断抖动问题,并提供了消除中断抖动的示例代码。
本文主要介绍在未联网(AP热点)情况下实现WEB交互界面的CSS和javascript库。
本文介绍如何使用Arduino-ESP32库中的API函数获取ESP32的芯片、RAM信息等,并提供了一个示例程序代码。
ESP32系列(包括ESP32-S3)搭载Xtensa双核处理器,默认情况下Arduino框架仅使用单核运行用户代码,通过多核编程,可以充分利用硬件资源来提升系统响应和性能。
ESP32 芯片有34个物理GPIO管脚。每个GPIO管脚都可用作一个通用IO,或连接一个内部的外设信号。IO_MUX ¹、RTC IO MUX 和GPIO交换矩阵用于将信号从外设传输至GPIO管脚。
ESP32Encoder库是一个利用ESP32脉冲计数器硬件外设实现高效旋转编码器读取的软件库。