65.9K
CodeProject 正在变化。 阅读更多。
Home

基于 TTGO 显示屏 T1 的风扇小部件

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2023年1月9日

MIT

8分钟阅读

viewsIcon

7524

downloadIcon

72

一个项目,允许您使用旋钮控制风扇的 RPM 并提供反馈

注意:GitHub版本稍微高级一些。我希望这个演示保持简单。

TTGO Fan RPM Controller

引言

我想要一个小的项目来展示我创建的一些物联网生态系统(htcw_*),而且这似乎是一种打发一两个小时的有趣方式。

这个项目使用了几乎无处不在且备受推崇(至少在ESP32社区中)的TTGO Display T1,因为它是一个不错的带有集成彩色显示屏的开发套件。它易于找到,并且相对于其功能而言相对便宜。我发现它们的价格低至12美元加上运费。它还使用一个基于PWM的4针风扇,就像你为PC购买的那种。

必备组件

  • 你需要一个12V基于PWM的4针风扇——带转速反馈的那种,并提防那些使用虚假转速表(总是报告相同RPM)的廉价风扇。
  • 你可能需要一个电平转换器。理论上你可能可以不用,但我建议使用它,并且不要让电路在其操作规格之外运行。
  • 你需要一个TTGO Display T1
  • 你需要一个12V >= 0.2A的电源
  • 有几根双头公杜邦线(两边都长)来制作风扇和12V电源鳄鱼夹的连接点会有帮助,尽管你可能可以使用实心线。
  • 你需要一个用于Arduino的2线正交编码器
  • 你需要安装了PlatformIO扩展的VS Code
  • 你需要一些耐心来按照wiring.txt连接整个东西。请小心操作,因为你正在处理12V电压,所以如果你将其连接到风扇以外的任何东西,都会烧坏设备。在共享地线时我会格外紧张,但对于这个电路来说没问题。

理解这个项目

在技术方面,我之前没有涵盖的内容不多,但在这里我们将所有这些整合在一起。我引入的唯一新东西是htcw_encoder,它非常简单,以及htcw_ttgo,它通过为你引入所有集成的硬件支持,简化了使用此特定开发套件的过程。它使用htcw_gfx进行显示,包括我从fontsquirrel.com获取并使用我的在线工具转换为头文件的True Type字体。

启动时,它会稍微运行风扇以确定其最大RPM。完成后,它将停止风扇,你可以顺时针转动编码器旋钮(除非你反接了引脚!)来提高RPM。然后它将使用自适应RPM目标来尝试将风扇保持在您通过编码器旋钮指示的相同RPM。请记住,这对于低于最小有效速率的RPM效果不佳,该速率因风扇而异,但在我的风扇上约为360左右。它会尝试,但在低RPM时会抖动。

您将通过显示屏看到风扇RPM的反馈。这些风扇的转速表并不完美,所以有时它们报告的值会瞬间飙升一点,但除此之外,一切都很简单。

编写这堆乱七八糟的代码

由于我的生态系统为您处理了大量的繁琐工作,所以这方面没有太多内容。其余的都非常简单,但我们仍将涵盖它。所有核心内容都在一个文件中。

main.cpp

首先,我们处理一些样板文件。

#define ENCODER_DATA 17
#define ENCODER_CLK 13
#define FAN_TACH 33
#define FAN_PWM 32
#define MAX_RPM NAN
#include <ttgo.hpp>
#include <encoder.hpp>
#include <fan_controller.hpp>
// downloaded from fontsquirrel.com and header generated with 
// https://honeythecodewitch.com/gfx/generator
#include <fonts/Telegrama.hpp>

在这里,我们只进行了一些定义,以保持代码的可读性和可维护性,然后我们包含了ttgo硬件支持、编码器支持、风扇支持,最后是为之生成头文件的字体。

接下来我们声明TTGO外部的硬件——编码器和风扇

static int_encoder<ENCODER_DATA,ENCODER_CLK,true> knob;
static fan_controller fan(
    [](uint16_t duty,void* state){ ledcWrite(0,duty>>8); },
    nullptr,
    FAN_TACH, MAX_RPM);

这里我们声明了通过中断工作的编码器和风扇控制器,以及一个扁平的lambda来处理脉冲生成回调。我们还为风扇最大RPM传入NAN,以便它能检测到,但你可以更改这个值。

临时字符串只是为了避免在需要格式化某些内容时占用宝贵的堆栈空间。

static char tmpsz[256];

最后,我们开始处理一些非琐碎的事情——在屏幕上绘制居中文本

static void draw_center_text(const char* text, int size=30) {
    draw::filled_rectangle(lcd,lcd.bounds(),color_t::purple);
    open_text_info oti;
    oti.font = &Telegrama;
    oti.text = text;
    oti.scale = oti.font->scale(size);
    oti.transparent_background = false;
    srect16 txtr = oti.font->measure_text(
        ssize16::max(),
        spoint16::zero(),
        oti.text,
        oti.scale).bounds();
    txtr.center_inplace((srect16)lcd.bounds());
    draw::text(lcd,txtr,oti,color_t::white,color_t::purple);
}

我们做的第一件事是通过绘制一个与 lcd 具有相同 bounds() 的大紫色 filled_rectangle() 来清除屏幕。

之后,我们用字体和文本信息填充一个`open_text_info`结构。字体基于您创建文件时在生成器工具上指示的名称,通常与头文件的文件名相同,只是没有扩展名。请注意,它是一个指向字体的指针,因为在这种情况下引用不起作用。

