mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
3844 字
10 分钟
科中考核电子琴项目
2025-10-31
2026-07-04
NOTE

没有修改文章内容,添加一处github链接,但以前写的好搓啊

sEvilDragon
/
ELABelectronic-organ
Waiting for api.github.com...
00K
0K
0K
Waiting...

首先,我是新手!
首先,我是新手!!
首先,我是新手!!!
(重要的事情说三遍!)

作为嵌入式新手,我的代码一定存在缺陷和不完美,实现的功能也十分基础,所以别让新人太难办呜呜呜~

本文将重点介绍有关于本次电子琴酱拯救计划的主要实现思路,没错,和论文写的截然不同,论文主要还是按照模板写的,而且为了字数有些语言不通顺,而博客就很随意了,嗯就是主这样~

首先,我需要简单介绍一下ELAB!ELAB是大连理工大学电气创新实践基地,原先为科技中心
故也称为“科中”~

基本信息#

关于本次考核电子琴酱,你最好知道:

  • 我使用了以下原件:ESP32-WROOM-32E-N4
    SS8550
    TTP229-BSF-16
    WS2812B
    TYPE-C 16PIN 2MD(073)
    SSD1306(7线)
  • 使用官方在VScode上的插件ESP-IDF编程,配置为espidfv5.5
  • c语言
  • 操作系统是Windows11 24H2
  • 本文中的AI均指代谷歌gemini
  • 我的全部文件在github上发布:https://github.com/sEvilDragon/ELABelectronic-organ.git
  • 工程结构如下:
├── CMakeLists.txt
├── components
│   └── u8g2
│       ├── inc
│       ├── src
│       ├── CMakeLists.txt
│       └── component.mk
├── main
│   ├── CMakeLists.txt
│   ├── electronic_organ_main.c
│   ├── electronic_organ_main.h
│   ├── led.c
│   ├── music.c
│   ├── TTP229.c
│   ├── word.c
│   ├── oled.c
│   ├── buzzer.c
│   ├── TASK_BUZZER.h
│   ├── TASK_LED.h
│   ├── TASK_OLED.h
│   └── TASK_TTP.h
└── README.md
  • 考核原理图 考核原理图

ESP-IDF的使用#

简单介绍esp-idf插件的使用。

安装方面:我采取的方案是学长提供的先下载espidf对应文件(我下载的是espidfv5.5)然后再在插件的配置页面中进行配置的方案,似乎也可以直接在配置界面中安装。详细的配置过程网上有详细教程,不过多描述。配置过程中存在配置失败的问题,具体情况不一定,有可能是目录错误 (最好不要修改espidf的默认安装路径),还有一种我遇到的很神秘的错误,需要删除两个神秘的文件就可以解决了,这个可以把报错扔给浏览器看看其他人是怎么解决的,AI在这方面似乎不靠谱,反正它没解决我的问题。

简单说一下操作,首先需要创建一个新的esp工程,在espidf的资源管理器(左侧工具栏)中有“新项目向导”点进去,配置一下项目名和项目位置(一般都不用改),使用esp32-custom board,然后choose template,使用template-app新建空项目(其实也就这一种能正常创建)。为了配合实际情况,还需要配置一下一些设置,点击下方状态栏中的SDK配置编辑器,搜索CPU,将160MHz改为240MHz,提高CPU运行频率,其实还有一个要改的,但我忘记是什么了,大概就是警告你4kb只使用2kb,使用的不充分,但我现在找不到了,这个是一条黄色的警告,也不影响使用,想改的话,扔给AI就好了。

下面最左边是git,主要就是几个键记一下,首先构建,烧录,监视,还有一个图标是小伙面的键可以一键执行三个。请注意,烧录之前一定要重新构建,有些问题可以通过esp-log在监视窗口查出(类似于正常c语言使用printf查看代码是否执行或打印对应变量看看值是什么),所以一般点小火苗一键执行就行。有些情况可能会提示需要修复,在VScode上方搜索栏中先输入“>”找一下对应命令即可(一般要用一次esp.py.reconfigure命令)。

C语言基础#

其实没什么很重要的基础,基本上只要简单学过基本语法就行了。

循环,结构体,指针,函数,位运算这类能正常使用就可以。最好会跨文件,函数多的情况下还是分文件比较好,实际上不难,跟着AI都没问题。

Free RTOS#

就是个小型的操作系统。

我们主要用到它的任务函数,队列函数,时钟函数与互斥锁函数。接下来一一说明。

任务函数#

