前言
本项目实现了2023寒假一起练平台(5)- 基于ESP32 WiFi 的综合应用的项目2的要求,“ 游戏手柄控制LCD上的信息”,即
用ESP32板测量IO扩展板上的PWM信号,在LCD上以图形化的方式显示游戏摇杆的变化,通过游戏摇杆的拨动,能够触及LCD的全屏幕。
硬件介绍
2023寒假一起练平台(5)是基于ESP32S2-WIFI无线模组的综合应用平台
ESP32-S2 是一款由上海乐鑫推出的高度集成、高性价比、低功耗、主打安全的单核 Wi-Fi SoC,具备强大的功能和丰富的 IO 接口。相较于我们经常使用的ESP32,其主要的区别是核心数由双核变成了单核并去掉了蓝牙的支持以降低成本和功耗,同时增加了IO的数量并添加了对USB接口的支持。
软件开发方面,与主流的ESP32基本相同。其开发环境也是同样的丰富,可以通过ESP-IDF, Micro Python, Arduino等多种手段进行开发。
本次硬禾使用的ESP32-S2硬件平台包含了一个基于ESP32-S2-MINI的WiFi模块和一块功能拓展底板。
WiFi模块的功能如下
- ESP32-S2-MINI-1模组
- 这是一款2.4 GHz WiFi 模组
- 内置 ESP32S2 系列芯片,Xtensa® 单核 32 位 LX7 微处理器
- 内置芯片叠封 4 MB flash,可叠封 2 MB PSRAM
- 37 个 GPIO,具有丰富的外设
- 板载 PCB 天线
原理图:
配套的扩展底板上板载了以下外设
- 按键、旋转编码器输入 - 以模拟信号的方式
- 双电位计控制输入 - 以数字信号的方式
- RGB三色LED显示
- 1.44寸128*128 LCD,SPI总线访问
- MMA7660三轴姿态传感器
- 电阻加热
- 温度传感器
- 与ESP32-S2核心模块的接口
原理图:
本次活动设定了6个不同的项目目标,我选择的是,项目2,游戏手柄控制LCD上的信息.即在LCD上以图形化的方式显示游戏摇杆的变化,通过游戏摇杆的拨动,触及LCD的全屏幕。对与本项目而言,我们将使用底板上的摇杆和LCD两个外设。这两个外设与ESP32S2的引脚映射如下:
外设信号 | 引脚编号 |
摇杆信号输入 | GPIO2 |
LCD显示屏时钟信号 SCLK | GPIO41 |
LCD显示屏数据信号 MOSI | GPIO21 |
LCD显示屏复位信号 RST | GPIO18 |
LCD显示屏片选信号 CS | GPIO13 |
LCD显示屏数据/指令选择信号 DCX | GPIO17 |
开发环境介绍
ESP32-S2可以通过ESP32-IDF, Micro Python, Arduino等多种手段进行开发。本项目中使用的是经典的Arduino IDE来进行开发。
实现思路
本项目的代码需要分成两个主要的功能逻辑部分。一部分是对摇杆当前状态的测量(输入子系统),包括PWM波形的测量和光标的管理两部分;另一部分则是对LCD屏幕的UI绘制(图形子系统),包括对LCD的控制(初始化,旋转控制等)和图形绘制(光标、边框等)。
因为使用了基于Arduino的开发平台,所以驱动子系统无需手动编写,直接由Arduino完成。我们可以直接忽略掉此部分。
程序主体架构示意图如下:
程序的主要流程如下:
摇杆状态的测量
由项目的硬件资料可知,与通常的输出模拟信号的摇杆不同。本次硬件平台的摇杆输出的是一个PWM方波讯号。摇杆在X方向的位置变化会影响PWM的频率,而摇杆Y方向的位置的变化则会影响PWM的占空比。这样,我们就不能通过ADC读取摇杆在两个方向上的分量来测量摇杆的当前位置,取而代之的是我们需要测量这个PWM波的频率和占空比,这分别对应摇杆在X,Y方向上的位置。之后我们就可以根据摇杆的位置来决定光标移动的方向和移动的速度。与传统的模拟输出相比,这样的好处是可以节省一个IO(模拟方式需要独立对两个方向上的输出电压进行测量)而且可以得到更加精确的位置(因为频率测量可以达到很高的精度),但是缺点是比起ADC方式来说不够直观。
那么,该如何测量频率和占空比呢?这里,我用到了ESP32S2的外部中断功能。
在初始化阶段,我们通过这行代码注册一个引脚中断的处理回调函数。这样,当引脚上的电平发生变化时,这个函数就会被调用。
pinMode(JOYSTICK_INPUT_PIN, INPUT); // 设置摇杆连接的引脚为输入模式
attachInterrupt(JOYSTICK_INPUT_PIN, PWMCouter, CHANGE); // 开启中断并注册中断处理函数
在函数被调用后,我们在代码中判断当前是波形的上升沿还是下降沿,这通过读取当前引脚的电平就可以获得。
// 中断处理函数
static void ARDUINO_ISR_ATTR PWMCouter() {
// 通过读取触发中断时的引脚状态来判断当前触发边沿是上升沿还是下降沿
if (digitalRead(JOYSTICK_INPUT_PIN)) {
// 上升沿时的操作
}
else {
// 下降沿时的操作
}
}
通过记录波形每两次上升沿之间的时间差,我们就可以得到波形的周期。这里我们使用Arduino驱动中提供的microSeconds函数来提供一个时间戳。microSeconds记录了程序自启动后的运行时间,单位是微秒。这样我们就可以实现最高1MHz的频率测量。
我们在每次上升沿触发的时候,通过计算本次触发的时间戳和上次触发的时间戳的差值就可以得到时间差(也就是波形周期)。
周期有了,它的倒数自然就是波形的频率了。
if (digitalRead(JOYSTICK_INPUT_PIN)) {
// 每两次上升沿之间的时间差就是周期
pwmPeriod = trigTimestamp - lastRiseTimestamp;
if (pwmPeriod != 0) {
// 周期的倒数就是频率
pwmFreq = 1000000UL / pwmPeriod;
}
lastRiseTimestamp = trigTimestamp;
}
占空比的测量也同样非常简单,我们记录任何下降沿发生的时间,然后这个下降沿与上一个上升沿的时间差就是波形的正脉宽。我们用正脉宽时间除以波形的周期,就可以得到波形的占空比了。这里通过乘以1000来获得占空比的千分比值。
if (lastFallingTimestamp < lastRiseTimestamp) {
// 正脉宽长度
highTime = trigTimestamp - lastRiseTimestamp;
if (pwmPeriod != 0) {
// 正脉宽除以周期就是占空比
pwmDuty = highTime * 1000 / pwmPeriod;
}
lastFallingTimestamp = trigTimestamp;
}
else if (trigTimestamp < lastFallingTimestamp) {
lastFallingTimestamp = trigTimestamp;
}
至此,我们就成功得到了摇杆当前的状态。
得到了摇杆的状态后,为了实现“光标移动的速度与拨动摇杆的力度相对应”的功能,我们就需要对光标的加速度进行计算。因为我们获取摇杆状态是在系统中断中完成的。基于“中断处理最简化”的策略,我们最好不要在中断处理函数中进行额外的操作。所以我们将此部分的功能放在接下来的UI绘制中完成。
图形子系统(LCD驱动和UI绘制)
这里,我使用了TFT_eSPI来驱动拓展板上的这一块儿SPI接口的LCD彩屏。
TFT_eSPI tft = TFT_eSPI();
TFT_eSprite spr = TFT_eSprite(&tft);
TFT_eSPI需要一个单独的配置文件,我们自己创建一个,名为“STEP_ESP32S2_ST7735_128x128.h”,里面主要是设定屏幕的引脚映射和SPI的参数
#define USER_SETUP_INFO "STEP_ESP32S2_ST7735_128x128"
#define ST7735_DRIVER // Define additional parameters below for this display
#define TFT_WIDTH 128
#define TFT_HEIGHT 128
#define ST7735_GREENTAB3
#define TFT_INVERSION_OFF
#define TFT_MISO -1
#define TFT_MOSI 21
#define TFT_SCLK 41
#define TFT_CS 13 // Chip select control pin
#define TFT_DC 17 // Data Command control pin
#define TFT_RST 18 // Reset pin (could connect to RST pin)
#define LOAD_GLCD // Font 1. Original Adafruit 8 pixel font needs ~1820 bytes in FLASH
#define LOAD_FONT2 // Font 2. Small 16 pixel high font, needs ~3534 bytes in FLASH, 96 characters
#define LOAD_FONT4 // Font 4. Medium 26 pixel high font, needs ~5848 bytes in FLASH, 96 characters
#define LOAD_FONT6 // Font 6. Large 48 pixel font, needs ~2666 bytes in FLASH, only characters 1234567890:-.apm
#define LOAD_FONT7 // Font 7. 7 segment 48 pixel font, needs ~2438 bytes in FLASH, only characters 1234567890:-.
#define LOAD_FONT8 // Font 8. Large 75 pixel font needs ~3256 bytes in FLASH, only characters 1234567890:-.
#define LOAD_GFXFF // FreeFonts. Include access to the 48 Adafruit_GFX free fonts FF1 to FF48 and custom fonts
#define SMOOTH_FONT
#define SPI_FREQUENCY 40000000
#define SPI_READ_FREQUENCY 20000000
#define SPI_TOUCH_FREQUENCY 2500000
在Setup我们对屏幕进行初始化。
// 初始化显示屏
tft.init();
tft.setRotation(0);
tft.fillScreen(TFT_BLACK);
tft.setTextSize(1);
tft.setTextColor(TFT_WHITE);
tft.setCursor(0, 0);
由于ESP32S2拥有高达320KB的SRAM,所以为了最高的图形表现,我这里创建了大小跟屏幕分辨率完全相同的“精灵”。
// 创建一个大小等于屏幕的“精灵”(或者说framebuffer)
spr.createSprite(TFT_WIDTH, TFT_HEIGHT);
使用精灵的好处是,我们可以在精灵中完成所有图形的绘制之后,再一次性的将精灵传输至屏幕。这样屏幕上就不会出现我们绘制UI的过程,也就没有闪烁和错位了。而且效率也会更高。
对于本次使用的LCD屏,全屏大小的精灵会消耗32KB的内存,但对于ESP32S2来说,这就是毛毛雨了。
初始化完成后,我们通过这个函数绘制开场的动画logo
void DrawIntroAnim() {
delay(1000);
for (int i = 0; i < 41; i++) {
spr.fillSprite(TFT_WHITE);
uint16_t* pImg = (uint16_t*)gImage_img_eetree_logo;
spr.pushImage(4, 85 - i, 120, i + 1, pImg, 16);
spr.pushSprite(0, 0);
delay(20);
}
}
然后进入程序的主循环loop了,在loop的每一次循环,我们绘制一帧。 在每一帧的开始,我们通过读取测量得到的PWM波形的频率和占空比,通过map函数转化为摇杆在XY方向的加速度
// 计算X方向加速度值
if (pwmFreq >= PWM_FREQ_TH_MAX && pwmFreq <= PWM_FREQ_MAX) {
accX = map(pwmFreq, PWM_FREQ_TH_MAX, PWM_FREQ_MAX, 0, JOYSTICK_ACC_MAX + 1);
}
else if (pwmFreq <= PWM_FREQ_TH_MIN && pwmFreq >= PWM_FREQ_MIN) {
accX = map(pwmFreq, PWM_FREQ_MIN, PWM_FREQ_TH_MIN, -JOYSTICK_ACC_MAX, 0);
}
else {
accX = 0;
}
// 计算Y方向加速度值
if (pwmDuty >= PWM_DUTY_TH_MAX && pwmDuty <= PWM_DUTY_MAX) {
accY = map(pwmDuty, PWM_DUTY_TH_MAX, PWM_DUTY_MAX, 0, JOYSTICK_ACC_MAX + 1);
}
else if (pwmDuty <= PWM_DUTY_TH_MIN && pwmDuty >= PWM_DUTY_MIN) {
accY = map(pwmDuty, PWM_DUTY_MIN, PWM_DUTY_TH_MIN, -JOYSTICK_ACC_MAX, 0);
} else {
accY = 0;
}
然后根据加速度更新光标的位置
// 计算光标的最新位置
curX += accX;
curY += accY;
if (curX >= TFT_WIDTH - 1) {
curX = TFT_WIDTH - 1;
}
if (curX < 0) {
curX = 0;
}
if (curY >= TFT_HEIGHT - 1) {
curY = TFT_HEIGHT - 1;
}
if (curY < 0) {
curY = 0;
}
然后将全部的信息和光标绘制出来。
// 绘制信息
spr.setTextColor(TFT_ORANGE, TFT_ORANGE);
sprintf(strbuf, "X:%d, Y:%d;", curX, curY);
spr.drawString(strbuf, 2, 2, 1);
sprintf(strbuf, "ACC-X:%d, ACC-Y:%d", accX, accY);
spr.drawString(strbuf, 2, 12, 1);
sprintf(strbuf, "Fq:%uHz, Dt:%d.%d%%", pwmFreq, pwmDuty / 10, pwmDuty % 10);
spr.drawString(strbuf, 2, 22, 1);
// 绘制光标
spr.drawLine(curX - JOYSTICK_CURSOR_SIZE, curY, curX + JOYSTICK_CURSOR_SIZE, curY, JOYSTICK_CURSOR_COLOR);
spr.drawLine(curX, curY - JOYSTICK_CURSOR_SIZE, curX, curY + JOYSTICK_CURSOR_SIZE, JOYSTICK_CURSOR_COLOR);
因为我们创建了全屏的缓冲区,所以其实我们刚才对于屏幕的所有操作都是在内存中执行的,并没有真的显示在屏幕上。在完成了所有内容的绘制后,我们需要把精灵中的图形传输至LCD屏显示。
在完成绘制后,我们稍微等待一会儿,绘制下一帧。这里等待40毫秒意味着我们的屏幕帧率大约是25帧,是适中的取舍。
spr.pushSprite(0, 0);
delay(40);
至此,我们就实现了全部的项目需求。
结语
通过本次活动,我们学习到了ESP32S2开发的一些基础知识、还有波形的测量和LCD屏的驱动。希望我在本项目中的分享可以帮到你。也希望电子森林的活动能越办越好。谢谢大家。