外观
软件开发
开发环境
- 软件环境:VSCode+PlatformIO
- 开发语言:C/C++
USB HID协议
在本项目中我们主要使用的是USB HID协议,在开发之前建议先认真阅读USB组织提供的USB HID协议文档 - 第10章 Keyboard/KeypadPage(0x07)
库
通过以下库完成本项目开发 - TinyUSB Library:用于USB协议收发
- NeoPixel:用于RGB驱动
- GFX Library:用于图形绘制
- SSD1306:用于屏幕驱动
函数定义
通过以下函数完成小键盘逻辑处理 - uint32_t Wheel(byte WheelPos) :彩虹色轮生成函数
- void effectRainbow():彩虹灯效
- void effectMarquee():跑马灯效
- void runLEDEffect():灯效调用
- void handleEncoderRotation():EC11旋钮逻辑判断
- float evaluateExpression(String expr):计算器逻辑处理
本项目的构建环境为VsCode、PlatformIO完成,当然也可以使用JetBrains CLion 2024、PlatfromIO或Arduino完成,根据个人喜好选择即可。在本教程中主要使用VsCode作为编译器开发
2.1.1 VsCode安装
方法1:官网下载
VsCode是一个强大的主流代码编辑器,也是完全免费使用的,我们可以在VsCode官网下载安装即可 https://code.visualstudio.com/insiders/
方法2:应用商店下载
这种方法更为简便,您可以使用Windows自带微软商店搜索并下载
2.1.2 PlatfromIO框架安装
PlatfromIO是一个更强大更专业的开发框架,支持ARM、FPGA、RISC-V以及8位单片机的开发,是一个嵌入式多平台开发工具,可以支持Arduino、ESPIDF、STM32Cube等多个固件的开发,且自定义程度较高、支持开源库的快速安装使用,非常方便。 在VsCode点击左侧插件图标,搜索安装PlatfromIO框架插件即可,使用ArduinoIDE也可直接用ArduinoIDE完成开发,但ArduinoIDE功能有限,修改库函数和板级配置困难,所以不推荐使用
2.1.3 程序下载说明
RP2350模组主要使用TTL串口下载方式,准备一个USB转TTL或者DAP-Link即可
(1)安装CH340驱动
鼠标右键以管理员身份进行安装; 如果出现安装失败,那可能是你电脑已经有了该驱动,你可以尝试先点击卸载,再次安装即可。
2.2 流程设计
在开发之前,我们先要对整个程序的流程进行设计,确保知道我们的项目需要做什么
2.2.1 整体程序分析
对于本项目来说,我们应该USB-HID功能模拟键盘功能,以及在非主机设备下为计算器功能,大致的程序流程如下。
当然,这仅局限于本教程,你也可以为你的小电视开发更多好玩有意思的功能
2.3 创建项目
2.3.1 构建项目框架
(1)创建项目
首先,我们新建一个PlatformIO项目,修改好项目名称,版型选择Espressif中的ESP32-S3-DevKitC-1-N8,使用Arduino固件然后点击创建。(版型的选择是随意的,主要是选对正确的芯片类型,因为ESP32S3有多种款式,我们使用的类型是ESP32,8M)
(2)配置SDK
因为默认的platform默认配置仅支持RP2040,但我们使用的是RP2350,所以这里我们还需修改配置
[env:rpipico2-riscv]
board = rpipico2
platform = https://github.com/maxgerhardt/platform-raspberrypi.git
framework = arduino
board_build.core = earlephilhower
build_flags = -DUSE_TINYUSB -DCFG_TUSB_RHPORT0_MODE
1
2
3
4
5
6
2
3
4
5
6
2.3.2 项目结构介绍
(1)项目文件夹
在项目文件夹下共有5个文件夹,这些文件夹我们在后面介绍。我们先来讲解该目录下最为重要的platformio.ini文件,这个文件是用于存放我们需要调用的第三方库和配置板级配置的文件,在这个文件中可以配置分区、配置处理器频率、固件类型等等 platformio.ini 默认的文件内容如下,platform指的是我们使用的平台,board指的是我们使用的版型,framework指的是我们使用的固件类型,完整的文件配置信息可以查看官方的说明文档 https://docs.platformio.org/en/latest/projectconf/index.html
(2).pio文件夹
.pio文件夹是用于存放我们依赖的第三方库文件以及构建后的项目固件及构建文件的地方,默认文件如下 build 用于存放构建文件,生成的固件也会在这个文件夹中 libdeps 当引入第三方库时才会生成该目录,在该目录下可以修改第三方库的源代码
(3)src文件夹
其他文件夹就不再讲解了,主要是用于分类和自行导入第三方库用的。 src文件夹是生成初始程序的文件夹,在新建的项目中会在这里生成main.cpp,这个就是我们的主程序了
2.3.3 程序结构介绍
这就是一个基本的Arduino程序结构,在程序最上面导入头文件和全局变量。 setup() 在这个方法中我们主要存放需要初始化的代码,在该方法中的所有程序仅在开机时执行一次 loop() 在这个方法在我们主要存放主代码,在该方法中的所有程序会进入死循环,无限次重复执行
2.4 驱动库
2.4.1 屏幕
屏幕这里我们选用SSD1306 0.91寸,128*32,使用Adafruit SSD1306驱动库,这是一个跨平台库,支持多个平台多种核心。
(1)TFT-eSPI驱动库导入
Adafruit SSD1306是一个特定驱动库,仅使用于SSD1306屏幕,如果你需要更换其他屏幕驱动可以选用U8G2库驱动。
lib_deps =
adafruit/Adafruit SSD1306 @ 2.5.14
1
2
2
然后我们点击左侧PlatformIO的图标,选择Dependencies,点击Update更新依赖
现在platformio会自动拉取第三方库的git仓库到项目
(3)屏幕代码初始化
在main.cpp中最顶部添加
#include <TFT_eSPI.h>
#include <SPI.h>
TFT_eSPI mylcd = TFT_eSPI();
1
2
3
2
3
在setup()中添加
mylcd.init();
mylcd.setRotation(0);
mylcd.fillScreen(TFT_BLACK);
1
2
3
2
3
2.4.2 WS2812驱动
WS2812这里使用NeoPixel库完成RGB驱动
(1)NeoPixel库导入
lib_deps =
adafruit/Adafruit NeoPixel @ 1.13.0
1
2
2
然后我们重新加载
(2)NeoPixel初始化
在main.cpp中最顶部添加
#include <NTPClient.h>
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "ntp1.aliyun.com", 8 * 3600, 60000);
1
2
3
2
3
2.4.3 USB驱动
USB HID的功能实现通过TinyUSB库完成,TinyUSB可以完成很多USB功能的实现。
(1)TinyUSB库导入
lib_deps =
adafruit/Adafruit TinyUSB Library @ 3.4.5
1
2
2
然后我们重新加载
(2)TinyUSB初始化
在main.cpp中最顶部添加
#include <ArduinoJson.h>
1
2.5 程序开发
现在,你已经完成了所有准备工作,开始编写程序吧。 为方便理解,本项目教程的程序并没有进行性能和安全优化,如果有需求可以自行修改。
2.5.1 库的导入
#include <Arduino.h>
#include <Adafruit_TinyUSB.h>
#include <Adafruit_NeoPixel.h>
#include <Adafruit_SSD1306.h>
#include <Wire.h>
#include <cmath>
1
2
3
4
5
6
2
3
4
5
6
2.5.2 宏定义
在本项目程序中,我们宏定义指定IO引脚
// 按键
#define NUM_KEYS 18
// 旋钮
#define EC11_CLK 11
#define EC11_DT 10
// 屏幕
#define SDA_PIN 8
#define SCL_PIN 9
#define OLED_ADDR 0x3C
Adafruit_SSD1306 display(128, 32, &Wire, -1);
//RGB
#define LED_PIN 27
#define NUM_LEDS 16
#define BTN_RAINBOW 13
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
2.5.3 屏幕部分
屏幕部分没什么要处理的,直接声明下对象就行
//----------------屏幕--------------------------
//声明对象
Adafruit_SSD1306 display(128, 32, &Wire, -1);
1
2
3
2
3
2.5.5 按键
按键部分也没有什么需要处理的,构建4个数组,存储IO引脚、HID码、字符、按压状态就好了,前面硬件设计为了让电路美观流畅,所以在软件这里要按照对应的io去配置,不过在设计时PCB背部是添加了对于按键的IO引脚丝印,按对应内容顺序添加就行。
//----------------按键--------------------------
// 键位对应的IO引脚(PCB背面有标注)
const uint8_t keyPins[NUM_KEYS] = {
12, 13,
7, 6, 5, 4,
20, 21, 22, 26,
3, 2, 1, 0,
16, 17, 18, 19};
// 键位对应的HID码
const uint8_t keyCodes[NUM_KEYS] = {
0X53, 0x2A,
0x5F, 0x60, 0x61, 0x57,
0x5C, 0x5D, 0x5E, 0x56,
0x59, 0x5A, 0x5B, 0x55,
0x62, 0x63, 0x58, 0x54};
// 键位对应的字符
const String keyNum[NUM_KEYS] = {
"AC", "DEL",
"7", "8", "9", "+",
"4", "5", "6", "-",
"1", "2", "3", "x",
".", "0", "=", "/"};
// 键位状态
bool keyPressed[NUM_KEYS] = {false};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2.5.6 USB-HID
HID这里我们直接使用TinyUSB库完成,只需要配置好结构体、报告描述符、声明对象就好了
//----------------USB-HID--------------------------
// 对象声明
Adafruit_USBD_HID usb_hid;
// HID报告描述符
uint8_t const desc_hid_report[] = {
TUD_HID_REPORT_DESC_KEYBOARD(HID_REPORT_ID(1)),
};
// USB HID结构体
typedef struct
{
uint8_t modifier;
uint8_t reserved;
uint8_t keycode[6];
} KeyboardReport;
KeyboardReport keyReport = {0};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2.5.7 RGB灯光
RGB灯光这里使用NeoPixel库驱动,要注意的是PCB设计一共有两组RGB,分别是旋钮右边的独立按键为一组,和其他按键为一组,所以RGB配置上要配置两组,当然你也可以设计上全部合为一组,修改下软件逻辑也没问题。
//----------------RGB灯光--------------------------
// 声明对象
Adafruit_NeoPixel strip(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel strip2(NUM_LEDS, LED_PIN2, NEO_GRB + NEO_KHZ800);
// RGB功能变量
volatile int targetBrightness = 50; // 目标亮度值
int currentBrightness = 0; // 当前实际亮度值
const int SMOOTHING_STEP = 1; // 亮度渐变步长
const unsigned long SMOOTHING_INTERVAL = 10; // 渐变间隔(ms)
int BRIGHTNESS_STEP = 1;
// RGB模式枚举
enum LEDMode
{
MODE_RAINBOW, // 彩虹灯
MODE_MARQUEE, // 跑马灯
};
volatile LEDMode ledMode = MODE_RAINBOW; // 当前灯效模式
unsigned long effectTimer = 0; // 效果计时器
// 跑马灯参数
int marqueePosition = 0;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2.5.8 色轮函数
色轮函数是为了渐变灯效而设计的,通过色轮函数依次遍历所以RGB组合,实现彩虹渐变灯效
- 先将整个255色环被均分为三个区段:
- 红->蓝(0-84) 2. 蓝->绿(85-169) 3. 绿->红(170-255)
- 每个区段通过两个颜色分量的线性变化实现平滑过渡
- 使用255色阶(3*85=255)确保颜色变化连贯无断层
// 彩虹灯效
uint32_t Wheel(byte WheelPos)
{
// 反转输入值,使颜色轮逆向旋转(255为起点,0为终点)
WheelPos = 255 - WheelPos;
/* 第一阶段:红色 -> 蓝色渐变(当WheelPos在0-84范围时)
* 红色分量从255递减至3(255 - 0*3 ~ 255 - 84*3)
* 蓝色分量从0递增至252(0*3 ~ 84*3)
* 绿色分量保持为0 */
if (WheelPos < 85)
return strip.Color(
255 - WheelPos * 3,
0,
WheelPos * 3
);
/* 第二阶段:蓝色 -> 绿色渐变(当WheelPos在85-169范围时)
* 蓝色分量从252递减至3(255 - 0*3 ~ 255 - 84*3)
* 绿色分量从0递增至252(0*3 ~ 84*3)
* 红色分量保持为0 */
if (WheelPos < 170)
{
WheelPos -= 85;
return strip.Color(
0,
WheelPos * 3,
255 - WheelPos * 3
);
}
/* 第三阶段:绿色 -> 红色渐变(当WheelPos在170-255范围时)
* 绿色分量从252递减至3(255 - 0*3 ~ 255 - 84*3)
* 红色分量从0递增至252(0*3 ~ 84*3)
* 蓝色分量保持为0 */
WheelPos -= 170;
return strip.Color(
WheelPos * 3,
255 - WheelPos * 3,
0
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
2.5.9 灯效显示
灯效显示这里预留了两种灯效,分别是彩虹渐变和跑马灯灯效,默认显示彩虹渐变灯效
- void effectRainbow():彩虹渐变灯效
- void effectMarquee():红绿蓝跑马灯灯效
- void runLEDEffect():灯效模式调用函数
void effectRainbow()
{
static uint8_t offset = 0;
for (int i = 0; i < NUM_LEDS; i++)
{
strip.setPixelColor(i, Wheel((i + offset) & 255));
strip2.setPixelColor(marqueePosition, 0xFF0000);
}
strip.show();
strip2.show();
offset++;
delay(50);
}
// 跑马灯效果
void effectMarquee()
{
if (millis() - effectTimer > 200)
{
effectTimer = millis();
strip.clear();
strip.setPixelColor(marqueePosition, 0xFF0000); // 红色光点
strip.setPixelColor((marqueePosition + 4) % NUM_LEDS, 0x00FF00); // 绿色跟随
strip.setPixelColor((marqueePosition + 8) % NUM_LEDS, 0x0000FF); // 蓝色跟随
strip.show();
marqueePosition = (marqueePosition + 1) % NUM_LEDS;
}
}
// 灯效模式调用
void runLEDEffect()
{
switch (ledMode)
{
case MODE_RAINBOW:
effectRainbow();
break;
case MODE_MARQUEE:
effectMarquee();
break;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
2.5.10 EC11旋钮逻辑判断
EC11旋钮逻辑判断
在旋钮逻辑判断函数里,这里对旋钮旋转方向的判断是通过EC11的CLK和DT两条信号线判断,这里我们可以先看一下旋钮旋转时的时序图
把这两个引脚状态合并成二进制来看,也就是说00 -> 01 -> 11 -> 10 -> 00为顺时针旋转,00 -> 10 -> 11 -> 01 -> 00为逆时针旋转,通过两两之间的变化就可以知道EC11旋钮是顺时针旋转还是逆时针旋转了
void handleEncoderRotation()
{
// 静态变量保存前次状态(CLK和DT的组合状态)
static uint8_t lastState = 0; // 存储上一次的引脚组合状态(2位二进制)
static unsigned long lastTime = 0; // 存储上次状态变化的时间戳(防抖用)
const unsigned long debounceDelay = 5; // 消抖时间阈值(单位:毫秒)
// 读取当前编码器状态(将两个引脚状态合并为2位二进制数)
// 格式:高1位是CLK引脚状态,低1位是DT引脚状态
uint8_t state = (digitalRead(EC11_CLK) << 1) | digitalRead(EC11_DT);
// 检测有效状态变化(状态不同且满足消抖时间条件)
if (state != lastState && (millis() - lastTime > debounceDelay))
{
/* 顺时针旋转状态序列检测(EC11编码器四步变化规律):
* 00 -> 01 -> 11 -> 10 -> 00
* 当检测到以下任一有效转换时判定为顺时针旋转:
*/
if ((lastState == 0b00 && state == 0b01) ||
(lastState == 0b01 && state == 0b11) ||
(lastState == 0b11 && state == 0b10) ||
(lastState == 0b10 && state == 0b00))
{
// 顺时针旋转:降低目标亮度(需确保不低于最小值)
targetBrightness = (targetBrightness - BRIGHTNESS_STEP);
}
/* 逆时针旋转状态序列检测(反向四步变化规律):
* 00 -> 10 -> 11 -> 01 -> 00
* 当检测到以下任一有效转换时判定为逆时针旋转:
*/
else if (
(lastState == 0b00 && state == 0b10) ||
(lastState == 0b10 && state == 0b11) ||
(lastState == 0b11 && state == 0b01) ||
(lastState == 0b01 && state == 0b00))
{
// 逆时针旋转:增加目标亮度(需确保不超过最大值)
targetBrightness = (targetBrightness + BRIGHTNESS_STEP);
}
// 更新状态变化时间戳
lastTime = millis();
// 保存当前状态用于下次比较
lastState = state;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
2.6.11 计算机逻辑
计算器这里则是通过按键拼接字符串,然后再解析字符串计算实现的,这里的函数主要是解析字符串
- if (isdigit(c) || c == '.' || (c == '-' && newNum)):数字解析,解析正负数及小数
- else if (!newNum):运算符处理,解析加减乘除
- if (c == '+' || c == '-'):运算符类型判断,确保先乘除后加减
- if (numStr.length() > 0):处理表达式末尾未被运算符触发的剩余数字
float evaluateExpression(String expr)
{
expr.replace(" ", ""); // 移除所有空格,确保表达式紧凑
float result = 0; // 最终计算结果
float currentTerm = 0; // 当前处理的运算项(用于处理乘除优先级)
char op = '+'; // 当前运算符(初始化为+)
String numStr; // 临时存储数字字符串(支持多位数和小数)
bool newNum = true; // 标志位,表示是否开始新数字输入
// 逐字符解析表达式
for (int i = 0; i < expr.length(); i++)
{
char c = expr[i];
/* 数字/符号处理:
* 1. 常规数字:0-9
* 2. 小数点:.
* 3. 合法负号:出现在数字开头或运算符后 */
if (isdigit(c) || c == '.' || (c == '-' && newNum))
{
numStr += c; // 将字符追加到数字缓冲区
newNum = false; // 标记进入数字输入状态
}
// 运算符处理
else if (!newNum)
{
float num = numStr.toFloat(); // 转换缓冲区为浮点数
numStr = ""; // 清空数字缓冲区
/* 根据前一个运算符处理当前项(实现乘除优先级):
* 加减运算符:将当前项加入结果
* 乘除运算符:立即计算当前项 */
switch (op)
{
case '+':
currentTerm = num; // 加法项直接存入当前项
break;
case '-':
currentTerm = -num; // 减法项转为负数存储
break;
case 'x':
currentTerm *= num; // 乘法立即计算
break;
case '/':
if (num == 0)
return NAN; // 除零错误返回特殊值
currentTerm /= num; // 除法立即计算
break;
}
/* 运算符类型判断:
* 遇到加减运算符时将当前项累加到结果
* 遇到乘除运算符时暂存当前项继续运算 */
if (c == '+' || c == '-')
{
result += currentTerm; // 将累计的当前项加入最终结果
currentTerm = 0; // 重置当前项
}
op = c; // 更新当前运算符
newNum = true; // 标记需要开始新数字
}
}
/* 处理最后一个数字项(表达式末尾没有运算符的情况)
if (numStr.length() > 0)
{
float num = numStr.toFloat();
switch (op) // 根据最后的运算符处理
{
case '+':
currentTerm = num;
break;
case '-':
currentTerm = -num;
break;
case 'x':
currentTerm *= num;
break;
case '/':
if (num == 0)
return NAN;
currentTerm /= num;
break;
}
}
result += currentTerm; // 将最后的当前项加入结果
return result; // 返回最终计算结果
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
2.5.12 核心一
初始化
//----------------核心1--------------------------
void setup()
{
// 初始化按键矩阵
for (int i = 0; i < NUM_KEYS; i++)
{
pinMode(keyPins[i], INPUT_PULLUP);
}
// 初始化编码器
pinMode(EC11_CLK, INPUT_PULLUP);
pinMode(EC11_DT, INPUT_PULLUP);
// HID 初始化
Serial1.print("HID INIT");
usb_hid.setBootProtocol(false);
usb_hid.setReportDescriptor(desc_hid_report, sizeof(desc_hid_report));
usb_hid.setPollInterval(2);
usb_hid.begin();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
循环逻辑
//----------------循环--------------------------
void loop()
{
handleEncoderRotation();
if (TinyUSBDevice.mounted())
{
// 处理按键
for (int i = 0; i < NUM_KEYS; i++)
{
if (digitalRead(keyPins[i]) == LOW && !keyPressed[i])
{
if (usb_hid.ready())
{
keyReport.keycode[0] = keyCodes[i];
usb_hid.sendReport(1, &keyReport, sizeof(keyReport));
delay(20);
memset(&keyReport, 0, sizeof(keyReport));
usb_hid.sendReport(1, &keyReport, sizeof(keyReport));
}
keyPressed[i] = true;
}
else if (digitalRead(keyPins[i]) == HIGH)
{
keyPressed[i] = false;
}
}
}
else
{ // 计算器模式
for (int i = 0; i < NUM_KEYS; i++)
{
if (digitalRead(keyPins[i]) == LOW && !keyPressed[i])
{
String key = keyNum[i];
if (key == "=")
{ // 计算结果
if (calcExpression.length() > 0)
{
float result = evaluateExpression(calcExpression);
if (isnan(result))
{
calcDisplay = "Error";
}
else
{
calcDisplay = String(result, 3);
}
needsClear = true;
calcExpression = String(result);
calcDisplayNum = "";
}
}
else if (key == ".")
{ // 处理小数点
if (calcExpression.indexOf('.') == -1)
{
calcExpression += key;
calcDisplayNum += key;
calcDisplay = calcDisplayNum;
}
}
else if (key == "DEL")
{ // 新增删除键处理
if (calcExpression.length() > 0)
{
calcExpression.remove(calcExpression.length() - 1);
calcDisplayNum.remove(calcDisplayNum.length() - 1);
calcDisplay = calcDisplayNum;
if (calcDisplay.length() == 0)
{
calcDisplay = "0";
}
}
else
{
calcDisplay = "0";
}
needsClear = false;
}
else if (key == "+" || key == "-" || key == "x" || key == "/")
{ // 其他有效按键
calcExpression += key;
calcDisplayNum = key;
calcDisplay = calcDisplayNum;
}
else if (key == "AC")
{ // 其他有效按键
calcDisplay = "";
needsClear = false;
calcExpression = "";
calcDisplayNum = "";
calcDisplay = "0";
}
else if (key.length() > 0)
{ // 其他有效按键
if (needsClear)
{
calcDisplay = "";
needsClear = false;
}
calcExpression += key;
calcDisplayNum += key;
calcDisplay = calcDisplayNum;
}
keyPressed[i] = true;
}
else if (digitalRead(keyPins[i]) == HIGH)
{
keyPressed[i] = false;
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
2.5.12 核心一
初始化
//----------------核心2--------------------------
//----------------入口--------------------------
void setup1()
{
Wire.setSDA(SDA_PIN);
Wire.setSCL(SCL_PIN);
Wire.begin();
// OLED 初始化
display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR);
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("EDA-Keypad");
display.setCursor(100, 22);
display.setTextSize(1);
display.println("v1.0");
display.display();
delay(3000);
// LED 初始化
pinMode(BTN_RAINBOW, INPUT_PULLUP);
strip.begin();
strip.setBrightness(currentBrightness);
strip.clear();
strip.show();
strip2.begin();
strip2.setBrightness(currentBrightness);
strip2.clear();
strip2.show();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
循环逻辑
//----------------循环--------------------------
void loop1()
{
static unsigned long lastSmoothUpdate = 0;
// 亮度平滑处理
if (millis() - lastSmoothUpdate >= SMOOTHING_INTERVAL)
{
lastSmoothUpdate = millis();
// 渐进式调整当前亮度
if(targetBrightness>255)targetBrightness=255;
if (currentBrightness != targetBrightness)
{
int direction = (targetBrightness > currentBrightness) ? 1 : -1;
currentBrightness = constrain(currentBrightness + direction * SMOOTHING_STEP, 0, 255);
strip.setBrightness(currentBrightness);
strip2.setBrightness(currentBrightness);
}
}
runLEDEffect();
if (TinyUSBDevice.mounted())//USB通讯成功时
{
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.println("-- KeyBoard Mode --");
display.printf("Brightness: %d%%\n", (currentBrightness * 100) / 255);
if (ledMode == 0)//
{
display.println("Light-Mode: RAINBOW");
}
else
{
display.println("Light-Mode: MARQUEE");
}
display.println("Version: V1.0");
display.display();
}
else//连接设备不支持USB通讯协议时
{
// 计算器显示模式
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.println("-- Calculator Mode --");
display.setTextSize(2);
display.setCursor(0, 16);
// 处理长数字显示
if (calcDisplay.length() > 10 && calcDisplay.length() <= 42)
{
display.setTextSize(1);
display.setCursor(0, 16);
display.println(calcDisplay);
}
else if (calcDisplay.length() <= 10)
{
display.println(calcDisplay);
}
else
{
display.println("Too long...");
needsClear = true;
calcExpression = "";
}
display.display();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
2.6 编译
2.6.1 编译主程序
点击底下一排中的勾,开始构建 如果显示SUCCESS则表示构建成功
2.7 烧录
2.7.1 接线
ESP32S3中板载了TTL芯片并且支持USB烧录,所有不需要再使用串口烧录器烧录 直接连接TypeC接口到电脑,此时在VsCode左下角应该有端口选择按钮,点击选择或使用默认的自动识别都可以。
2.7.2 烧录主程序
方法1:使用官方程序
构建成功后,在.pio/build/rpipico2-riscv文件夹下,应该会有bin文件、uf2文件、elf文件,这都是固件,只是类型不同,RP2350按照boot接入电脑后会以U盘形式存在,将UF2复制到RP2350的U盘模式中即可自动刷写固件,非常方便
方法2:通过编译器
点击底部箭头,等待烧录完成即可