任务函数是使用Free RTOS的主体函数,在app_main函数(其实就是esp特有的main函数)中使用,freertos可以使得一个核心同时进行多个任务(逻辑和正常操作系统是一样的,不详细介绍原理了),大概就是下面的形式

xTaskCreatePinnedToCore(led_animation_task, "ledanimation_task", 2048, NULL, 5, NULL, 1);

简单说明一下,第一个参数是对应的函数名,引号中的是系统中这个任务的名字(一般也就监视里能看见这东西),倒数第一个是核心(通常情况下,和通讯相关的任务会给至核0(PRO_CPU),和应用程序相关的任务分配到核1(APP_CPU),但你会发现我乱用的),倒数第二个是事件句柄传入(这个我没用到,有些任务可能用到,就是在某些情况才会执行任务的话就需要),那个5是事件优先级,大的先执行,所以重要的文件往大的写就好。

队列函数#

这个还真不好介绍,可以简单的理解成freertos的一种处理吧,就是收到一个数据,让这个队列函数来处理一下,实现一些功能,比如我的工程中,读取到的按键信息发送给队列,队列对这个数据进行处理判断(其实就是if那种),在创建动画给LED处理。没错,实际上完全可以让这个处理在按键读取任务中完成,通过全局变量(或是互斥锁)的形式,传递给LED任务,只是freertos提供了另一种更稳定的方案吧算是。

时钟函数#

延时和读取时间任务,任务必须延时,因为多任务执行的核心是在一个任务延时时执行其他任务,所以任务一定要延时。至于读取时间,多数只是为了一些变化或者按键误触功能,比如我的项目中使用的三角函数的变量就是系统时间,按键误触就是读取系统时间保证在一定时间里连续点击算作一次点击,防止系统认为你点了一下判断为好多下。

需要注意的是,esp中时钟相关的一些函数单位是不一样的。 (单位好像可以自定义)

vTaskDelay(pdMS_TO_TICKS(200));//这个单位是1ms
esp_rom_delay_us(100);//这个单位是1微秒
xTaskGetTickCount()//获取系统时间的函数,单位是10ms

互斥锁#

互斥锁就是锁,防止一个变量同时被写入和读取的东西,其实也是无所谓的东西。毕竟一般也很难出现同时被写入和读取的情况发生。

下面简单展示一下我led中的互斥锁函数,大概都是一样的逻辑,但因为变量不同一些传入值和处理也不同,但框架一样。

// 创建一个互斥锁
void led_mutex_start(void)
{
    led_mutex = xSemaphoreCreateMutex();
    if (led_mutex == NULL)
        ESP_LOGE(TAG, "FATAL: Failed to create led_mutex");
}
// 创建一个函数用于写入互斥锁数据
void led_mutex_write(int tag, int value)
{
    if (tag < 0 || tag >= 5)
        return;
    if (xSemaphoreTake(led_mutex, portMAX_DELAY) == pdTRUE)
    {
        max_shine[tag] = value;
        xSemaphoreGive(led_mutex);
    }
}
// 创建一个函数用于读取互斥锁数据
int led_mutex_get(int tag)
{
    int value = 0;
    if (tag < 0 || tag >= 5)
        return value;
    if (xSemaphoreTake(led_mutex, portMAX_DELAY) == pdTRUE)
    {
        value = max_shine[tag];
        xSemaphoreGive(led_mutex);
    }
    return value;
}

具体实现#

LED任务#

我的WS2812使用官方的rmt驱动(也可以根据时序要求自己写,但我不会),这里需要引入新的官方头文件(espressif/led_strip • v3.0.1~1 • ESP Component Registry),下载下来led_shrip文件后将文件放在“C:\Espressif\frameworks\esp-idf-v5.5\components”文件下(如果你安装时没改路径的话),然后重新构建工程即可(就是下面那个小扳手),官方提供的示例就是针对WS2812的,直接套用其初始化程序就好了。

// 灯rmt初始化(官方文档内容)
led_strip_handle_t configure_led(void)
{
    // LED strip general initialization, according to your led board design
    led_strip_config_t strip_config = {
        .strip_gpio_num = LED_STRIP_GPIO_PIN,                        // The GPIO that connected to the LED strip's data line
        .max_leds = LED_STRIP_LED_COUNT,                             // The number of LEDs in the strip,
        .led_model = LED_MODEL_WS2812,                               // LED strip model
        .color_component_format = LED_STRIP_COLOR_COMPONENT_FMT_GRB, // The color order of the strip: GRB
        .flags = {
            .invert_out = false, // don't invert the output signal
        }};
    // LED strip backend configuration: RMT
    led_strip_rmt_config_t rmt_config = {
        .clk_src = RMT_CLK_SRC_DEFAULT,                    // different clock source can lead to different power consumption
        .resolution_hz = LED_STRIP_RMT_RES_HZ,             // RMT counter clock frequency
        .mem_block_symbols = LED_STRIP_MEMORY_BLOCK_WORDS, // the memory block size used by the RMT channel
        .flags = {
            .with_dma = LED_STRIP_USE_DMA, // Using DMA can improve performance when driving more LEDs
        }};
    // LED Strip object handle
    led_strip_handle_t led_strip;
    ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip));
    ESP_LOGI(TAG, "Created LED strip object with RMT backend");
    return led_strip;
}