注意比例。`scale()` 例程将以像素行高为单位的字体大小转换为字体原生高度的分数百分比(这个原生高度很大,所以 `scale()` 返回的数字会很小)。

我们关闭了 `transparent_background`(默认开启),原因有二。首先是性能,因为 alpha 混合需要时间,因此对远程帧缓冲区(例如 LCD 的帧缓冲区)进行抗锯齿处理会耗时。如果您绘制到非透明背景,则无需从 LCD 读取。第二个原因更为关键且非常不幸。Espressif 似乎在最新的 Arduino 框架实现中破坏了“SDA 读取”,导致它们仍然有点作用,但速度非常慢,并且会在串口输出大量“无效引脚选择”错误消息。ST7789 显示控制器需要使用 SDA 读取才能进行 alpha 混合和抗锯齿处理,这就会产生问题。将背景设置为非透明可以规避上述问题。

现在我们使用 `measure_text()`。第一个参数是文本布局区域的有效大小。我们只使用 `ssize16` 的最大值。第二个参数是字体布局的偏移量(如果有的话)——它开始绘制的位置。我们使用 (0,0)。下一个参数是要测量的文本,最后,我们指示用于字体的比例。然后我们将 `ssize16` 转换为 `srect16`,通过获取其 `bounds()`。

接下来,我们通过就地修改矩形的值来使其居中,并根据显示器的大小进行操作。

最后,我们将`text()`绘制到LCD上,指定目标矩形,使用我们上面填充的`struct`,并指定前景色和背景色。

现在我们进入 `setup()` 函数,在这里我们开始所有操作

void setup() {
    Serial.begin(115200);
    ttgo_initialize();
    lcd.rotation(1);
    
    ledcSetup(0,25*1000,8);
    ledcAttachPin(FAN_PWM,0);
    if(MAX_RPM!=MAX_RPM) {
        draw_center_text("detecting fan...",20);
    }
    fan.initialize();
    fan.pwm_duty(0);
    snprintf(tmpsz,
            sizeof(tmpsz),
            "Max RPM: %d",
            (int)fan.max_rpm());
    draw_center_text(tmpsz,20);
    knob.initialize();
    delay(3000);
}

首先,我们启动串口,然后初始化TTGO硬件。现在我们将LCD的旋转设置为横向,以便按钮在右侧。

现在我们为ESP32设置一个PWM通道。风扇控制器不直接处理PWM信号,因为您可能正在使用外部硬件作为发生器,或者您可能在一个需要外部硬件的平台上。这里我们只使用ESP32的内部硬件,所以我们相应地将其设置为25KHz,在通道0上具有8位分辨率,并将其连接到风扇的PWM线(当然,通过电平转换器间接连接!)。

现在我们检查 MAX_RPM 是否是一个有效数字。如果不是,它将不等于自身,所以我们知道 `fan.initialize()` 调用将通过全速驱动风扇并获取结果来确定最大RPM。这需要一点时间,所以在此过程中我们会显示一条消息。

接下来,我们将PWM占空比设置为零,关闭风扇。

下一行用风扇的最大报告转速格式化一个字符串。

最后,我们绘制字符串,初始化旋钮,并等待3秒。

现在我们有 `loop()` 和一些簿记变量

static float old_rpm=NAN;
static long long old_knob=-1;
static uint32_t ts=0;
void loop() {
    fan.update();
    uint32_t ms = millis();
    if(ms>ts+250) {
        ts = ms;
        if(old_rpm!=fan.rpm()) {
            Serial.print("RPM: ");
            Serial.println(fan.rpm());
            old_rpm = fan.rpm();
            snprintf(tmpsz,
                    sizeof(tmpsz),
                    "Fan RPM: %d",
                    (int)fan.rpm());
            draw_center_text(tmpsz,20);
        }
    }
    
    if(knob.position()<0) {
        knob.position(0);
    } else if(knob.position()>100) {
        knob.position(100);
    }
    if(old_knob!=knob.position()) {
        Serial.print("Knob: ");
        Serial.println(knob.position());
        float new_rpm = fan.max_rpm()*
            (knob.position()/100.0);
        Serial.print("New RPM: ");
        Serial.println(new_rpm);
        fan.rpm(new_rpm);
        old_knob = knob.position();
    }

    dimmer.wake();
    ttgo_update();
}

这里我们有一些变量来跟踪之前的RPM和之前的旋钮位置,然后是一个时间戳,这样我们就可以在`loop()`中实现一个简单的计时器。

首先,我们让风扇有机会 `update()` 它需要做的任何事情。

接下来我们实现一个简单的计时器。我们获取当前的 `millis()` 并将其与上次时间戳加上四分之一秒进行比较。如果大于,我们运行 `if(){...}` 中的代码。

在上面,我们重置了时间戳,并将先前的RPM与当前的RPM进行比较。如果它发生了变化,我们会将其报告到串口,并格式化一个字符串,然后报告到屏幕上。

接下来,我们将旋钮的 `position()` 限制在0到100之间(包括0和100)。

现在,如果旋钮的值自上次检查以来发生变化,我们会将其打印到串口,并重新计算目标RPM,将旋钮的位置视为最大RPM的百分比。

现在我们将新的转速报告到串口。

最后,我们唤醒调光器,因为我们没有使用它,也不想让它超时,然后我们给TTGO硬件一个更新的机会。我们不需要做最后一步,因为我们没有使用内置按钮,但我为了完整性将其包含在内。

祝您编码愉快!

历史

  • 2023年1月9日 - 初次提交
© . All rights reserved.