LVGL多显示-基于GC9A01A的1.28寸圆屏

原创 嵌入式Lee 2024-07-10 11:56

一. 前言

前面系列文章我们分享了ST7789GC9A01A的驱动,并移植了LVGLemWin跑了DemoLVGL本身支持多显示器,刚好手里也有多块基于GC9A01A1.28寸圆屏,这一篇我们就在之前的基础上演示LVGL的多屏显示。

二. 驱动修改

前面我们分享LVGL移植时可以看到,显示驱动移植就是根据模板实现用户自己的显示函数即可,然后注册对应的驱动。对于显示器驱动可能用的一套底层驱动使用id区分不同显示器,也可能是完全不同的驱动,不管是什么方式,对于LVGL只需要注册多个驱动即可,一个驱动对应一个显示器。

我们的1.28寸屏使用SPI驱动,为了减少引脚使用所以两个屏幕共用DICLKDC三个引脚,CSRST两个屏幕独立使用,所以一共7个引脚,采用分时刷屏方式。所以驱动也只需要在原来的基础上稍微修改,接口中增加一个ID参数用于区分显示器即可。

gc9a01a_itf.h/c相应的接口中,增加ID参数用于区分显示屏,相应的增加gc9a01a设备实例,和CSRST等控制接口区分,修改后内容如下

gc9a01a_itf.h

#ifndef GC9A01A_ITF_H#define GC9A01A_ITF_H
#ifdef __cplusplus extern "C"{#endif
#include
#define GC9A01A_ITF_SPI_IO 0 /**< 配置为1使用IO模拟, 配置为0使用硬件SPI */
#define GC9A01A_HSIZE 240#define GC9A01A_VSIZE 240
/** * \fn gc9a01a_itf_init * 初始化 * \retval 0 成功 * \retval 其他值 失败*/int gc9a01a_itf_init(void);
/** * \fn gc9a01a_itf_deinit * 解除初始化 * \retval 0 成功 * \retval 其他值 失败*/int gc9a01a_itf_deinit(void);
/** * \fn gc9a01a_itf_sync * 刷新显示 * \param[in] id 设备索引 * \retval 0 成功 * \retval 其他值 失败*/int gc9a01a_itf_sync(int id);
/** * \fn gc9a01a_itf_set_pixel * 写点 * \param[in] id 设备索引 * \param[in] x x坐标位置 * \param[in] y y坐标位置 * \param[in] rgb565 颜色*/void gc9a01a_itf_set_pixel(int id, uint16_t x, uint16_t y, uint16_t rgb565);
/** * \fn gc9a01a_itf_get_pixel * 读点 * \param[in] id 设备索引 * \param[in] x x坐标位置 * \param[in] y y坐标位置 * \return rgb565颜色*/uint16_t gc9a01a_itf_get_pixel(int id, uint16_t x, uint16_t y);
#ifdef __cplusplus }#endif
#endif

gc9a01a_itf.c