我的LED动画实现了在按键按下时出现互动灯效(灯像波浪一样散开),这个是AI最初实现的,思路也很简单,大概是这样的: 创建一个结构体储存动画

#define MAX_CONCURRENT_ANIMATIONS 6
typedef struc
{
    int button_bit;        // 触发动画的按键位 (例如 2048, 1024, 16, 32 等)
    TickType_t start_time; // 动画开始的时间戳
    bool is_active;        // 动画是否仍在进行
    int center_led;        // 动画的中心 LED 序号 (0-11)
} animation_instance_t;
extern animation_instance_t g_animations[MAX_CONCURRENT_ANIMATIONS];
//结构体数组,实现了多个任务可以同时进行

队列函数处理接收按键的值

void vQueueProcessTask(void *pvParameters)
{
    int received_bit; // 接收按键位掩码
    while (1)
    {
        // 阻塞等待直到接收到新的按键事件
        if (xQueueReceive(xQueue, &received_bit, portMAX_DELAY))
        {
            int center_led = -1;
            char letter = 0;
            switch (received_bit)
            {
            case 2048://这里的数字对应的是二进制,就是判断哪个键被按下了,1就是按下了
                center_led = 0;
                letter = '7';
                break;
            case 1024:
                center_led = 1;
                letter = '+';
                break;
            ...
            ...
            default:
                ESP_LOGW(TAG, "Received bit %d does not map to a standard LED.", received_bit);
                continue;
            }
            // 2. 创建新的动画实例
            animation_instance_t new_animation = {
                .button_bit = received_bit,
                .start_time = xTaskGetTickCount(),
                .is_active = true,
                .center_led = center_led};
            // 3. 将新动画实例放入管理数组中
            // 找到第一个空闲的槽位(或允许替换最老的槽位)
            for (int i = 0; i < MAX_CONCURRENT_ANIMATIONS; i++)
            {
                if (!g_animations[i].is_active) // 原来是0会变为1就会执行
                {
                    g_animations[i] = new_animation; // 启动新动画
                    break;
                }
            }
        }
        else
            continue;
        vTaskDelay(pdMS_TO_TICKS(5));//这个可有可无,因为前面有阻塞函数
    }
}

动画处理上就是把每个动画数组通过计算得到值,由于我的动画函数还有其他功能,这里展示部分,全部代码在github上有。

// 当前时间下的最大延伸距离 (范围从 0 到 MAX_REACH)
                float current_max_reach = MAX_REACH * ((float)elapsed_ms / ANIMATION_DURATION_MS);
                // 3. 实现延伸和渐淡逻辑
                int center_led = g_animations[i].center_led;
                for (int j = 0; j < LED_STRIP_LED_COUNT; j++)
                {
                    // 计算该像素点离中心的距离 (0, 1, 2, ...)
                    int distance = abs(j - center_led);
                    // 只有在当前最大延伸范围内才计算颜色
                    if (distance <= current_max_reach)
                    {
                        // 距离衰减率 (离中心越近越亮,从 1.0 递减到 0.0)
                        float dist_falloff = 1.0f - ((float)distance / current_max_reach);
                        // 最终强度 = (时间衰减) * (距离衰减)
                        float final_intensity = time_decay_factor * dist_falloff;
                        // 累加到最终颜色数组 (使用 uint16_t 防止溢出)
                        final_r[j] += (uint16_t)(BASE_R * final_intensity);
                        final_g[j] += (uint16_t)(BASE_G * final_intensity);
                        final_b[j] += (uint16_t)(BASE_B * final_intensity);
                    }
                }

用三角函数实现灯色随时间变化(也就是RGB灯效):

final_b[i] = (uint16_t)((25 * sin((xTaskGetTickCount() / speed + i)) + 35) * set_led);

speed是我的一个变量,这里是控制变化速度用的。 RGB实现上也可以用数组,那个似乎更稳定,但我选择了更简单的三角函数进行处理。实际上由于三角函数最后取整的问题,灯的变化很有限,效果不会那么好。

TTP229电容触摸板#

提供参考连接https://zhuanlan.zhihu.com/p/502348392

别忘了初始化:

void ttp_start(void)
{
    ESP_LOGE(TAG, "开始初始化");
    // 初始化GPIO
    gpio_config_t io_conf;
    // 触摸芯片的SCL
    io_conf.intr_type = GPIO_INTR_DISABLE;           // 禁止中断
    io_conf.mode = GPIO_MODE_OUTPUT;                 // 输出模式
    io_conf.pin_bit_mask = (1ULL << TTP229_PIN_SCL); // 选择引脚
    io_conf.pull_down_en = 0;                        // 禁止下拉
    io_conf.pull_up_en = 0;                          // 禁止上拉
    gpio_config(&io_conf);                           // 应用配置
    // 触摸芯片的SDO
    io_conf.intr_type = GPIO_INTR_DISABLE;           // 禁止中断
    io_conf.mode = GPIO_MODE_INPUT;                  // 输入模式
    io_conf.pin_bit_mask = (1ULL << TTP229_PIN_SDO); // 选择引脚
    /* 防止 SDO 悬空导致默认高电平,启用下拉(若你的模块空闲为高,请改为 pull_up_en = 1) */
    //实际情况是我把四种组合都试了一遍,该有的问题仍没有解决,下面详细说一下。
    io_conf.pull_down_en = 0;
    io_conf.pull_up_en = 0;
    gpio_config(&io_conf); // 应用配置
}

相关的内容也可以参考官方文档,这个一定要对好接口不同的接口状态其实际编写也不同。提供的文档使用的是stm32按照高电平有效进行的书写,但比如我的项目使用的就是低电平有效的情况,下面我提供一下我的模拟代码(低电平有效),实际上写反了也没啥事,因为电压转变和稳定是很快的,不延时一般也不会初始,更何况像我延时了100ms呢。

unsigned short vReadttp229Task(void)
{
    unsigned short i;
    uint16_t local_dat = 0;
    /* 发起读序列:把 SDA 作为输出并产生启动脉冲,然后切回输入读取 */
    SDA_OUT();
    gpio_set_level(TTP229_PIN_SDO, 0);
    esp_rom_delay_us(100);
    gpio_set_level(TTP229_PIN_SDO, 1);
    esp_rom_delay_us(100);
    SDA_IN();
    for (i = 0; i < NUM_TOUCH; i++)
    {
      gpio_set_level(TTP229_PIN_SCL, 0);
        esp_rom_delay_us(100);
        gpio_set_level(TTP229_PIN_SCL, 1);
        esp_rom_delay_us(100);
        if (!gpio_get_level(TTP229_PIN_SDO))
        {
            local_dat |= (1 << i);
        }
        esp_rom_delay_us(50);
    }
    return local_dat;
}

TTP229是自己神秘的通讯协议,所以没用硬件支持,只能软件模拟通讯。然后就是说,TTP229这个我也不明白,反正有时候会串键,我是连电源时会串键,连充电宝就没事了。其他问题优先考虑硬件问题吧我不明白。而且我的ttp229高低有效会反转(有时高电平有效,有时低电平有效,我也不知道为啥),我简单加了一个判断,让它根据上电那一刻是高电平还是低电平来确定怎么读取。

蜂鸣器(buzzer)#

最简单的部分,我不懂DAC,所以用的PWM。 PWM频率对应发声的频率,PWM占空比对应声音大小(有些设备对占空比变化不明显,比如我的,调很小才能听出区别,同时注意,占空比为50%时最大) PWM初始化和发声函数:

void timer(void)
{
    ledc_timer_config_t pwm_timer = {
        .duty_resolution = LEDC_TIMER_12_BIT, // 占空比分辨率为12位二进制数,4095max
        .freq_hz = PWM_FREQ,
        .speed_mode = LEDC_HIGH_SPEED_MODE,
        .timer_num = LEDC_TIMER};  // esp32有0~7高速通道,尽量选择0~7通道
    ledc_timer_config(&pwm_timer); // 调用配置函数
    ledc_channel_config_t pwm_config = {
        .channel = LEDC_CHANNEL,
        .duty = 0,
        .gpio_num = BUZZER_GPIO,
        .speed_mode = LEDC_HIGH_SPEED_MODE,
        .timer_sel = LEDC_TIMER};
    ledc_channel_config(&pwm_config);
}
// 发声函数
void buzzer_set_tone(int freq_hz)
{
    if (freq_hz > 0)
    {
        // 设置新的频率
        ledc_set_freq(LEDC_HIGH_SPEED_MODE, LEDC_TIMER, freq_hz);
        ledc_set_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL, buzzer_mutex_get());
        ledc_update_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL);
    }
    else
    {
        // 停止声音:将占空比设置为 0
        ledc_set_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL, 0);
        ledc_update_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL);
    }
}