#include #include "gc9a01a_itf.h"#include "gc9a01a.h"
#define USE_SPI_PORT SPI_PORT1 #define DC_PIN GPIO_20 #define SCL_PIN GPIO_29 #define SDA_PIN GPIO_39
#define CS1_PIN GPIO_30 #define CS2_PIN GPIO_38 #define RST1_PIN GPIO_31#define RST2_PIN GPIO_27
/****************************************************************************** * 以下是底层适配 * ******************************************************************************/
#if GC9A01A_ITF_SPI_IO/* 使用IO模拟SPI方式 */ #include "io_spi.h"
static void port_io_spi_cs_write(uint8_t val){ (void)val;}
static void port_gc9a01a_spi_enable1(uint8_t val){ if(val) { gpio_write(CS1_PIN,0); } else { gpio_write(CS1_PIN,1); }}
static void port_gc9a01a_spi_enable2(uint8_t val){ if(val) { gpio_write(CS2_PIN,0); } else { gpio_write(CS2_PIN,1); }}
static void port_io_spi_sck_write(uint8_t val){ gpio_write(SCL_PIN,val);}
static void port_io_spi_mosi_write(uint8_t val){ gpio_write(SDA_PIN,val);}
static uint8_t port_io_spi_miso_read(void){ return 0;}
static void port_io_spi_init(void){ gpio_open(CS1_PIN, GPIO_DIRECTION_OUTPUT); gpio_set_pull_mode(CS1_PIN, GPIO_PULL_UP); gpio_write(CS1_PIN,1);
gpio_open(CS2_PIN, GPIO_DIRECTION_OUTPUT); gpio_set_pull_mode(CS2_PIN, GPIO_PULL_UP); gpio_write(CS2_PIN,1);
gpio_open(SCL_PIN, GPIO_DIRECTION_OUTPUT); gpio_set_pull_mode(SCL_PIN, GPIO_PULL_UP); gpio_write(SCL_PIN,1); gpio_open(SDA_PIN, GPIO_DIRECTION_OUTPUT); gpio_set_pull_mode(SDA_PIN, GPIO_PULL_UP); gpio_write(SDA_PIN,1); if(RST1_PIN < GPIO_INVALID) { gpio_open(RST1_PIN, GPIO_DIRECTION_OUTPUT); gpio_set_pull_mode(RST1_PIN, GPIO_PULL_UP); gpio_write(RST1_PIN,1); } if(RST2_PIN < GPIO_INVALID) { gpio_open(RST2_PIN, GPIO_DIRECTION_OUTPUT); gpio_set_pull_mode(RST2_PIN, GPIO_PULL_UP); gpio_write(RST2_PIN,1); }}
static void port_io_spi_deinit(void){ gpio_close(CS1_PIN); gpio_close(CS2_PIN); gpio_close(SCL_PIN); gpio_close(SDA_PIN); }
/* IO模拟SPI设备实例 */static io_spi_dev_st s_io_spi_dev ={ .cs_write = port_io_spi_cs_write, .sck_write = port_io_spi_sck_write, .mosi_write = port_io_spi_mosi_write, .miso_read = port_io_spi_miso_read, .delay_pf = 0, .init = port_io_spi_init, .deinit = port_io_spi_deinit, .delayns = 1, .mode = 0, .msb = 1,};
static void port_gc9a01a_set_dcx(uint8_t val){ gpio_write(DC_PIN, val);}
static void port_gc9a01a_set_reset1(uint8_t val){ (void)val; if(RST1_PIN < GPIO_INVALID) { gpio_write(RST1_PIN, val); }}
static void port_gc9a01a_set_reset2(uint8_t val){ (void)val; if(RST2_PIN < GPIO_INVALID) { gpio_write(RST2_PIN, val); }}
static void port_gc9a01a_spi_write(uint8_t* buffer, uint32_t len){ io_spi_trans(&s_io_spi_dev, buffer, 0, len);}
static void port_gc9a01a_delay_ms(uint32_t t){ if(t > 0) { os_delay(t); }}
static void port_gc9a01a_init(void){ gpio_open(DC_PIN, GPIO_DIRECTION_OUTPUT); gpio_set_pull_mode(DC_PIN, GPIO_PULL_UP); gpio_write(DC_PIN,1);
io_spi_init(&s_io_spi_dev);}
static void port_gc9a01a_deinit(void){ gpio_close(DC_PIN);
io_spi_deinit(&s_io_spi_dev);}
#else/* 使用硬件SPI方式 */#include "spi.h"
static void port_gc9a01a_set_dcx(uint8_t val){ gpio_write(DC_PIN, val);}
static void port_gc9a01a_set_reset1(uint8_t val){ (void)val; if(RST1_PIN < GPIO_INVALID) { gpio_write(RST1_PIN, val); }}
static void port_gc9a01a_set_reset2(uint8_t val){ (void)val; if(RST2_PIN < GPIO_INVALID) { gpio_write(RST2_PIN, val); }}
volatile uint32_t s_gc9a01a_spi_busy_flag = 0;
static void spi_dma_cb(void){ //printf("spi done\r\n"); s_gc9a01a_spi_busy_flag = 0; //timer_delay_us(2);}
static void port_gc9a01a_spi_write(uint8_t* buffer, uint32_t len){ RET ret; int timeout; if(len < 100) { timeout = 2; /* 数据比较少时,是在发命令,最多等2ms */ } else { timeout = 200; /* 数据比较多时肯定是在刷屏,最多等200mS */ } s_gc9a01a_spi_busy_flag = 1; if(RET_OK != (ret = spi_dma_trans_direct(USE_SPI_PORT, buffer, 0, len, spi_dma_cb))) { printf("spi err %d\r\n", ret); s_gc9a01a_spi_busy_flag = 0; return; } /* 等待传输完成,由于一个SPI多CS分时,所以必须阻塞等一次传完,才能进行下一次 */ while(s_gc9a01a_spi_busy_flag != 0) { os_delay(1); /* 释放CPU */ timeout--; if(timeout <= 0) { s_gc9a01a_spi_busy_flag = 0; printf("spi busy\r\n"); return; } } }
static void port_gc9a01a_spi_enable1(uint8_t val){ if(val) { gpio_write(CS1_PIN,0); } else { gpio_write(CS1_PIN,1); }}
static void port_gc9a01a_spi_enable2(uint8_t val){ if(val) { gpio_write(CS2_PIN,0); } else { gpio_write(CS2_PIN,1); }}
static void port_gc9a01a_delay_ms(uint32_t t){ os_delay(t);}
static void port_gc9a01a_init(void){ static int s_port_gc9a01a_init_flag = 0; if(s_port_gc9a01a_init_flag == 0) { /* 共用,底层接口只需要初始化一次 */ s_port_gc9a01a_init_flag = 1; gpio_open(DC_PIN, GPIO_DIRECTION_OUTPUT); gpio_set_pull_mode(DC_PIN, GPIO_PULL_UP); gpio_write(DC_PIN,1);
gpio_open(GPIO_PMM00, GPIO_DIRECTION_OUTPUT); gpio_set_pull_mode(GPIO_PMM00, GPIO_PULL_UP); gpio_write(GPIO_PMM00,1); gpio_open(GPIO_PMM01, GPIO_DIRECTION_OUTPUT); gpio_set_pull_mode(GPIO_PMM01, GPIO_PULL_UP); gpio_write(GPIO_PMM01,1);
gpio_open(CS1_PIN, GPIO_DIRECTION_OUTPUT); gpio_set_pull_mode(CS1_PIN, GPIO_PULL_UP); gpio_write(CS1_PIN,1);
gpio_open(CS2_PIN, GPIO_DIRECTION_OUTPUT); gpio_set_pull_mode(CS2_PIN, GPIO_PULL_UP); gpio_write(CS2_PIN,1);
if(RST1_PIN < GPIO_INVALID) { gpio_open(RST1_PIN, GPIO_DIRECTION_OUTPUT); gpio_set_pull_mode(RST1_PIN, GPIO_PULL_UP); gpio_write(RST1_PIN,1); } if(RST2_PIN < GPIO_INVALID) { gpio_open(RST2_PIN, GPIO_DIRECTION_OUTPUT); gpio_set_pull_mode(RST2_PIN, GPIO_PULL_UP); gpio_write(RST2_PIN,1); } printf("io init\r\n"); /* SPI配置 */ spi_cfg_t spi_cfg; spi_gpio_cfg_t gpio_cfg;
spi_cfg.frequency = 80ul * 1000ul * 1000ul; // SPI时钟源是350M,在此基础上再分频。本TFT支持最大100M. 设置60M实际是350/6=58.3MHz gpio_cfg.auto_cs = false; gpio_cfg.cs = GPIO_INVALID; gpio_cfg.clk = SCL_PIN; gpio_cfg.miso = GPIO_INVALID; gpio_cfg.mosi = SDA_PIN;
//if((gpio_cfg.auto_cs == false) && (gpio_cfg.cs < GPIO_INVALID)) //{ // gpio_open(gpio_cfg.cs, GPIO_DIRECTION_OUTPUT); // gpio_set_pull_mode(gpio_cfg.cs, GPIO_PULL_UP); //}
RET ret; if(RET_OK != (ret = spi_init(USE_SPI_PORT))) { printf("spi init err %d\r\n", ret); } if(RET_OK != (ret = spi_open(USE_SPI_PORT, &spi_cfg, &gpio_cfg))) { printf("spi open err %d\r\n", ret); } spi_modify_CPOL_CPHA(USE_SPI_PORT, SPI_CLK_MODE_0);
printf("spi init\r\n"); }}
static void port_gc9a01a_deinit(void){ gpio_close(DC_PIN); if(RST1_PIN < GPIO_INVALID) { gpio_close(RST1_PIN); } if(RST2_PIN < GPIO_INVALID) { gpio_close(RST2_PIN); } spi_close(USE_SPI_PORT);} #endif
/****************************************************************************** * 以下是GC9A01A设备实例 * ******************************************************************************/
static uint16_t s_gc9a01a_itf_buffer[2][GC9A01A_HSIZE][GC9A01A_VSIZE]; /**< 显存 */
/* 设备实例 */static gc9a01a_dev_st s_gc9a01a_itf_dev1 ={ .set_dcx = port_gc9a01a_set_dcx, .set_reset = port_gc9a01a_set_reset1, .write = port_gc9a01a_spi_write, .enable = port_gc9a01a_spi_enable1, .delay = port_gc9a01a_delay_ms, .init = port_gc9a01a_init, .deinit = port_gc9a01a_deinit,
.buffer = (uint16_t*)(s_gc9a01a_itf_buffer[0]),};
/* 设备实例 */static gc9a01a_dev_st s_gc9a01a_itf_dev2 ={ .set_dcx = port_gc9a01a_set_dcx, .set_reset = port_gc9a01a_set_reset2, .write = port_gc9a01a_spi_write, .enable = port_gc9a01a_spi_enable2, .delay = port_gc9a01a_delay_ms, .init = port_gc9a01a_init, .deinit = port_gc9a01a_deinit,
.buffer = (uint16_t*)(s_gc9a01a_itf_buffer[1]),};
/****************************************************************************** * 以下是对外操作接口 * ******************************************************************************/

/** * \fn gc9a01a_itf_init * 初始化 * \retval 0 成功 * \retval 其他值 失败*/int gc9a01a_itf_init(void){#if 0 gpio_open(DC_PIN, GPIO_DIRECTION_OUTPUT); gpio_set_pull_mode(DC_PIN, GPIO_PULL_UP); gpio_write(DC_PIN,1); gpio_open(CS_PIN, GPIO_DIRECTION_OUTPUT); gpio_set_pull_mode(CS_PIN, GPIO_PULL_UP); gpio_write(CS_PIN,1); gpio_open(SCL_PIN, GPIO_DIRECTION_OUTPUT); gpio_set_pull_mode(SCL_PIN, GPIO_PULL_UP); gpio_write(SCL_PIN,1); gpio_open(SDA_PIN, GPIO_DIRECTION_OUTPUT); gpio_set_pull_mode(SDA_PIN, GPIO_PULL_UP); gpio_write(SDA_PIN,1); while(1) { static int s_cnt = 0; if(s_cnt%1 == 0) { gpio_toggle(DC_PIN); } if(s_cnt%2 == 1) { gpio_toggle(CS_PIN); } if(s_cnt%4 == 2) { gpio_toggle(SCL_PIN); } if(s_cnt%8 == 3) { gpio_toggle(SDA_PIN); } os_delay(1); s_cnt++; }#endif static int s_gc9a01a_init_flag = 0; if(s_gc9a01a_init_flag == 0) { s_gc9a01a_init_flag = 1; /* 只初始化一次 */ gc9a01a_init(&s_gc9a01a_itf_dev1); gc9a01a_init(&s_gc9a01a_itf_dev2); } return 0;}
/** * \fn gc9a01a_itf_deinit * 解除初始化 * \retval 0 成功 * \retval 其他值 失败*/int gc9a01a_itf_deinit(void){ gc9a01a_deinit(&s_gc9a01a_itf_dev1); gc9a01a_deinit(&s_gc9a01a_itf_dev2); return 0;}
/** * \fn gc9a01a_itf_sync * 刷新显示 * \param[in] id 设备索引 * \retval 0 成功 * \retval 其他值 失败*/int gc9a01a_itf_sync(int id){ if(id == 0) { gc9a01a_sync(&s_gc9a01a_itf_dev1, 0, GC9A01A_HSIZE-1, 0, GC9A01A_VSIZE-1, s_gc9a01a_itf_dev1.buffer, GC9A01A_HSIZE*GC9A01A_VSIZE*2); return 0; } else if(id == 1) { gc9a01a_sync(&s_gc9a01a_itf_dev2, 0, GC9A01A_HSIZE-1, 0, GC9A01A_VSIZE-1, s_gc9a01a_itf_dev2.buffer, GC9A01A_HSIZE*GC9A01A_VSIZE*2); return 0; }
return -1;}
/** * \fn gc9a01a_itf_set_pixel * 写点 * \param[in] id 设备索引 * \param[in] x x坐标位置 * \param[in] y y坐标位置 * \param[in] rgb565 颜色*/void gc9a01a_itf_set_pixel(int id, uint16_t x, uint16_t y, uint16_t rgb565){ //if(x >= GC9A01A_HSIZE) //{ // return -1; //} //if(y >= GC9A01A_VSIZE) //{ // return -1; //} if(id == 0) { s_gc9a01a_itf_dev1.buffer[y*GC9A01A_HSIZE + x] = (uint16_t)((rgb565>>8)&0xFF) | (uint16_t)((rgb565<<8) & 0xFF00); } else if(id == 1) { s_gc9a01a_itf_dev2.buffer[y*GC9A01A_HSIZE + x] = (uint16_t)((rgb565>>8)&0xFF) | (uint16_t)((rgb565<<8) & 0xFF00); }}
/** * \fn gc9a01a_itf_get_pixel * 读点 * \param[in] id 设备索引 * \param[in] x x坐标位置 * \param[in] y y坐标位置 * \return rgb565颜色*/uint16_t gc9a01a_itf_get_pixel(int id, uint16_t x, uint16_t y){ if(id == 0) { uint16_t color = s_gc9a01a_itf_dev1.buffer[y*GC9A01A_HSIZE + x]; return ((uint16_t)(color>>8) | (uint16_t)(color<<8)); } else if(id == 1) { uint16_t color = s_gc9a01a_itf_dev2.buffer[y*GC9A01A_HSIZE + x]; return ((uint16_t)(color>>8) | (uint16_t)(color<<8)); } return 0;}

gc9a01a_test.h/c也相应的增加两个屏幕的测试,修改后内容如下

gc9a01a_test.h

#ifndef GC9A01A_TEST_H#define GC9A01A_TEST_H
#ifdef __cplusplus extern "C"{#endif
#include
int gc9a01a_test(void);
#ifdef __cplusplus }#endif
#endif

gc9a01a_test.c


#include #include #include #include "timer.h"#include "gc9a01a_itf.h"#include "gc9a01a_test.h"
static void rgb_test(void){ for(int x=0;x { for(int y=0;y { gc9a01a_itf_set_pixel(0,x, y, 0xF800); gc9a01a_itf_set_pixel(1,x, y, 0xF800); } } gc9a01a_itf_sync(0); gc9a01a_itf_sync(1); os_delay(1000);
for(int x=0;x { for(int y=0;y { gc9a01a_itf_set_pixel(0,x, y, 0x07E0); gc9a01a_itf_set_pixel(1,x, y, 0x07E0); } } gc9a01a_itf_sync(0); gc9a01a_itf_sync(1); os_delay(1000);
for(int x=0;x { for(int y=0;y { gc9a01a_itf_set_pixel(0,x, y, 0x001F); gc9a01a_itf_set_pixel(1,x, y, 0x001F); } } gc9a01a_itf_sync(0); gc9a01a_itf_sync(1); os_delay(1000);}
int gc9a01a_test(void){ printf("gc9a01a test\r\n"); gc9a01a_itf_init(); rgb_test(); uint32_t start; uint32_t end; uint32_t ftime = 0; while(0) { start = timer_get_time(); for(int i=0;i<10;i++) { gc9a01a_itf_sync(0); gc9a01a_itf_sync(1); } end = timer_get_time(); ftime = (end - start);
uint32_t fps = (ftime*2+100)/(100*2); /* 刷新一次的时间uS */ if(fps > 0) { printf("FPS:%d\r\n",1000000/fps); } else { printf("FPS:%d\r\n",0); } } return 0;}

Gc9a01a.c/h不需要变化,内容如下

gc9a01a.c

#include "gc9a01a.h"
/** * \struct gc9a01a_cmd_st * 命令结构体*/typedef struct{ uint8_t cmd; /**< 命令 */ uint8_t data[12]; /**< 参数,最多5个参数 */ uint8_t datalen; /**< 参数长度 */ uint16_t delay; /**< 延时时间 */} gc9a01a_cmd_st;
static gc9a01a_cmd_st s_gc9a01a_cmd_init_list[]={ ///{0xEF,{0},0,0}, ///{0xEB,{0x14},1,0},
/* 很多寄存器访问都需要Inter_command为高(默认为低)所以先发FE和EF配置Inter_command为高 */ {GC9A01A_CMD_IRE1,{0},0,0}, {GC9A01A_CMD_IRE2,{0},0,0}, {0xEB,{0x14},1,0},
{0x84,{0x40},1,0}, {0x85,{0xFF},1,0}, {0x86,{0xFF},1,0}, {0x87,{0xFF},1,0}, {0x88,{0x0A},1,0}, {0x89,{0x21},1,0}, {0x8A,{0x00},1,0}, {0x8B,{0x80},1,0}, {0x8C,{0x01},1,0}, {0x8D,{0x01},1,0}, {0x8E,{0xFF},1,0}, {0x8F,{0xFF},1,0},
/* 设置GS SS * 第一个参数为0,第二个参数有效 * GS bit6 0:G1->G32 1:G32->G1 * SS bit5 0:S1->S360 1:S360->S1 */ {GC9A01A_CMD_DFC,{0x00,0x20},2,0}, /** * Memory Access Control * 7 6 5 4 3 2 1 0 * MY MX MV ML BGR MH 0 0 * Y反转 X反转 XY交换 垂直刷新方向 0-RGB 水平刷新方向 * 1-BGR */ {GC9A01A_CMD_MADCTL,{0x08},1,0}, /* Pixel Format Set *7 【6 5 4】 3 【2 1 0】 * DPI DBI * RGB接口 MCU接口 * 101 16位 011 12位 * 110 18位 101 16位 * 110 18位 */ {GC9A01A_CMD_COLMOD,{0x55},1,0},
{0x90,{0x08,0x08,0x08,0x08},4,0}, {0xBD,{0x06},1,0}, {0xBC,{0x00},1,0},
{0xFF,{0x60,0x01,0x04},3,0},
/* 电源控制 */ {GC9A01A_CMD_PC2,{0x13},1,0}, //vbp {GC9A01A_CMD_PC3,{0x13},1,0}, //vbn {GC9A01A_CMD_PC4,{0x22},1,0}, //vrh
{0xBE,{0x11},1,0}, {0xE1,{0x10,0x0E},2,0}, {0xDF,{0x21,0x0C,0x02},3,0}, /* 设置gamma曲线 */ {GC9A01A_CMD_SETGAMMA1,{0x45,0x09,0x08,0x08,0x26,0x2A},6,0}, //默认值 80 03 08 06 05 2B {GC9A01A_CMD_SETGAMMA2,{0x43,0x70,0x72,0x36,0x37,0x6F},6,0}, //默认值 41 97 98 13 17 CD {GC9A01A_CMD_SETGAMMA3,{0x45,0x09,0x08,0x08,0x26,0x2A},6,0}, //默认值 40 03 08 0B 08 2E {GC9A01A_CMD_SETGAMMA3,{0x43,0x70,0x72,0x36,0x37,0x6F},6,0}, //默认值 3F 98 B4 14 18 CD
{0xED,{0x1B,0x0B},2,0}, {0xAE,{0x77},1,0}, {0xED,{0x1B,0x0B},2,0}, {0xCD,{0x63},1,0},
{0x70,{0x07,0x07,0x04,0x0E,0x0F,0x09,0x07,0x08,0x03},9,0}, {0xEB,{0x34},1,0}, {0x62,{0x18,0x0D,0x71,0xED,0x70,0x70,0x18,0x0F,0x71,0xEF,0x70,0x70},12,0}, {0x63,{0x18,0x11,0x71,0xF1,0x70,0x70,0x18,0x13,0x71,0xF3,0x70,0x70},12,0}, {0x64,{0x28,0x29,0xF1,0x01,0xF1,0x00,0x07},7,0}, {0x66,{0x3C,0x00,0xCD,0x67,0x45,0x45,0x10,0x00,0x00,0x00},10,0}, {0x67,{0x00,0x3C,0x00,0x00,0x00,0x01,0x54,0x10,0x32,0x98},10,0}, {0x74,{0x10,0x85,0x80,0x00,0x00,0x4E,0x00},7,0},
{0x98,{0x3E,0x07},2,0},
{GC9A01A_CMD_TELON,{0},0,0}, /* Tearing Effect Line ON */ {GC9A01A_CMD_INVON, {0x00},0,0}, {GC9A01A_CMD_SLPOUT,{0 },0,120}, /**< SLPOUT (11h): Sleep Out */ {GC9A01A_CMD_DISPON,{0}, 0,20}, /**< DISPON (29h): Display On */};
/** * \fn gc9a01a_write_cmd * 写命令 * \param[in] dev \ref gc9a01a_dev_st * \param[in] cmd 命令字节 * \retval 0 成功 * \retval 其他值 失败*/static int gc9a01a_write_cmd(gc9a01a_dev_st* dev,uint8_t cmd){ uint8_t tmp;#if GC9A01A_CHECK_PARAM if(dev == (gc9a01a_dev_st*)0) { return -1; } if(dev->set_dcx == (gc9a01a_set_dcx_pf)0) { return -1; } if(dev->write == (gc9a01a_spi_write_pf)0) { return -1; }#endif tmp = cmd; dev->enable(1); dev->set_dcx(0); dev->write(&tmp,1); dev->enable(0); return 0;}
/** * \fn gc9a01a_write_data * 写数据 * \param[in] dev \ref gc9a01a_dev_st * \param[in] data 待写入数据 * \param[in] len 待写入数据长度 * \retval 0 成功 * \retval 其他值 失败*/static int gc9a01a_write_data(gc9a01a_dev_st* dev,uint8_t* data, uint32_t len){#if GC9A01A_CHECK_PARAM if(dev == (gc9a01a_dev_st*)0) { return -1; } if(dev->set_dcx == (gc9a01a_set_dcx_pf)0) { return -1; } if(dev->write == (gc9a01a_spi_write_pf)0) { return -1; }#endif dev->enable(1); dev->set_dcx(1); dev->write(data,len); dev->enable(0); return 0;}
/** * \fn gc9a01a_set_windows * 设置窗口范围(行列地址) * \param[in] dev \ref gc9a01a_dev_st * \param[in] data 待写入数据 * \param[in] len 待写入数据长度 * \retval 0 成功 * \retval 其他值 失败*/static int gc9a01a_set_windows(gc9a01a_dev_st* dev, uint16_t x0, uint16_t x1, uint16_t y0, uint16_t y1){ uint8_t data[4]; gc9a01a_write_cmd(dev, GC9A01A_CMD_CASET); data[0] = (x0>>8) & 0xFF; /* 列开始地址 大端 */ data[1] = x0 & 0xFF; data[2] = (x1>>8) & 0xFF; /* 列结束地址 大端 */ data[3] = x1 & 0xFF; gc9a01a_write_data(dev, data, 4);
gc9a01a_write_cmd(dev, GC9A01A_CMD_RASET); data[0] = (y0>>8) & 0xFF; /* 行开始地址 大端 */ data[1] = y0 & 0xFF; data[2] = (y1>>8) & 0xFF; /* 行结束地址 大端 */ data[3] = y1 & 0xFF; gc9a01a_write_data(dev, data, 4);
return 0;}
/** * \fn gc9a01a_sync * 现存写入gc9a01a * \param[in] dev \ref gc9a01a_dev_st * \paran[in] x0 列开始地址 * \paran[in] x1 列结束地址 * \paran[in] y0 行开始地址 * \paran[in] y1 行结束地址 * \paran[in] buffer 待写入数据 * \paran[in] len 待写入数据长度 * \retval 0 成功 * \retval 其他值 失败*/int gc9a01a_sync(gc9a01a_dev_st* dev, uint16_t x0, uint16_t x1, uint16_t y0, uint16_t y1, uint16_t* buffer, uint32_t len){ (void)dev; gc9a01a_set_windows(dev, x0, x1, y0, y1); gc9a01a_write_cmd(dev,GC9A01A_CMD_RAMWR); gc9a01a_write_data(dev, (uint8_t*)buffer, len); return 0;}
/** * \fn gc9a01a_init * 初始化 * \param[in] dev \ref gc9a01a_dev_st * \retval 0 成功 * \retval 其他值 失败*/int gc9a01a_init(gc9a01a_dev_st* dev){#if GC9A01A_CHECK_PARAM if(dev == (gc9a01a_dev_st*)0) { return -1; }#endif if(dev->init_flag != 0) { return 0; } dev->init_flag = 1; if(dev->init != 0) { dev->init(); } dev->set_reset(1); dev->delay(120); dev->set_reset(0); dev->delay(120); dev->set_reset(1); dev->delay(120);
/* 初始化序列 */ for(uint32_t i=0; i<sizeof(s_gc9a01a_cmd_init_list)/sizeof(s_gc9a01a_cmd_init_list[0]); i++) { gc9a01a_write_cmd(dev, s_gc9a01a_cmd_init_list[i].cmd); if(s_gc9a01a_cmd_init_list[i].datalen > 0) { gc9a01a_write_data(dev, s_gc9a01a_cmd_init_list[i].data,s_gc9a01a_cmd_init_list[i].datalen); if(s_gc9a01a_cmd_init_list[i].delay > 0) { dev->delay(s_gc9a01a_cmd_init_list[i].delay); } } }
return 0;}
/** * \fn gc9a01a_deinit * 解除初始化 * \param[in] dev \ref gc9a01a_dev_st * \return 总是返回0*/int gc9a01a_deinit(gc9a01a_dev_st* dev){#if GC9A01A_CHECK_PARAM if(dev == (gc9a01a_dev_st*)0) { return -1; }#endif
/* @todo 添加IO等解除初始化配置 */
if(dev->deinit != 0) { dev->deinit(); } return 0;}

gc9a01a.h

#ifndef GC9A01A_H#define GC9A01A_H
#ifdef __cplusplus extern "C"{#endif
#include
#define GC9A01A_CHECK_PARAM 1
typedef void (*gc9a01a_set_dcx_pf)(uint8_t val); /**< DCX引脚操作接口,val=1为数据和参数, val=0为命令 */typedef void (*gc9a01a_set_reset_pf)(uint8_t val); /**< 复位引脚操作,val=1输出高,val=0输出低 */typedef void (*gc9a01a_spi_write_pf)(uint8_t* buffer, uint32_t len); /**< MOSI写接口接口,buffer为待写数据,len为待写长度 */typedef void (*gc9a01a_spi_enable_pf)(uint8_t val); /**< 使能接口 */typedef void (*gc9a01a_spi_delay_ms_pf)(uint32_t t); /**< 延时接口 */typedef void (*gc9a01a_init_pf)(void); /**< 初始化接口 */typedef void (*gc9a01a_deinit_pf)(void); /**< 解除初始化接口 */
#define GC9A01A_CMD_SLPOUT 0x11 /* 退出SLEEP模式 */#define GC9A01A_CMD_INVON 0x21 /* 显示反转 */#define GC9A01A_CMD_DISPON 0x29 /* 打开显示 */#define GC9A01A_CMD_CASET 0x2A /* 设置列地址 */#define GC9A01A_CMD_RASET 0x2B /* 设置行地址 */#define GC9A01A_CMD_RAMWR 0x2C /* 写数据 */#define GC9A01A_CMD_MADCTL 0x36 /* 显存访问控制 */#define GC9A01A_CMD_COLMOD 0x3A /* 点格式 */#define GC9A01A_CMD_IRE1 0xFE /* Inter Register Enable1 */#define GC9A01A_CMD_IRE2 0xEF /* Inter Register Enable2 */#define GC9A01A_CMD_DFC 0xB6 /* Display Function Control */#define GC9A01A_CMD_PC2 0xC3 /* Power Control 2 */#define GC9A01A_CMD_PC3 0xC4 /* Power Control 3 */#define GC9A01A_CMD_PC4 0xC9 /* Power Control 4 */#define GC9A01A_CMD_TELON 0x35 /* Tearing Effect Line ON */#define GC9A01A_CMD_SETGAMMA1 0xF0 /*SET_GAMMA1 */#define GC9A01A_CMD_SETGAMMA2 0xF1 /*SET_GAMMA2 */#define GC9A01A_CMD_SETGAMMA3 0xF2 /*SET_GAMMA3 */#define GC9A01A_CMD_SETGAMMA4 0xF3 /*SET_GAMMA4 */
/** * \struct gc9a01a_dev_st * 设备接口结构体*/typedef struct{ gc9a01a_set_dcx_pf set_dcx; /**< DCX写接口 */ gc9a01a_set_reset_pf set_reset; /**< RESET写接口 */ gc9a01a_spi_write_pf write; /**< 数据写接口 */ gc9a01a_spi_enable_pf enable; /**< 使能接口 */ gc9a01a_spi_delay_ms_pf delay; /**< 延时接口 */ gc9a01a_init_pf init; /**< 初始化接口 */ gc9a01a_deinit_pf deinit; /**< 解除初始化接口 */
uint16_t* buffer; /**< 显存,用户分配 */ int init_flag; /**< 初始化标志 */ } gc9a01a_dev_st;
/** * \fn gc9a01a_sync * 现存写入gc9a01a * \param[in] dev \ref gc9a01a_dev_st * \paran[in] x0 列开始地址 * \paran[in] x1 列结束地址 * \paran[in] y0 行开始地址 * \paran[in] y1 行结束地址 * \paran[in] buffer 待写入数据 * \paran[in] len 待写入数据长度 * \retval 0 成功 * \retval 其他值 失败*/int gc9a01a_sync(gc9a01a_dev_st* dev, uint16_t x0, uint16_t x1, uint16_t y0, uint16_t y1, uint16_t* buffer, uint32_t len);
/** * \fn gc9a01a_init * 初始化 * \param[in] dev \ref gc9a01a_dev_st * \retval 0 成功 * \retval 其他值 失败*/int gc9a01a_init(gc9a01a_dev_st* dev);
/** * \fn gc9a01a_deinit * 解除初始化 * \param[in] dev \ref gc9a01a_dev_st * \return 总是返回0*/int gc9a01a_deinit(gc9a01a_dev_st* dev);
#ifdef __cplusplus }#endif
#endif

三. LVGL驱动注册

LVGL是通过数据结构lv_disp_t来绑定显示器的,

在调用lv_disp_drv_register注册驱动时会返回lv_disp_t*

此时用户就可以使用该返回的lv_disp_t*,作为参数, 在创建obj时指定obj绑定到该lv_disp_t

所以我们需要将原来的

void  lv_port_disp_init(void);

改为

lv_disp_t* lv_port_disp_init(void);

以返回lv_disp_t*给用户使用。

所以在原来一份驱动的基础上

lv_port_disp.c

lv_port_disp.h

改为2份驱动

lv_port_disp1.c

lv_port_disp1.h

lv_port_disp2.c

lv_port_disp2.h

分别提供初始化函数给用户调用

lv_disp_t* lv_port_disp1_init(void);

lv_disp_t* lv_port_disp2_init(void);

因为使用的是同样的屏幕

接口lcd_itf_set_pixel增加一个ID参数即可。

对应的

lv_port_disp1.c

lv_port_disp1.h

内容如下

/** * @file lv_port_disp_templ.c * */
/*Copy this file as "lv_port_disp.c" and set this value to "1" to enable content*/#if 1
/********************* * INCLUDES *********************/#include "lcd_itf.h"#include "lv_port_disp1.h"#include #define MY_DISP_HOR_RES LCD_ITF_H_SIZE#define MY_DISP_VER_RES LCD_ITF_V_SIZE
/********************* * DEFINES *********************/#ifndef MY_DISP_HOR_RES #warning Please define or replace the macro MY_DISP_HOR_RES with the actual screen width, default value 320 is used for now. #define MY_DISP_HOR_RES 320#endif
#ifndef MY_DISP_VER_RES #warning Please define or replace the macro MY_DISP_HOR_RES with the actual screen height, default value 240 is used for now. #define MY_DISP_VER_RES 240#endif
/********************** * TYPEDEFS **********************/
/********************** * STATIC PROTOTYPES **********************/static void disp_init(void);
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p);//static void gpu_fill(lv_disp_drv_t * disp_drv, lv_color_t * dest_buf, lv_coord_t dest_width,// const lv_area_t * fill_area, lv_color_t color);
/********************** * STATIC VARIABLES **********************/
/********************** * MACROS **********************/
/********************** * GLOBAL FUNCTIONS **********************/
lv_disp_t* lv_port_disp1_init(void){ /*------------------------- * Initialize your display * -----------------------*/ disp_init();
/*----------------------------- * Create a buffer for drawing *----------------------------*/
/** * LVGL requires a buffer where it internally draws the widgets. * Later this buffer will passed to your display driver's `flush_cb` to copy its content to your display. * The buffer has to be greater than 1 display row * * There are 3 buffering configurations: * 1. Create ONE buffer: * LVGL will draw the display's content here and writes it to your display * * 2. Create TWO buffer: * LVGL will draw the display's content to a buffer and writes it your display. * You should use DMA to write the buffer's content to the display. * It will enable LVGL to draw the next part of the screen to the other buffer while * the data is being sent form the first buffer. It makes rendering and flushing parallel. * * 3. Double buffering * Set 2 screens sized buffers and set disp_drv.full_refresh = 1. * This way LVGL will always provide the whole rendered screen in `flush_cb` * and you only need to change the frame buffer's address. */
/* Example for 1) */ static lv_disp_draw_buf_t draw_buf_dsc_1; static lv_color_t buf_1[MY_DISP_HOR_RES * 10]; /*A buffer for 10 rows*/ lv_disp_draw_buf_init(&draw_buf_dsc_1, buf_1, NULL, MY_DISP_HOR_RES * 10); /*Initialize the display buffer*/
/* Example for 2) */ //static lv_disp_draw_buf_t draw_buf_dsc_2; //static lv_color_t buf_2_1[MY_DISP_HOR_RES * 10]; /*A buffer for 10 rows*/ //static lv_color_t buf_2_2[MY_DISP_HOR_RES * 10]; /*An other buffer for 10 rows*/ //lv_disp_draw_buf_init(&draw_buf_dsc_2, buf_2_1, buf_2_2, MY_DISP_HOR_RES * 10); /*Initialize the display buffer*/
/* Example for 3) also set disp_drv.full_refresh = 1 below*/ //static lv_disp_draw_buf_t draw_buf_dsc_3; //static lv_color_t buf_3_1[MY_DISP_HOR_RES * MY_DISP_VER_RES]; /*A screen sized buffer*/ //static lv_color_t buf_3_2[MY_DISP_HOR_RES * MY_DISP_VER_RES]; /*Another screen sized buffer*/ //lv_disp_draw_buf_init(&draw_buf_dsc_3, buf_3_1, buf_3_2, // MY_DISP_VER_RES * LV_VER_RES_MAX); /*Initialize the display buffer*/
/*----------------------------------- * Register the display in LVGL *----------------------------------*/
static lv_disp_drv_t disp_drv; /*Descriptor of a display driver*/ lv_disp_drv_init(&disp_drv); /*Basic initialization*/
/*Set up the functions to access to your display*/
/*Set the resolution of the display*/ disp_drv.hor_res = MY_DISP_HOR_RES; disp_drv.ver_res = MY_DISP_VER_RES;
/*Used to copy the buffer's content to the display*/ disp_drv.flush_cb = disp_flush;
/*Set a display buffer*/ disp_drv.draw_buf = &draw_buf_dsc_1;
/*Required for Example 3)*/ //disp_drv.full_refresh = 1;
/* Fill a memory array with a color if you have GPU. * Note that, in lv_conf.h you can enable GPUs that has built-in support in LVGL. * But if you have a different GPU you can use with this callback.*/ //disp_drv.gpu_fill_cb = gpu_fill;
/*Finally register the driver*/ return lv_disp_drv_register(&disp_drv);}
/********************** * STATIC FUNCTIONS **********************/
/*Initialize your display and the required peripherals.*/static void disp_init(void){ lcd_itf_init(); /*You code here*/}
volatile bool disp1_flush_enabled = true;
/* Enable updating the screen (the flushing process) when disp_flush() is called by LVGL */void disp1_enable_update(void){ disp1_flush_enabled = true;}
/* Disable updating the screen (the flushing process) when disp_flush() is called by LVGL */void disp1_disable_update(void){ disp1_flush_enabled = false;}
/*Flush the content of the internal buffer the specific area on the display *You can use DMA or any hardware acceleration to do this operation in the background but *'lv_disp_flush_ready()' has to be called when finished.*/static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p){ if(disp1_flush_enabled) { /*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/
int32_t x; int32_t y; for(y = area->y1; y <= area->y2; y++) { for(x = area->x1; x <= area->x2; x++) { /*Put a pixel to the display. For example:*/ /*put_px(x, y, *color_p)*/ lcd_itf_set_pixel(0,x, y, *((uint16_t*)color_p)); color_p++; } } }
/*IMPORTANT!!! *Inform the graphics library that you are ready with the flushing*/ lv_disp_flush_ready(disp_drv);}
/*OPTIONAL: GPU INTERFACE*/
/*If your MCU has hardware accelerator (GPU) then you can use it to fill a memory with a color*///static void gpu_fill(lv_disp_drv_t * disp_drv, lv_color_t * dest_buf, lv_coord_t dest_width,// const lv_area_t * fill_area, lv_color_t color)//{// /*It's an example code which should be done by your GPU*/// int32_t x, y;// dest_buf += dest_width * fill_area->y1; /*Go to the first line*///// for(y = fill_area->y1; y <= fill_area->y2; y++) {// for(x = fill_area->x1; x <= fill_area->x2; x++) {// dest_buf[x] = color;// }// dest_buf+=dest_width; /*Go to the next line*/// }//}
#else /*Enable this file at the top*/
/*This dummy typedef exists purely to silence -Wpedantic.*/typedef int keep_pedantic_happy;#endif
/** * @file lv_port_disp_templ.h * */
/*Copy this file as "lv_port_disp.h" and set this value to "1" to enable content*/#if 1
#ifndef LV_PORT_DISP1_TEMPL_H#define LV_PORT_DISP1_TEMPL_H
#ifdef __cplusplusextern "C" {#endif
/********************* * INCLUDES *********************/#if defined(LV_LVGL_H_INCLUDE_SIMPLE)#include "lvgl.h"#else#include "lvgl/lvgl.h"#endif
/********************* * DEFINES *********************/
/********************** * TYPEDEFS **********************/
/********************** * GLOBAL PROTOTYPES **********************//* Initialize low level display driver */lv_disp_t* lv_port_disp1_init(void);
/* Enable updating the screen (the flushing process) when disp_flush() is called by LVGL */void disp1_enable_update(void);
/* Disable updating the screen (the flushing process) when disp_flush() is called by LVGL */void disp1_disable_update(void);
/********************** * MACROS **********************/
#ifdef __cplusplus} /*extern "C"*/#endif
#endif /*LV_PORT_DISP_TEMPL_H*/
#endif /*Disable/Enable content*/

lv_port_disp2.c

lv_port_disp2.h

内容如下

/** * @file lv_port_disp_templ.c * */
/*Copy this file as "lv_port_disp.c" and set this value to "1" to enable content*/#if 1
/********************* * INCLUDES *********************/#include "lcd_itf.h"#include "lv_port_disp2.h"#include #define MY_DISP_HOR_RES LCD_ITF_H_SIZE#define MY_DISP_VER_RES LCD_ITF_V_SIZE
/********************* * DEFINES *********************/#ifndef MY_DISP_HOR_RES #warning Please define or replace the macro MY_DISP_HOR_RES with the actual screen width, default value 320 is used for now. #define MY_DISP_HOR_RES 320#endif
#ifndef MY_DISP_VER_RES #warning Please define or replace the macro MY_DISP_HOR_RES with the actual screen height, default value 240 is used for now. #define MY_DISP_VER_RES 240#endif
/********************** * TYPEDEFS **********************/
/********************** * STATIC PROTOTYPES **********************/static void disp_init(void);
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p);//static void gpu_fill(lv_disp_drv_t * disp_drv, lv_color_t * dest_buf, lv_coord_t dest_width,// const lv_area_t * fill_area, lv_color_t color);
/********************** * STATIC VARIABLES **********************/
/********************** * MACROS **********************/
/********************** * GLOBAL FUNCTIONS **********************/
lv_disp_t* lv_port_disp2_init(void){ /*------------------------- * Initialize your display * -----------------------*/ disp_init();
/*----------------------------- * Create a buffer for drawing *----------------------------*/
/** * LVGL requires a buffer where it internally draws the widgets. * Later this buffer will passed to your display driver's `flush_cb` to copy its content to your display. * The buffer has to be greater than 1 display row * * There are 3 buffering configurations: * 1. Create ONE buffer: * LVGL will draw the display's content here and writes it to your display * * 2. Create TWO buffer: * LVGL will draw the display's content to a buffer and writes it your display. * You should use DMA to write the buffer's content to the display. * It will enable LVGL to draw the next part of the screen to the other buffer while * the data is being sent form the first buffer. It makes rendering and flushing parallel. * * 3. Double buffering * Set 2 screens sized buffers and set disp_drv.full_refresh = 1. * This way LVGL will always provide the whole rendered screen in `flush_cb` * and you only need to change the frame buffer's address. */
/* Example for 1) */ static lv_disp_draw_buf_t draw_buf_dsc_1; static lv_color_t buf_1[MY_DISP_HOR_RES * 10]; /*A buffer for 10 rows*/ lv_disp_draw_buf_init(&draw_buf_dsc_1, buf_1, NULL, MY_DISP_HOR_RES * 10); /*Initialize the display buffer*/
/* Example for 2) */ //static lv_disp_draw_buf_t draw_buf_dsc_2; //static lv_color_t buf_2_1[MY_DISP_HOR_RES * 10]; /*A buffer for 10 rows*/ //static lv_color_t buf_2_2[MY_DISP_HOR_RES * 10]; /*An other buffer for 10 rows*/ //lv_disp_draw_buf_init(&draw_buf_dsc_2, buf_2_1, buf_2_2, MY_DISP_HOR_RES * 10); /*Initialize the display buffer*/
/* Example for 3) also set disp_drv.full_refresh = 1 below*/ //static lv_disp_draw_buf_t draw_buf_dsc_3; //static lv_color_t buf_3_1[MY_DISP_HOR_RES * MY_DISP_VER_RES]; /*A screen sized buffer*/ //static lv_color_t buf_3_2[MY_DISP_HOR_RES * MY_DISP_VER_RES]; /*Another screen sized buffer*/ //lv_disp_draw_buf_init(&draw_buf_dsc_3, buf_3_1, buf_3_2, // MY_DISP_VER_RES * LV_VER_RES_MAX); /*Initialize the display buffer*/
/*----------------------------------- * Register the display in LVGL *----------------------------------*/
static lv_disp_drv_t disp_drv; /*Descriptor of a display driver*/ lv_disp_drv_init(&disp_drv); /*Basic initialization*/
/*Set up the functions to access to your display*/
/*Set the resolution of the display*/ disp_drv.hor_res = MY_DISP_HOR_RES; disp_drv.ver_res = MY_DISP_VER_RES;
/*Used to copy the buffer's content to the display*/ disp_drv.flush_cb = disp_flush;
/*Set a display buffer*/ disp_drv.draw_buf = &draw_buf_dsc_1;
/*Required for Example 3)*/ //disp_drv.full_refresh = 1;
/* Fill a memory array with a color if you have GPU. * Note that, in lv_conf.h you can enable GPUs that has built-in support in LVGL. * But if you have a different GPU you can use with this callback.*/ //disp_drv.gpu_fill_cb = gpu_fill;
/*Finally register the driver*/ return lv_disp_drv_register(&disp_drv);}
/********************** * STATIC FUNCTIONS **********************/
/*Initialize your display and the required peripherals.*/static void disp_init(void){ lcd_itf_init(); /*You code here*/}
volatile bool disp2_flush_enabled = true;
/* Enable updating the screen (the flushing process) when disp_flush() is called by LVGL */void disp2_enable_update(void){ disp2_flush_enabled = true;}
/* Disable updating the screen (the flushing process) when disp_flush() is called by LVGL */void disp2_disable_update(void){ disp2_flush_enabled = false;}
/*Flush the content of the internal buffer the specific area on the display *You can use DMA or any hardware acceleration to do this operation in the background but *'lv_disp_flush_ready()' has to be called when finished.*/static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p){ if(disp2_flush_enabled) { /*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/
int32_t x; int32_t y; for(y = area->y1; y <= area->y2; y++) { for(x = area->x1; x <= area->x2; x++) { /*Put a pixel to the display. For example:*/ /*put_px(x, y, *color_p)*/ lcd_itf_set_pixel(1,x, y, *((uint16_t*)color_p)); color_p++; } } }
/*IMPORTANT!!! *Inform the graphics library that you are ready with the flushing*/ lv_disp_flush_ready(disp_drv);}
/*OPTIONAL: GPU INTERFACE*/
/*If your MCU has hardware accelerator (GPU) then you can use it to fill a memory with a color*///static void gpu_fill(lv_disp_drv_t * disp_drv, lv_color_t * dest_buf, lv_coord_t dest_width,// const lv_area_t * fill_area, lv_color_t color)//{// /*It's an example code which should be done by your GPU*/// int32_t x, y;// dest_buf += dest_width * fill_area->y1; /*Go to the first line*///// for(y = fill_area->y1; y <= fill_area->y2; y++) {// for(x = fill_area->x1; x <= fill_area->x2; x++) {// dest_buf[x] = color;// }// dest_buf+=dest_width; /*Go to the next line*/// }//}
#else /*Enable this file at the top*/
/*This dummy typedef exists purely to silence -Wpedantic.*/typedef int keep_pedantic_happy;#endif
/** * @file lv_port_disp_templ.h * */
/*Copy this file as "lv_port_disp.h" and set this value to "1" to enable content*/#if 1
#ifndef LV_PORT_DISP2_TEMPL_H#define LV_PORT_DISP2_TEMPL_H
#ifdef __cplusplusextern "C" {#endif
/********************* * INCLUDES *********************/#if defined(LV_LVGL_H_INCLUDE_SIMPLE)#include "lvgl.h"#else#include "lvgl/lvgl.h"#endif
/********************* * DEFINES *********************/
/********************** * TYPEDEFS **********************/
/********************** * GLOBAL PROTOTYPES **********************//* Initialize low level display driver */lv_disp_t* lv_port_disp2_init(void);
/* Enable updating the screen (the flushing process) when disp_flush() is called by LVGL */void disp2_enable_update(void);
/* Disable updating the screen (the flushing process) when disp_flush() is called by LVGL */void disp2_disable_update(void);
/********************** * MACROS **********************/
#ifdef __cplusplus} /*extern "C"*/#endif
#endif /*LV_PORT_DISP_TEMPL_H*/
#endif /*Disable/Enable content*/

.测试

分别在两个屏幕各创建一个按钮显示不同内容


static void btn_event_cb(lv_event_t * e){    lv_event_code_t code = lv_event_get_code(e);    lv_obj_t * btn = lv_event_get_target(e);    if(code == LV_EVENT_CLICKED) {        static uint8_t cnt = 0;        cnt++;
        /*Get the first child of the button which is the label and change its text*/        lv_obj_t * label = lv_obj_get_child(btn, 0);        lv_label_set_text_fmt(label, "Button: %d", cnt);    }}
/** * Create a button with a label and react on click event. */static void lv_example_get_started_1(lv_disp_t* disp1, lv_disp_t* disp2){    lv_obj_t * btn1 = lv_btn_create(lv_disp_get_scr_act(disp1));     /*Add a button the current screen*/    lv_obj_set_pos(btn1, (LCD_ITF_H_SIZE-120)/2, (LCD_ITF_V_SIZE-50)/2);                            /*Set its position*/    lv_obj_set_size(btn1, 120, 50);                          /*Set its size*/    lv_obj_add_event_cb(btn1, btn_event_cb, LV_EVENT_ALL, NULL);           /*Assign a callback to the button*/
    lv_obj_t * label1 = lv_label_create(btn1);          /*Add a label to the button*/    lv_label_set_text(label1, "This is disp1!");                     /*Set the labels text*/    lv_obj_center(label1);
    lv_obj_t * btn2 = lv_btn_create(lv_disp_get_scr_act(disp2));     /*Add a button the current screen*/    lv_obj_set_pos(btn2, (LCD_ITF_H_SIZE-120)/2, (LCD_ITF_V_SIZE-50)/2);                            /*Set its position*/    lv_obj_set_size(btn2, 120, 50);                     /*Set its size*/    lv_obj_add_event_cb(btn2, btn_event_cb, LV_EVENT_ALL, NULL);           /*Assign a callback to the button*/
    lv_obj_t * label2 = lv_label_create(btn2);          /*Add a label to the button*/    lv_label_set_text(label2, "This is disp2!");                     /*Set the labels text*/    lv_obj_center(label2);}
static lv_disp_t* s_disp1;static lv_disp_t* s_disp2;

执行

while(1)    {        lv_log_register_print_cb(my_print);
        lv_init();        s_disp1 = lv_port_disp1_init();        if(s_disp1 == (lv_disp_t*)0)        {            printf("disp1 init err\r\n");        }        s_disp2 = lv_port_disp2_init();        if(s_disp2 == (lv_disp_t*)0)        {            printf("disp2 init err\r\n");        }        lv_example_get_started_1(s_disp1,s_disp2);        while(1)        {                   uint32_t delay = lv_timer_handler();            if (delay < 1) delay = 1;            os_delay(delay);        }        lv_deinit();    }

效果如下

五. MusicDemo

Demo中接口增加lv_disp_t参数由用户传入以绑定不同显示器,同时对应的list等全局变量也增加一份,根据不同显示器区分索引。

lv_demo_music.h改为如下

/** * @file lv_demo_music.h * */
#ifndef LV_DEMO_MUSIC_H#define LV_DEMO_MUSIC_H
#ifdef __cplusplusextern "C" {#endif
/********************* * INCLUDES *********************/#include "../lv_demos.h"
#if LV_USE_DEMO_MUSIC
/********************* * DEFINES *********************/
#if LV_DEMO_MUSIC_LARGE# define LV_DEMO_MUSIC_HANDLE_SIZE 40#else# define LV_DEMO_MUSIC_HANDLE_SIZE 20#endif
/********************** * TYPEDEFS **********************/
/********************** * GLOBAL PROTOTYPES **********************/
void lv_demo_music(lv_disp_t* disp1, lv_disp_t* disp2);void lv_demo_music_close(lv_disp_t* disp1, lv_disp_t* disp2);
const char * _lv_demo_music_get_title(uint32_t track_id);const char * _lv_demo_music_get_artist(uint32_t track_id);const char * _lv_demo_music_get_genre(uint32_t track_id);uint32_t _lv_demo_music_get_track_length(uint32_t track_id);
/********************** * MACROS **********************/
#endif /*LV_USE_DEMO_MUSIC*/
#ifdef __cplusplus} /* extern "C" */#endif
#endif /*LV_DEMO_MUSIC_H*/

lv_demo_music.c改为如下

/** * @file lv_demo_music.c * */
/********************* * INCLUDES *********************/#include "lv_demo_music.h"
#if LV_USE_DEMO_MUSIC
#include "lv_demo_music_main.h"#include "lv_demo_music_list.h"
/********************* * DEFINES *********************/
/********************** * TYPEDEFS **********************/
/********************** * STATIC PROTOTYPES **********************/#if LV_DEMO_MUSIC_AUTO_PLAY static void auto_step_cb(lv_timer_t * timer);#endif
/********************** * STATIC VARIABLES **********************/static lv_obj_t * ctrl[2];static lv_obj_t * list[2];
static const char * title_list[] = { "Waiting for true love", "Need a Better Future", "Vibrations", "Why now?", "Never Look Back", "It happened Yesterday", "Feeling so High", "Go Deeper", "Find You There", "Until the End", "Unknown", "Unknown", "Unknown", "Unknown",};
static const char * artist_list[] = { "The John Smith Band", "My True Name", "Robotics", "John Smith", "My True Name", "Robotics", "Robotics", "Unknown artist", "Unknown artist", "Unknown artist", "Unknown artist", "Unknown artist", "Unknown artist", "Unknown artist", "Unknown artist",};
static const char * genre_list[] = { "Rock - 1997", "Drum'n bass - 2016", "Psy trance - 2020", "Metal - 2015", "Metal - 2015", "Metal - 2015", "Metal - 2015", "Metal - 2015", "Metal - 2015", "Metal - 2015", "Metal - 2015", "Metal - 2015", "Metal - 2015", "Metal - 2015",};
static const uint32_t time_list[] = { 1 * 60 + 14, 2 * 60 + 26, 1 * 60 + 54, 2 * 60 + 24, 2 * 60 + 37, 3 * 60 + 33, 1 * 60 + 56, 3 * 60 + 31, 2 * 60 + 20, 2 * 60 + 19, 2 * 60 + 20, 2 * 60 + 19, 2 * 60 + 20, 2 * 60 + 19,};
#if LV_DEMO_MUSIC_AUTO_PLAY static lv_timer_t * auto_step_timer[2]; #endif
static lv_color_t original_screen_bg_color[2];
/********************** * MACROS **********************/
/********************** * GLOBAL FUNCTIONS **********************/
void lv_demo_music(lv_disp_t* disp1, lv_disp_t* disp2){ original_screen_bg_color[0] = lv_obj_get_style_bg_color(lv_disp_get_scr_act(disp1), 0); lv_obj_set_style_bg_color(lv_disp_get_scr_act(disp1), lv_color_hex(0x343247), 0); original_screen_bg_color[1] = lv_obj_get_style_bg_color(lv_disp_get_scr_act(disp2), 0); lv_obj_set_style_bg_color(lv_disp_get_scr_act(disp2), lv_color_hex(0x343247), 0);
list[0] = _lv_demo_music_list_create(lv_disp_get_scr_act(disp1)); list[1] = _lv_demo_music_list_create(lv_disp_get_scr_act(disp2)); ctrl[0] = _lv_demo_music_main_create(lv_disp_get_scr_act(disp1)); ctrl[1] = _lv_demo_music_main_create(lv_disp_get_scr_act(disp2));#if LV_DEMO_MUSIC_AUTO_PLAY auto_step_timer[0] = lv_timer_create(auto_step_cb, 1000, (void*)0); auto_step_timer[1] = lv_timer_create(auto_step_cb, 1000, (void*)1);#endif}
void lv_demo_music_close(lv_disp_t* disp1, lv_disp_t* disp2){ /*Delete all aniamtions*/ lv_anim_del(NULL, NULL);
#if LV_DEMO_MUSIC_AUTO_PLAY lv_timer_del(auto_step_timer[0]); lv_timer_del(auto_step_timer[1]);#endif _lv_demo_music_list_close(); _lv_demo_music_main_close();
lv_obj_clean(lv_disp_get_scr_act(disp1)); lv_obj_clean(lv_disp_get_scr_act(disp2));
lv_obj_set_style_bg_color(lv_disp_get_scr_act(disp1), original_screen_bg_color[0], 0); lv_obj_set_style_bg_color(lv_disp_get_scr_act(disp2), original_screen_bg_color[1], 0);}
const char * _lv_demo_music_get_title(uint32_t track_id){ if(track_id >= sizeof(title_list) / sizeof(title_list[0])) return NULL; return title_list[track_id];}
const char * _lv_demo_music_get_artist(uint32_t track_id){ if(track_id >= sizeof(artist_list) / sizeof(artist_list[0])) return NULL; return artist_list[track_id];}
const char * _lv_demo_music_get_genre(uint32_t track_id){ if(track_id >= sizeof(genre_list) / sizeof(genre_list[0])) return NULL; return genre_list[track_id];}
uint32_t _lv_demo_music_get_track_length(uint32_t track_id){ if(track_id >= sizeof(time_list) / sizeof(time_list[0])) return 0; return time_list[track_id];}
/********************** * STATIC FUNCTIONS **********************/
#if LV_DEMO_MUSIC_AUTO_PLAYstatic void auto_step_cb(lv_timer_t * t){ void* user_data = t->user_data; int id = (int)user_data; LV_UNUSED(t); static uint32_t state = 0;
#if LV_DEMO_MUSIC_LARGE const lv_font_t * font_small = &lv_font_montserrat_22; const lv_font_t * font_large = &lv_font_montserrat_32;#else const lv_font_t * font_small = &lv_font_montserrat_12; const lv_font_t * font_large = &lv_font_montserrat_16;#endif
switch(state) { case 5: _lv_demo_music_album_next(true); break;
case 6: _lv_demo_music_album_next(true); break; case 7: _lv_demo_music_album_next(true); break; case 8: _lv_demo_music_play(0); break;#if LV_DEMO_MUSIC_SQUARE || LV_DEMO_MUSIC_ROUND case 11: lv_obj_scroll_by(ctrl[id], 0, -LV_VER_RES, LV_ANIM_ON); break; case 13: lv_obj_scroll_by(ctrl[id], 0, -LV_VER_RES, LV_ANIM_ON); break;#else case 12: lv_obj_scroll_by(ctrl[id], 0, -LV_VER_RES, LV_ANIM_ON); break;#endif case 15: lv_obj_scroll_by(list[id], 0, -300, LV_ANIM_ON); break; case 16: lv_obj_scroll_by(list[id], 0, 300, LV_ANIM_ON); break; case 18: _lv_demo_music_play(1); break; case 19: lv_obj_scroll_by(ctrl[id], 0, LV_VER_RES, LV_ANIM_ON); break;#if LV_DEMO_MUSIC_SQUARE || LV_DEMO_MUSIC_ROUND case 20: lv_obj_scroll_by(ctrl[id], 0, LV_VER_RES, LV_ANIM_ON); break;#endif case 30: _lv_demo_music_play(2); break; case 40: { lv_obj_t * bg = lv_layer_top(); lv_obj_set_style_bg_color(bg, lv_color_hex(0x6f8af6), 0); lv_obj_set_style_text_color(bg, lv_color_white(), 0); lv_obj_set_style_bg_opa(bg, LV_OPA_COVER, 0); lv_obj_fade_in(bg, 400, 0); lv_obj_t * dsc = lv_label_create(bg); lv_obj_set_style_text_font(dsc, font_small, 0); lv_label_set_text(dsc, "The average FPS is"); lv_obj_align(dsc, LV_ALIGN_TOP_MID, 0, 90);
lv_obj_t * num = lv_label_create(bg); lv_obj_set_style_text_font(num, font_large, 0);#if LV_USE_PERF_MONITOR lv_label_set_text_fmt(num, "%ld", lv_refr_get_fps_avg());#endif lv_obj_align(num, LV_ALIGN_TOP_MID, 0, 120);
lv_obj_t * attr = lv_label_create(bg); lv_obj_set_style_text_align(attr, LV_TEXT_ALIGN_CENTER, 0); lv_obj_set_style_text_font(attr, font_small, 0);#if LV_DEMO_MUSIC_SQUARE || LV_DEMO_MUSIC_ROUND lv_label_set_text(attr, "Copyright 2020 LVGL Kft.\nwww.lvgl.io | lvgl@lvgl.io");#else lv_label_set_text(attr, "Copyright 2020 LVGL Kft. | www.lvgl.io | lvgl@lvgl.io");#endif lv_obj_align(attr, LV_ALIGN_BOTTOM_MID, 0, -10); break; } case 41: lv_scr_load(lv_obj_create(NULL)); _lv_demo_music_pause(); break; } state++;}
#endif /*LV_DEMO_MUSIC_AUTO_PLAY*/
#endif /*LV_USE_DEMO_MUSIC*/

执行如下

while(1)    {        lv_log_register_print_cb(my_print);
        lv_init();        s_disp1 = lv_port_disp1_init();        if(s_disp1 == (lv_disp_t*)0)        {            printf("disp1 init err\r\n");        }        s_disp2 = lv_port_disp2_init();        if(s_disp2 == (lv_disp_t*)0)        {            printf("disp2 init err\r\n");        }        lv_demo_music(s_disp1,s_disp2);        while(1)        {                   uint32_t delay = lv_timer_handler();            if (delay < 1) delay = 1;            os_delay(delay);        }        lv_deinit();    }

两份实例,所以lcd_conf.h中堆要增大,原来48k改为128k

#define LV_MEM_SIZE (128 * 1024U) /*[bytes]*/

效果见视频,

这里实际并没有修改完整,demo中用到的obj对象实例,都要根据两个显示器准备两份,绑定到不同的显示器,这里只作为演示就不花时间去全部修改了。

六. 总结

LVGL的多显示支持比较简单,只需要注册多个驱动,根据注册返回的lv_disp_t*, 用户即可根据该lv_disp_t* 来操作对象位于哪个显示器。


评论 (0)
  • ‌亥姆霍兹线圈‌是由两组相同的线圈组成,线圈之间的距离等于它们的半径。当电流同时流过这两个线圈时,会在它们中间形成一个几乎均匀的磁场。这种设计克服了普通线圈磁场不均匀的缺陷,能够在中心区域形成稳定、均匀的磁场‌。‌亥姆霍兹线圈的应用领域‌包括材料、电子、生物、医疗、航空航天、化学、应用物理等各个学科。由于其操作简便且能够提供极微弱的磁场直至数百高斯的磁场,亥姆霍兹线圈在各研究所、高等院校及企业中被广泛用于物质磁性或检测实验。‌亥姆霍兹线圈可以根据不同的标准进行分类‌:‌按磁场方向分类‌:‌一维亥
    锦正茂科技 2025-04-09 17:20 134浏览
  • 技术原理:非扫描式全局像的革新Flash激光雷达是一种纯固态激光雷达技术,其核心原理是通过面阵激光瞬时覆盖探测区域,配合高灵敏度传感器实现全局三维成像。其工作流程可分解为以下关键环节:1. 激光发射:采用二维点阵光源(如VCSEL垂直腔面发射激光器),通过光扩散器在单次脉冲中发射覆盖整个视场的面阵激光,视场角通常可达120°×75°,部分激光雷达产品可以做到120°×90°的超大视场角。不同于传统机械扫描或MEMS微振镜方案,Flash方案无需任何移动部件,直接通过电信号控制激光发射模式。2.
    robolab 2025-04-10 15:30 89浏览
  •     前几天同事问我,电压到多少伏就不安全了?考虑到这位同事的非电专业背景,我做了最极端的答复——多少伏都不安全,非专业人员别摸带电的东西。    那么,是不是这么绝对呢?我查了一下标准,奇怪的知识增加了。    标准的名字值得玩味——《电流对人和家畜的效应》,GB/T 13870.5 (IEC 60749-5)。里面对人、牛、尸体分类讨论(搞硬件的牛马一时恍惚,不知道自己算哪种)。    触电是电流造成的生理效应
    电子知识打边炉 2025-04-09 22:35 178浏览
  •   卫星故障预警系统软件:卫星在轨安全的智能护盾   北京华盛恒辉卫星故障预警系统软件,作为确保卫星在轨安全运行的关键利器,集成前沿的监测、诊断及预警技术,对卫星健康状况予以实时评估,提前预判潜在故障。下面将从核心功能、技术特性、应用场景以及发展走向等方面展开详尽阐述。   应用案例   目前,已有多个卫星故障预警系统在实际应用中取得了显著成效。例如,北京华盛恒辉和北京五木恒润卫星故障预警系统。这些成功案例为卫星故障预警系统的推广和应用提供了有力支持。   核心功能   实时状态监测:
    华盛恒辉l58ll334744 2025-04-09 19:49 159浏览
  • 政策驱动,AVAS成新能源车安全刚需随着全球碳中和目标的推进,新能源汽车产业迎来爆发式增长。据统计,2023年中国新能源汽车渗透率已突破35%,而欧盟法规明确要求2024年后新能效车型必须配备低速提示音系统(AVAS)。在此背景下,低速报警器作为车辆主动安全的核心组件,其技术性能直接关乎行人安全与法规合规性。基于WT2003H芯片开发的AVAS解决方案,以高可靠性、强定制化能力及智能场景适配特性,正成为行业技术升级的新标杆。WT2003H方案技术亮点解析全场景音效精准触发方案通过多传感器融合技术
    广州唯创电子 2025-04-10 08:53 188浏览
  • 由西门子(Siemens)生产的SIMATIC S7 PLC在SCADA 领域发挥着至关重要的作用。在众多行业中,SCADA 应用都需要与这些 PLC 进行通信。那么,有哪些高效可行的解决方案呢?宏集为您提供多种选择。传统方案:通过OPC服务器与西门子 PLC 间接通信SIMATIC S7系列的PLC是工业可编程控制器,能够实现对生产流程的实时SCADA监控,提供关于设备和流程状态的准确、最新数据。S7Comm(全称S7 Communication),也被称为工业以太网或Profinet,是西门
    宏集科技 2025-04-10 13:44 76浏览
  • 什么是车用高效能运算(Automotive HPC)?高温条件为何是潜在威胁?作为电动车内的关键核心组件,由于Automotive HPC(CPU)具备高频高效能运算电子组件、高速传输接口以及复杂运算处理、资源分配等诸多特性,再加上各种车辆的复杂应用情境等等条件,不难发见Automotive HPC对整个平台讯号传输实时处理、系统稳定度、耐久度、兼容性与安全性将造成多大的考验。而在各种汽车使用者情境之中,「高温条件」就是你我在日常生活中必然会面临到的一种潜在威胁。不论是长时间将车辆停放在室外的高
    百佳泰测试实验室 2025-04-10 15:09 71浏览
  • 文/Leon编辑/侯煜‍关税大战一触即发,当地时间4月9日起,美国开始对中国进口商品征收总计104%的关税。对此,中国外交部回应道:中方绝不接受美方极限施压霸道霸凌,将继续采取坚决有力措施,维护自身正当权益。同时,中国对原产于美国的进口商品加征关税税率,由34%提高至84%。随后,美国总统特朗普在社交媒体宣布,对中国关税立刻提高至125%,并暂缓其他75个国家对等关税90天,在此期间适用于10%的税率。特朗普政府挑起关税大战的目的,实际上是寻求制造业回流至美国。据悉,特朗普政府此次宣布对全球18
    华尔街科技眼 2025-04-10 16:39 95浏览
  • 行业痛点:电动车智能化催生语音交互刚需随着全球短途出行市场爆发式增长,中国电动自行车保有量已突破3.5亿辆。新国标实施推动行业向智能化、安全化转型,传统蜂鸣器报警方式因音效单一、缺乏场景适配性等问题,难以满足用户对智能交互体验的需求。WT2003HX系列语音芯片,以高性能处理器架构与灵活开发平台,为两轮电动车提供从基础报警到智能交互的全栈语音解决方案。WT2003HX芯片技术优势深度解读1. 高品质硬件性能,重塑语音交互标准搭载32位RISC处理器,主频高达120MHz,确保复杂算法流畅运行支持
    广州唯创电子 2025-04-10 09:12 163浏览
  • 背景近年来,随着国家对资源、能源有效利用率的要求越来越高,对环境保护和水处理的要求也越来越严格,因此有大量的固液分离问题需要解决。真空过滤器是是由负压形成真空过滤的固液分离机械。用过滤介质把容器分为上、下两层,利用负压,悬浮液加入上腔,在压力作用下通过过滤介质进入下腔成为滤液,悬浮液中的固体颗粒吸附在过滤介质表面形成滤饼,滤液穿过过滤介质经中心轴内部排出,达到固液分离的目的。目前市面上的过滤器多分为间歇操作和连续操作两种。间歇操作的真空过滤机可过滤各种浓度的悬浮液,连续操作的真空过滤机适于过滤含
    宏集科技 2025-04-10 13:45 70浏览
  •   海上电磁干扰训练系统:全方位解析      海上电磁干扰训练系统,作为模拟复杂海上电磁环境、锻炼人员应对电磁干扰能力的关键技术装备,在军事、科研以及民用等诸多领域广泛应用。接下来从系统构成、功能特点、技术原理及应用场景等方面展开详细解析。   应用案例   系统软件供应可以来这里,这个首肌开始是幺伍扒,中间是幺幺叁叁,最后一个是泗柒泗泗,按照数字顺序组合就可以找到。   一、系统构成   核心组件   电磁信号模拟设备:负责生成各类复杂的电磁信号,模拟海上多样
    华盛恒辉l58ll334744 2025-04-10 16:45 94浏览
  •   天空卫星健康状况监测维护管理系统:全方位解析  在航天技术迅猛发展的当下,卫星在轨运行的安全与可靠至关重要。整合多种技术,实现对卫星的实时监测、故障诊断、健康评估以及维护决策,有力保障卫星长期稳定运转。  应用案例       系统软件供应可以来这里,这个首肌开始是幺伍扒,中间是幺幺叁叁,最后一个是泗柒泗泗,按照数字顺序组合就可以找到。  一、系统架构与功能模块  数据采集层  数据处理层  智能分析层  决策支持层  二、关键技术  故障诊断技术  
    华盛恒辉l58ll334744 2025-04-10 15:46 63浏览
  • 行业变局:从机械仪表到智能交互终端的跃迁全球两轮电动车市场正经历从“功能机”向“智能机”的转型浪潮。数据显示,2024年智能电动车仪表盘渗透率已突破42%,而传统LED仪表因交互单一、扩展性差等问题,难以满足以下核心需求:适老化需求:35%中老年用户反映仪表信息辨识困难智能化缺口:78%用户期待仪表盘支持手机互联与语音交互成本敏感度:厂商需在15元以内BOM成本实现功能升级在此背景下,集成语音播报与蓝牙互联的WT2605C-32N芯片方案,以“极简设计+智能交互”重构仪表盘技术生态链。技术破局:
    广州唯创电子 2025-04-11 08:59 122浏览
我要评论
0
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