后面根据按键发声就很简单了,我的实现是buzzertask每次循环检测按键读取值进行对应发声。也就是说,我的蜂鸣器和led函数是分开的,这很重要,为下面的音乐功能提供了方便。

OLED(SSD1306)#

这个可有的说了,首先,虽然它是i2c和spi共用线的东西,但网上几乎都是i2c,就连它自己的手册都只是着重介绍了i2c,但我们要求用的spi。一些ssd1306的特性,可以参考官方文档和网上的各类介绍。

屏幕激活过程中,使用了3种SPI激活方式,接下来一一介绍

ESP自带SPI硬件#

使用esp32自身的spi硬件是最好的方式,因为硬件的延时很低,软件模拟的很难做到。 这个初始化跟着AI写一便就好了,不难,AI一遍过,只需要简单改一下参数 (没有初始化函数,因为我找不到了,但跟AI说一下你要用spi初始化七线ssd1306就好了)

// 发送
void oled_send_cmd(uint8_t cmd)
{
    spi_transaction_t t;
    memset(&t, 0, sizeof(t));
    t.length = 8; // 8位数据
    t.tx_buffer = &cmd;
    t.user = (void *)(0); // DC 引脚拉低 (命令)
    spi_device_transmit(spi, &t);
}
// 发送数据
void oled_send_data(uint8_t data[], size_t length)
{ // 这里的length是字节长度
    spi_transaction_t t;
    memset(&t, 0, sizeof(t));
    t.length = 8 * length;
    t.tx_buffer = data;
    t.user = (void *)(1);
    spi_device_transmit(spi, &t);
}
// 定位
void location(int page, int y)
{
    int m = 176 + page;
    oled_send_cmd(m);
    int under = y & 0b00001111;
    oled_send_cmd(under);
    int up = y & 0b11110000;
    up = up >> 4;
    up += 16;
    oled_send_cmd(up);
}
// OLED清屏(去除雪花)这里只写了清除一页的来做测试
void OLED_clear(unsigned int page)
{
    location(page, 0);
    uint8_t data[128] = {0};
    oled_send_data(data, 128);
}
void oled_spi_pre_transfer_callback(spi_transaction_t *t)
{
    // t->user 存储了是命令还是数据的标识符
    int dc_level = (int)t->user;
    gpio_set_level(OLED_DC, dc_level);
}
void oled_init(void)
{
    oled_send_cmd(0xAE); // 说是一开始要关,为了安全
    // 设置时针用的,这里套用建议的平均值
    oled_send_cmd(0xD5);
    oled_send_cmd(0x80);
    // 这个好像是用来确定多少行的
    oled_send_cmd(0xA8);
    oled_send_cmd(0x3F);
    // 让屏幕像素点可以亮
    oled_send_cmd(0x8D);
    oled_send_cmd(0x14);
    // 就是设置亮度的好像,也是套用常用值
    oled_send_cmd(0x81);
    oled_send_cmd(0xCF);
    // 这个好像就是预加载,提高显示效果的
    oled_send_cmd(0xD9);
    oled_send_cmd(0xF1);
    // 设置电压的,也是用了推荐的,说是防止鬼影
    oled_send_cmd(0xDB);
    oled_send_cmd(0x20);
    // 启用页地址,我想实现的功能需要这个
    oled_send_cmd(0x20);
    oled_send_cmd(0x02);
    // 这个就是调整用的
    oled_send_cmd(0xA1); // (如果图像是镜像的,使用A0h)
    // oled_send_cmd(0xC8); // (如果上下颠倒,使用C0h)
    //  显示自己想让他实现的功能
    oled_send_cmd(0xA4);
    oled_send_cmd(0xAF); // 配置完了再打开屏幕
    OLED_clear(0);
    OLED_clear(1);
    OLED_clear(2);
    OLED_clear(3);
    OLED_clear(4);
    OLED_clear(5);
    OLED_clear(6);
    OLED_clear(7);
    OLED_word();
}

软件模拟#

这个最好能掌握,和上面TTP229的通讯一样,根据时序模拟SPI通讯,SSD1306的SPI是常规SPI

下面展示一下: (特别说明一下,其实不延时也没事,原因和上面一样,电压转换和稳定是一个瞬间的事情)

模拟实现
void oled_init();//就是上面那个代码块的那个函数
void OLED_start(void)
{
    gpio_set_direction(OLED_SCL, GPIO_MODE_OUTPUT);
    gpio_set_direction(OLED_SDA, GPIO_MODE_OUTPUT);
    gpio_set_direction(OLED_CS, GPIO_MODE_OUTPUT);
    gpio_set_direction(OLED_DC, GPIO_MODE_OUTPUT);
    gpio_set_direction(OLED_RES, GPIO_MODE_OUTPUT);
    gpio_set_level(OLED_RES, 0);    // 拉低 RES
    vTaskDelay(pdMS_TO_TICKS(10));  // 延迟一段时间
    gpio_set_level(OLED_RES, 1);    // 释放 RES
    vTaskDelay(pdMS_TO_TICKS(100)); // 再次等待设备启动
    oled_init();
}
void oled_send_cmd(uint8_t cmd)
{
    gpio_set_level(OLED_CS, 0);
    gpio_set_level(OLED_DC, 0);
    gpio_set_level(OLED_SCL, 0);
    esp_rom_delay_us(1);
    for (int i = 0; i < 8; i++)
    {
        uint8_t bit = (cmd >> (7 - i)) & 0x01;
        gpio_set_level(OLED_SDA, bit);
        esp_rom_delay_us(1);
        gpio_set_level(OLED_SCL, 1);
        esp_rom_delay_us(1);
        gpio_set_level(OLED_SCL, 0);
        esp_rom_delay_us(1);
    }
    gpio_set_level(OLED_SDA, 0);
    gpio_set_level(OLED_CS, 1);
}
void oled_send_data(uint8_t data[], size_t length)
{
    gpio_set_level(OLED_CS, 0);
    gpio_set_level(OLED_DC, 1);
    gpio_set_level(OLED_SCL, 0);
    esp_rom_delay_us(1);
    for (int i = 0; i < length; i++)
    {
        for (int j = 0; j < 8; j++)
        {
            uint8_t bit = ((data[i]) >> (7 - j)) & 0x01;
            gpio_set_level(OLED_SDA, bit);
            esp_rom_delay_us(1);
            gpio_set_level(OLED_SCL, 1);
            esp_rom_delay_us(1);
            gpio_set_level(OLED_SCL, 0);
            esp_rom_delay_us(1);
        }
    }
}

u8g2#

不难,但这很重要,从上面就能看出,我在这个项目中引入了u8g2。

u8g2内置了软件激活ssd1306的方案,难的是怎么引入u8g2,这里我们使用的是对单个文件引入u8g2,这里提供教程来源<基于esp-idf移植使用u8g2 - 知乎>

好好跟着教程写cmake文件,别问AI,AI提供的cmake有问题(可能是版本吧),一定是跑不起来的! 一定要禁用硬件SPI,硬件SPI会强制接管IO口,让软件SPI起不来作用!! 一定要注意命名和文件格式!!

实际上我写的和教程有些许区别,可以参考一下(如果和我一样教程不行的话)

idf_component_register(SRCS "src/mui.c"
                           "src/mui_u8g2.c"
                           "src/u8g2_bitmap.c"
                           "src/u8g2_box.c"
                           "src/u8g2_buffer.c"
                           "src/u8g2_button.c"
                           "src/u8g2_circle.c"
                           "src/u8g2_cleardisplay.c"
                           "src/u8g2_d_memory.c"
                           "src/u8g2_d_setup.c"
                           "src/u8g2_font.c"
                           "src/u8g2_fonts.c"
                           "src/u8g2_hvline.c"
                           "src/u8g2_input_value.c"
                           "src/u8g2_intersection.c"
                           "src/u8g2_kerning.c"
                           "src/u8g2_line.c"
                           "src/u8g2_ll_hvline.c"
                           "src/u8g2_message.c"
                           "src/u8g2_polygon.c"
                           "src/u8g2_selection_list.c"
                           "src/u8g2_setup.c"
                           "src/u8log.c"
                           "src/u8log_u8g2.c"
                           "src/u8log_u8x8.c"
                           "src/u8x8_8x8.c"
                           "src/u8x8_byte.c"
                           "src/u8x8_cad.c"
                           "src/u8x8_capture.c"
                           "src/u8x8_debounce.c"
                           "src/u8x8_display.c"
                           "src/u8x8_d_ssd1306_128x64_noname.c"
                           "src/u8x8_fonts.c"
                           "src/u8x8_gpio.c"
                           "src/u8x8_input_value.c"
                           "src/u8x8_message.c"
                           "src/u8x8_selection_list.c"
                           "src/u8x8_setup.c"
                           "src/u8x8_string.c"
                           "src/u8x8_u16toa.c"
                           "src/u8x8_u8toa.c"
                            INCLUDE_DIRS "inc"
                            REQUIRES "driver")

u8g2激活SPI很简单,是一套标准流程,这里还是展示一下:

uint8_t u8g2_gpio_and_delay_cb(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
    switch (msg)
    {
    case U8X8_MSG_GPIO_AND_DELAY_INIT:
        break;
    case U8X8_MSG_GPIO_SPI_CLOCK:
        gpio_set_level(OLED_SCL, arg_int);
        break;
    case U8X8_MSG_GPIO_SPI_DATA:
        gpio_set_level(OLED_SDA, arg_int);
        break;
    case U8X8_MSG_GPIO_CS:
        gpio_set_level(OLED_CS, arg_int);
        break;
    case U8X8_MSG_GPIO_DC:
        gpio_set_level(OLED_DC, arg_int);
        break;
    case U8X8_MSG_GPIO_RESET:
        gpio_set_level(OLED_RES, arg_int);
        break;
    case U8X8_MSG_DELAY_MILLI:
        vTaskDelay(pdMS_TO_TICKS(arg_int));
        break;
    default:
        return 0;
    }
    return 1;
}
void u8g2_app_init(void)
{
    u8g2_Setup_ssd1306_128x64_noname_f(&u8g2,
                                       U8G2_R0,
                                       u8x8_byte_4wire_sw_spi, // <-- 使用 U8G2 内置的软件 SPI 驱动
                                       u8g2_gpio_and_delay_cb);
    u8g2_InitDisplay(&u8g2);
    u8g2_SetPowerSave(&u8g2, 0);
    u8g2_ClearBuffer(&u8g2);
    u8g2_SendBuffer(&u8g2);
}
void oled_spi_start(void)
{
    ESP_LOGI(TAG, "初始化 OLED GPIO 和复位");
    // 初始化所有需要的 GPIO(包括 SCL, SDA, CS, DC, RES)
    gpio_config_t io_conf = {};
    io_conf.intr_type = GPIO_INTR_DISABLE;
    io_conf.mode = GPIO_MODE_OUTPUT;
    // 确保这里包含了所有 SPI 信号线,因为它们现在是普通 GPIO
    io_conf.pin_bit_mask = (1ULL << OLED_SCL) | (1ULL << OLED_SDA) |
                           (1ULL << OLED_CS) | (1ULL << OLED_DC) | (1ULL << OLED_RES);
    io_conf.pull_down_en = 0;
    io_conf.pull_up_en = 0;
    gpio_config(&io_conf);
    // 执行硬件复位操作
    gpio_set_level(OLED_RES, 0);
    vTaskDelay(pdMS_TO_TICKS(10));
    gpio_set_level(OLED_RES, 1);
    vTaskDelay(pdMS_TO_TICKS(100));
    u8g2_app_init();
    OLED_menu();
}

展示屏幕的一个页码代码,其他基本是一样的

void OLED_menu(void)
{
  char display_buffer[64]; // 临时缓冲区
    int array_index = 1;     // 想要打印的数组索引
    u8g2_ClearBuffer(&u8g2);
    u8g2_SetFont(&u8g2, u8g2_font_helvR08_tf);
    u8g2_DrawUTF8(&u8g2, 50, 8, "Ciallo~");
    u8g2_SetFont(&u8g2, u8g2_font_helvR14_tf);
    array_index = 0;
    snprintf(display_buffer, sizeof(display_buffer),
             "%c", letter[array_index]);
    u8g2_DrawUTF8(&u8g2, 10, 40, display_buffer);
    array_index = 1;
    snprintf(display_buffer, sizeof(display_buffer),
             "%c", letter[array_index]);
    u8g2_DrawUTF8(&u8g2, 32, 40, display_buffer);
    u8g2_SetFont(&u8g2, u8g2_font_helvR24_tf);
    array_index = 2;
    snprintf(display_buffer, sizeof(display_buffer),
             "%c", letter[array_index]);
    u8g2_DrawUTF8(&u8g2, 55, 40, display_buffer);
    u8g2_SetFont(&u8g2, u8g2_font_unifont_t_chinese3);
    u8g2_DrawUTF8(&u8g2, 1, 62, "设定B");
    u8g2_DrawUTF8(&u8g2, 85, 62, "菜单L");
    u8g2_SendBuffer(&u8g2);
}

展示这个代码主要是因为这里面刚刚好展示了一个重要的点,首先,写入中文一定要用DrawUTF8,其次,想输出变量,一定要建立缓冲区

如果不使用u8g2的话要注意ssd1306是页地址寻址! 详见官方文档

u8g2支持你定位在屏幕外面(虽然写不上去),利用这个特性,可以实现屏幕文字滚动效果

void OLED_scroll_music(void)
{
    u8g2_SetFont(&u8g2, u8g2_font_unifont_t_chinese3);
    const char *text_to_scroll = "  正在播放<春日影> ";
    u8g2_uint_t text_width = u8g2_GetUTF8Width(&u8g2, text_to_scroll);
    u8g2_DrawUTF8(&u8g2, x, 40, text_to_scroll);
    u8g2_SendBuffer(&u8g2);
    x -= 2;
    if (x <= -text_width)
    {
        x = 128;
    }
}

u8g2支持丰富的第三方库,只是我没用~ 比如说u8g2自带中文库真的很菜,有第三方中文库,甚至字体都不止一种,也有ui,动画库甚至是游戏库,不再过多论述。

music#

展示音乐函数:

void musictask(int message, int continue_time, int stop_time)
{
    if (check_oled == 0)
        return;
    ttp_mutex_write(~message);
    message = (message & 0b1111111100001111);
    if (xQueueSend(xQueue, &message, pdMS_TO_TICKS(100)) != pdPASS)
    {
        ESP_LOGW(TAG, "Queue full, message not sent");
    }
    uint16_t mew_dat = 0;
    uint16_t datt;
    while (continue_time)
    {
        if (check_oled == 0)
            return;
        if (!panduan == 0)
        {
            mew_dat = vReadttp229Task();
        }
        else
        {
            datt = (~vReadttp229Task());
            mew_dat = (datt);
        }
        if (((~mew_dat) & 65503) == 65503)
        {
            vTaskDelay(pdMS_TO_TICKS(50));
            mew_dat = (~mew_dat);
        }
        if (((~mew_dat) & 32) == 32)
        {
            vTaskDelay(pdMS_TO_TICKS(50));
            return;
        }
        if (check_oled == 8 || ((~mew_dat) & 128) == 128)
        {
            check_oled = 8;
            while (1)
            {
                vTaskDelay(pdMS_TO_TICKS(200));
                if (!panduan == 0)
                {
                    mew_dat = vReadttp229Task();
                }
                else
                {
                    datt = (~vReadttp229Task());
                    mew_dat = (datt);
                }
                if ((((~mew_dat) & 16) == 16))
                {
                    check_led++;
                    if (check_led == 5)
                        check_led = 0;
                }
                if ((((~mew_dat) & 64) == 64))
                {
                    if (speedm == 20)
                        speedm = 40;
                    else
                      speedm = 20;
                }
                if (((~mew_dat) & 128) == 128)
                {
                    vTaskDelay(pdMS_TO_TICKS(150));
                    check_oled = 7;
                    break;
                }
                if (((~mew_dat) & 32) == 32)
                {
                    vTaskDelay(pdMS_TO_TICKS(150));
                    ttp_mutex_write(65535);
                    check_oled = 0;
                    break;
                }
            }
        }
        if (check_oled == 7)
        {
            continue_time -= speedm;
            if (continue_time <= 0)
                break;
        }
        vTaskDelay(pdMS_TO_TICKS(20));
    }
    ttp_mutex_write(65535);
    vTaskDelay(pdMS_TO_TICKS(stop_time));
}

简单来说,我的音乐函数是通过模拟传入按键实现的,然后将按键传给队列实现led对应发光,修改互斥锁的按键值,实现蜂鸣器对应发声。所以需要注意,上面的两点决定了music函数不能放在led和蜂鸣器任务中,不然对应函数的功能是不会执行的,因为music函数还没走完。

由于传入的是音符,有多少个音就有多少个上述的函数执行,所以快进只要return函数就好了,开头检测是否结束了音乐实现关闭功能,也就是说,虽然看起来音乐函数结束了,实际上并没有,只是接下来的函数刚开始就return了看起来像没有执行一样,暂停就是进入死循环而已。

由于蜂鸣器和led是分开激活的特性,可以将该音乐函数改写成一个简单的音游,只需要让蜂鸣器发声只发生在对应按键被按下时就行了,不多描述。

感谢

以上就是电子琴的全部,这是我第一次接触嵌入式,肯定有些地方做的不够到位,希望我能在后续的学习过程中不断专精。感谢你能观看这篇博客!
感谢所有学长提供的帮助!
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

科中考核电子琴项目
https://mizuki.mysqil.com/posts/科中考核电子琴项目/
作者
sEvil_Dragon
发布于
2025-10-31
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录