基于ARM Cortex-A53与LVGL 9.0的智能车载系统设计与实现

目录

前言

[一、 系统架构与技术选型](#一、 系统架构与技术选型)

[二、 核心功能实现与个人技术贡献](#二、 核心功能实现与个人技术贡献)

[1. 驾驶模式:拟真仪表盘与多路音效并发管理](#1. 驾驶模式:拟真仪表盘与多路音效并发管理)

[2. 监测模式:多传感器高并发数据采集与线程安全](#2. 监测模式:多传感器高并发数据采集与线程安全)

[3. 娱乐模式:LRC音画精准同步(核心重难点)](#3. 娱乐模式:LRC音画精准同步(核心重难点))

[三、 难点攻坚与工程化思考](#三、 难点攻坚与工程化思考)

[四、 项目集成与团队管理](#四、 项目集成与团队管理)

五、核心控制代码

六、项目展示


前言

在本次电子信息工程专业的综合实践中,我作为项目组长与核心软件开发者 ,带领团队基于 GEC6818开发板(三星S5P6818,Cortex-A53架构) 与 嵌入式Linux 系统,从零设计并实现了一款集驾驶控制、环境监测与多媒体娱乐于一体的智能车载系统。

本项目不仅是对嵌入式Linux交叉开发全流程的深度实践,更是一次从底层驱动、多线程并发控制到上层高级GUI交互的完整工程闭环。本文将从系统架构、核心技术实现、难点攻坚等维度进行硬核复盘。

一、 系统架构与技术选型

项目以嵌入式Linux 为核心,采用轻量级、高性能的LVGL 9.0图形框架作为人机交互界面。

  • 硬件平台:GEC6818开发板,外接GY39气象传感器、MQ-2烟雾传感器、ATGM336H-5N GPS模块。

  • 软件技术栈:C语言、LVGL 9.0、Linux多线程编程(pthread)、多进程管理(fork/exec)、串口通信(Termios/Select)、NMEA协议解析、LRC时间轴同步算法。

整个系统划分为三大核心模式:驾驶模式 (速度控制与音效反馈)、监测模式 (多传感器并发采集与实时显示)、娱乐模式(音乐播放与歌词同步)。


二、 核心功能实现与个人技术贡献

作为项目的技术骨干,我主导了整体软件架构的设计、核心UI开发、多媒体模块以及最终的系统代码集成。

1. 驾驶模式:拟真仪表盘与多路音效并发管理
  • UI交互设计 :基于LVGL的 lv_arc 控件构建了双仪表盘,实时渲染当前车速与加速度。根据车速动态调整仪表盘颜色(绿-黄-红),实现超速预警,界面帧率稳定在60FPS以上。

  • 并发音效控制 :油门、刹车、喇叭音效需在极短时间内高频切换。我采用 fork() 子进程配合 execlp() 调用 madplay 播放器,并通过 kill() 发送系统信号(SIGSTOP/SIGCONT/SIGKILL)精细化管理音效生命周期。设计了播放器状态机与资源互锁机制,确保多路音效叠加时互不干扰,响应延迟严格控制在50ms以内。

2. 监测模式:多传感器高并发数据采集与线程安全
  • 非阻塞采集 :系统开启独立的 sensor_thread 线程处理底层硬件数据。通过 select() I/O多路复用机制实现串口超时读取,彻底解决阻塞式读取导致的系统卡死问题。

  • 协议解析 :针对GY39与GPS模块,编写了严格的数据帧头校验与解析算法;利用 nmea_get_field 对NMEA报文进行逗号分隔解析,并完成经纬度向标准度数的数学转换。

  • 线程安全(生产者-消费者模型) :引入 pthread_mutex_t 互斥锁保护全局数据结构。后台线程采集数据后加锁写入,LVGL定时器加锁读取并刷新UI,完美解决了多线程环境下的数据竞争与UI崩溃问题,确保了数据采集的精准与低延时。

3. 娱乐模式:LRC音画精准同步(核心重难点)
  • 系统不仅支持MP3播放,还实现了LRC歌词的实时联动。核心逻辑在于:通过 clock_gettime(CLOCK_MONOTONIC) 获取当前毫秒级高精度时间戳,将其与预解析的LRC时间轴数组进行**二分查找(时间复杂度O(logN))**比对,提取对应歌词。

  • 为保证非阻塞,采用独立的LVGL定时器轮询当前进度并高亮渲染当前行。同时处理了文件读取时的UTF-8/GBK编码及缺行异常过滤,实现了"所听即所见"的极致体验。


三、 难点攻坚与工程化思考

在开发过程中,我重点攻克了以下工程难题,这也是本系统稳定运行的基石:

  1. 多音效并发与系统资源抢占 :

    频繁触发音效极易导致音频解码卡顿和内存泄漏。我设计的解决方案是:利用操作系统层面的独立进程隔离,结合信号机制进行音效打断与平滑切换。将核心音效资源预加载至内存,极大降低了播放时的I/O延迟,确保了高负载下UI渲染与音频输出的双重流畅。

  2. UI刷新与后台数据采集的解耦 :

    早期方案中,传感器数据的轮询直接写在UI回调里,导致触摸响应迟钝。重构后,严格践行**"数据生产与UI消费分离"**的架构,底层线程专职采集与解析,UI层通过定时器安全拉取,极大提升了系统的鲁棒性与交互流畅度。


四、 项目集成与团队管理

作为项目组长,我不仅负责核心代码的编写,还承担了整个项目的技术统筹与进度管控:

  • 架构制定:设计底层硬件交互与上层UI逻辑的接口规范,确保模块间低耦合。

  • 代码审查与集成:负责最终软件代码的Review与Git仓库合并,排查解决多环境下的交叉编译依赖问题。

  • 全链路闭环:统筹安排硬件电路连接、文献资料梳理、PPT排版与答辩逻辑,协调各成员技术成果的无缝对接,确保系统在GEC6818开发板上一次跑通并稳定演示。

五、核心控制代码

复制代码
  /* LVGL 9.0 智能车载系统 */

  #include "lvgl/lvgl.h"
  #include <unistd.h>
  #include <pthread.h>
  #include <stdio.h>
  #include <stdlib.h>
  #include <stdint.h>
  #include <string.h>
  #include <fcntl.h>
  #include <termios.h>
  #include <sys/select.h>
  #include <sys/time.h>
  #include <time.h>
  #include <signal.h>
  #include <math.h>
  #include <sys/wait.h>

  #include "Chinese.h"   // 中文库

  /* UI 字符串 */
  #define CHINESE_FONT  &Chinese

  #define UI_TITLE      "传感器仪表盘"
  #define UI_ENV        "环境信息"
  #define UI_TEMP       "温度:"
  #define UI_HUMIDITY   "湿度:"
  #define UI_PRESSURE   "气压:"
  #define UI_LIGHT      "光照:"
  #define UI_SMOKE      "烟雾浓度"
  #define UI_GPS        "GPS 定位"
  #define UI_GPS_STATUS "状态:"
  #define UI_GPS_COORD  "坐标:"
  #define UI_GPS_TIME   "时间:"
  #define UI_VALID      "有效"
  #define UI_INVALID    "无效"

  #define UI_MAIN_TITLE  "小米SU7车载系统"
  #define UI_BTN_SPEED   "油门刹车控制"
  #define UI_BTN_SENSOR  "传感器仪表盘"
  #define UI_BTN_MUSIC   "音乐播放器"
  #define UI_BACK        "返回主菜单"
  #define UI_SPEED_TITLE "智能车载系统控制"
  #define UI_SPEED_VAL   "当前速度"
  #define UI_MUSIC_TITLE "音乐播放器"
  #define UI_PLAY        "播放"
  #define UI_PAUSE       "暂停"
  #define UI_PREV        "上一首"
  #define UI_NEXT        "下一首"

  /* 串口辅助 */
  static int read_byte_timeout(int fd, unsigned char *buf, int timeout_ms)
  {
      fd_set fds;
      struct timeval tv;
      FD_ZERO(&fds);
      FD_SET(fd, &fds);
      tv.tv_sec  = timeout_ms / 1000;
      tv.tv_usec = (timeout_ms % 1000) * 1000;
      int ret = select(fd + 1, &fds, NULL, NULL, &tv);
      if (ret > 0)
          return read(fd, buf, 1);
      return 0;
  }

  /* 串口初始化 */
  int uart_init(const char *uart_name)
  {
      int fd = open(uart_name, O_RDWR);
      if (fd == -1)
      {
          perror("open uart error");
          return -1;
      }

      struct termios myserial;
      memset(&myserial, 0, sizeof(myserial));

      myserial.c_cflag |= (CLOCAL | CREAD);
      myserial.c_cflag &= ~CSIZE;
      myserial.c_cflag &= ~CRTSCTS;
      myserial.c_cflag |= CS8;
      myserial.c_cflag &= ~CSTOPB;
      myserial.c_cflag &= ~PARENB;

      cfsetospeed(&myserial, B9600);
      cfsetispeed(&myserial, B9600);

      tcflush(fd, TCIFLUSH);
      tcsetattr(fd, TCSANOW, &myserial);

      return fd;
  }

  /* GY39 */
  unsigned char cmd_light[] = {0xA5, 0x81, 0x26};
  unsigned char cmd_tph[]   = {0xA5, 0x82, 0x27};

  typedef struct {
      unsigned int light;
      int temperature;
      unsigned int pressure;
      unsigned int humidity;
      int altitude;
  } GY39_Data;

  void gy39_send_cmd(int fd, unsigned char *cmd)
  {
      tcflush(fd, TCIFLUSH);
      write(fd, cmd, 3);
      usleep(200000);
  }

  int gy39_recv_frame(int fd, unsigned char *recvbuf, int bufsize)
  {
      int i = 0, flag = 0;
      while (1)
      {
          unsigned char buf = 0;
          int ret = read_byte_timeout(fd, &buf, 3000);
          if (ret <= 0) return -1;

          if (flag == 0 && buf == 0x5A) {
              recvbuf[0] = buf; i = 1; flag = 1;
          } else if (flag == 1 && buf == 0x5A) {
              recvbuf[1] = buf; i = 2; flag = 2;
          } else if (flag >= 2) {
              if (i < bufsize) recvbuf[i] = buf;
              i++;
          } else if (flag == 1 && buf != 0x5A) {
              flag = 0; i = 0;
          }

          if (flag >= 2 && i >= 3) {
              if (recvbuf[2] == 0x15 && i >= 9) return 0;
              if (recvbuf[2] == 0x45 && i >= 15) return 1;
          }
      }
  }

  int gy39_read_light(int fd, unsigned int *light)
  {
      unsigned char recvbuf[15] = {0};
      gy39_send_cmd(fd, cmd_light);
      int frame_type = gy39_recv_frame(fd, recvbuf, sizeof(recvbuf));
      if (frame_type < 0) return -1;
      if (frame_type == 1) {
          frame_type = gy39_recv_frame(fd, recvbuf, sizeof(recvbuf));
          if (frame_type < 0) return -1;
      }
      if (frame_type != 0) return -1;

      unsigned char checksum = 0;
      for (int j = 0; j < 8; j++) checksum += recvbuf[j];
      if (checksum != recvbuf[8]) return -1;

      *light = ((unsigned int)recvbuf[4] << 24) | (recvbuf[5] << 16) |
               (recvbuf[6] << 8) | recvbuf[7];
      *light /= 100;
      return 0;
  }

  int gy39_read_tph(int fd, GY39_Data *data)
  {
      unsigned char recvbuf[15] = {0};
      gy39_send_cmd(fd, cmd_tph);
      int frame_type = gy39_recv_frame(fd, recvbuf, sizeof(recvbuf));
      if (frame_type < 0) return -1;
      if (frame_type == 0) {
          frame_type = gy39_recv_frame(fd, recvbuf, sizeof(recvbuf));
          if (frame_type < 0) return -1;
      }
      if (frame_type != 1) return -1;

      unsigned char checksum = 0;
      for (int j = 0; j < 14; j++) checksum += recvbuf[j];
      if (checksum != recvbuf[14]) return -1;

      data->temperature = (recvbuf[4] << 8) | recvbuf[5];
      data->pressure    = ((unsigned int)recvbuf[6] << 24) | (recvbuf[7] << 16) |
                          (recvbuf[8] << 8) | recvbuf[9];
      data->humidity    = (recvbuf[10] << 8) | recvbuf[11];
      data->altitude    = (recvbuf[12] << 8) | recvbuf[13];
      return 0;
  }

  /* MQ-2 */
  unsigned char cmd_mq2[] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79};

  int mq2_read(int fd, int *smoke)
  {
      unsigned char recvbuf[9] = {0};
      int i = 0, flag = 0;
      tcflush(fd, TCIFLUSH);
      write(fd, cmd_mq2, 9);

      while (1) {
          unsigned char buf = 0;
          int ret = read_byte_timeout(fd, &buf, 2000);
          if (ret <= 0) return -1;
          if (buf == 0xFF) { flag = 1; i = 0; }
          if (flag == 1) {
              recvbuf[i++] = buf;
              if (i == 9) break;
          }
      }
      *smoke = (recvbuf[2] << 8) | recvbuf[3];
      return 0;
  }

  /* GPS */
  typedef struct {
      int valid;
      double latitude;
      double longitude;
      double altitude;
      double speed;
      int satellites;
      char time[12];
      char date[12];
  } GPS_Data;

  static int nmea_get_field(const char *sentence, int index, char *out, int outlen)
  {
      int field = 0, i = 0, j = 0;
      while (sentence[i] && sentence[i] != ',') i++;
      if (sentence[i] == ',') i++;
      while (sentence[i] && sentence[i] != '*') {
          if (field == index) {
              if (sentence[i] == ',') { out[j] = '\0'; return j; }
              if (j < outlen - 1) out[j++] = sentence[i];
          }
          if (sentence[i] == ',') field++;
          i++;
      }
      out[j] = '\0';
      return j;
  }

  static double nmea_to_degrees(const char *str, int lat)
  {
      double val = atof(str);
      int deg_width = lat ? 2 : 3;
      int deg = 0;
      char deg_str[4] = {0};
      strncpy(deg_str, str, deg_width);
      deg = atoi(deg_str);
      double minutes = atof(str + deg_width);
      return deg + minutes / 60.0;
  }

  static int parse_gprmc(const char *sentence, GPS_Data *gps)
  {
      char field[32];
      nmea_get_field(sentence, 1, field, sizeof(field));
      gps->valid = (field[0] == 'A') ? 1 : 0;
      nmea_get_field(sentence, 0, field, sizeof(field));
      if (strlen(field) >= 6) {
          snprintf(gps->time, sizeof(gps->time), "%c%c:%c%c:%c%c",
                   field[0], field[1], field[2], field[3], field[4], field[5]);
      }
      nmea_get_field(sentence, 2, field, sizeof(field));
      if (strlen(field) > 0) {
          gps->latitude = nmea_to_degrees(field, 1);
          nmea_get_field(sentence, 3, field, sizeof(field));
          if (field[0] == 'S') gps->latitude = -gps->latitude;
      }
      nmea_get_field(sentence, 4, field, sizeof(field));
      if (strlen(field) > 0) {
          gps->longitude = nmea_to_degrees(field, 0);
          nmea_get_field(sentence, 5, field, sizeof(field));
          if (field[0] == 'W') gps->longitude = -gps->longitude;
      }
      nmea_get_field(sentence, 6, field, sizeof(field));
      gps->speed = atof(field);
      nmea_get_field(sentence, 9, field, sizeof(field));
      if (strlen(field) >= 6) {
          snprintf(gps->date, sizeof(gps->date), "%c%c/%c%c/20%c%c",
                   field[0], field[1], field[2], field[3], field[4], field[5]);
      }
      return 0;
  }

  static int parse_gpgga(const char *sentence, GPS_Data *gps)
  {
      char field[32];
      nmea_get_field(sentence, 7, field, sizeof(field));
      gps->satellites = atoi(field);
      nmea_get_field(sentence, 9, field, sizeof(field));
      gps->altitude = atof(field);
      return 0;
  }

  int gps_read(int fd, GPS_Data *gps)
  {
      char line[128];
      int li = 0, got_rmc = 0, got_gga = 0;
      memset(gps, 0, sizeof(GPS_Data));
      while (!(got_rmc && got_gga)) {
          unsigned char ch = 0;
          int ret = read_byte_timeout(fd, &ch, 5000);
          if (ret <= 0) return -1;
          if (ch == '$') {
              li = 0;
              line[li++] = ch;
          } else if (li > 0 && li < sizeof(line) - 1) {
              line[li++] = ch;
              if (ch == '\n') {
                  line[li] = '\0';
                  if (strstr(line, "$GPRMC") || strstr(line, "$GNRMC")) {
                      parse_gprmc(line, gps);
                      got_rmc = 1;
                  } else if (strstr(line, "$GPGGA") || strstr(line, "$GNGGA")) {
                      parse_gpgga(line, gps);
                      got_gga = 1;
                  }
                  li = 0;
              }
          }
      }
      return 0;
  }

  /* 全局数据 */
  typedef struct {
      pthread_mutex_t mutex;
      unsigned int light;
      int temperature;
      unsigned int pressure;
      unsigned int humidity;
      int altitude;
      int smoke;
      GPS_Data gps;
      int gy39_ok;
      int mq2_ok;
      int gps_ok;
  } SensorData;

  static SensorData g_sensor_data = {0};

  /* LVGL 控件 */
  static lv_obj_t *temp_label;
  static lv_obj_t *humidity_label;
  static lv_obj_t *pressure_label;
  static lv_obj_t *light_label;
  static lv_obj_t *smoke_label;
  static lv_obj_t *smoke_bar;           // 改为进度条控件
  static lv_obj_t *gps_status_label;
  static lv_obj_t *gps_coord_label;
  static lv_obj_t *gps_time_label;

  #define SMOKE_MAX 1000

  /* 多界面 */
  static lv_obj_t *screen_main;
  static lv_obj_t *screen_speed;
  static lv_obj_t *screen_sensor;
  static lv_obj_t *screen_music;
  static lv_obj_t *screen_playlist;

  /* 速度界面 */
  static lv_obj_t *speed_value_label;
  static lv_obj_t *speed_arc;
  static lv_obj_t *accel_value_label;
  static lv_obj_t *accel_arc;
  static int current_speed = 0;

  /* 音乐播放器 */
  static int current_song = 0;          // 0, 1, 2
  static int music_playing = 0;         // 0=暂停, 1=播放
  static pid_t music_pid = -1;          // madplay 进程 PID
  static lv_obj_t *song_label;
  static lv_obj_t *play_btn_label;
  static lv_obj_t *lrc_label_prev;
  static lv_obj_t *lrc_label_curr;
  static lv_obj_t *lrc_label_next;
  static const char *song_files[] = {"./1.mp3", "./2.mp3", "./3.mp3"};
  static const char *song_names[] = {"晴天", "第十幕", "恋人"};

  /* 传感器线程 */
  static void *sensor_thread_func(void *arg)
  {
      int gy39_fd = uart_init("/dev/ttySAC1");
      int mq2_fd  = uart_init("/dev/ttySAC2");
      int gps_fd  = uart_init("/dev/ttySAC3");
      if (gy39_fd < 0 || mq2_fd < 0 || gps_fd < 0) {
          fprintf(stderr, "UART init failed\n");
          return NULL;
      }

      while (1) {
          SensorData local;
          pthread_mutex_lock(&g_sensor_data.mutex);
          local = g_sensor_data;
          pthread_mutex_unlock(&g_sensor_data.mutex);

          if (gy39_read_light(gy39_fd, &local.light) == 0)
              local.gy39_ok = 1;
          else
              local.gy39_ok = 0;

          GY39_Data gy39_tph;
          if (gy39_read_tph(gy39_fd, &gy39_tph) == 0) {
              local.temperature = gy39_tph.temperature;
              local.pressure    = gy39_tph.pressure;
              local.humidity    = gy39_tph.humidity;
              local.altitude    = gy39_tph.altitude;
              local.gy39_ok     = 1;
          } else {
              local.gy39_ok = 0;
          }

          if (mq2_read(mq2_fd, &local.smoke) == 0)
              local.mq2_ok = 1;
          else
              local.mq2_ok = 0;

          if (gps_read(gps_fd, &local.gps) == 0) {
              local.gps_ok = 1;
              printf("GPS: valid=%d lat=%.6f lon=%.6f sat=%d time=%s\n",
                     local.gps.valid, local.gps.latitude, local.gps.longitude,
                     local.gps.satellites, local.gps.time);
          } else {
              local.gps_ok = 0;
              printf("GPS: read failed\n");
          }

          pthread_mutex_lock(&g_sensor_data.mutex);
          memcpy(&g_sensor_data, &local, sizeof(SensorData));
          pthread_mutex_unlock(&g_sensor_data.mutex);

          sleep(1);
      }

      close(gy39_fd);
      close(mq2_fd);
      close(gps_fd);
      return NULL;
  }

  /* UI 更新 */
  static void ui_update_timer(lv_timer_t *timer)
  {
      if (lv_screen_active() != screen_sensor) return;  /* 只在传感器界面更新 */
      SensorData data;
      pthread_mutex_lock(&g_sensor_data.mutex);
      data = g_sensor_data;
      pthread_mutex_unlock(&g_sensor_data.mutex);

      char buf[64];

      if (data.gy39_ok) {
          snprintf(buf, sizeof(buf), "%.2f °C", data.temperature / 100.0);
          lv_label_set_text(temp_label, buf);
          snprintf(buf, sizeof(buf), "%.2f %%", data.humidity / 100.0);
          lv_label_set_text(humidity_label, buf);
          snprintf(buf, sizeof(buf), "%.2f kPa", data.pressure / 1000.0);
          lv_label_set_text(pressure_label, buf);
          snprintf(buf, sizeof(buf), "%u lux", data.light);
          lv_label_set_text(light_label, buf);
      } else {
          lv_label_set_text(temp_label, "-- °C");
          lv_label_set_text(humidity_label, "-- %%");
          lv_label_set_text(pressure_label, "-- kPa");
          lv_label_set_text(light_label, "-- lux");
      }

      /* 烟雾浓度:更新进度条和数值标签 */
      if (data.mq2_ok) {
          snprintf(buf, sizeof(buf), "%d", data.smoke);
          lv_label_set_text(smoke_label, buf);
          lv_bar_set_value(smoke_bar, data.smoke, LV_ANIM_ON);
      } else {
          lv_label_set_text(smoke_label, "--");
          lv_bar_set_value(smoke_bar, 0, LV_ANIM_ON);
      }

      if (data.gps_ok && data.gps.valid) {
          snprintf(buf, sizeof(buf), "%s\n%.6f, %.6f", UI_VALID, data.gps.latitude, data.gps.longitude);
          lv_label_set_text(gps_coord_label, buf);
          snprintf(buf, sizeof(buf), "%s  %s", data.gps.time, data.gps.date);
          lv_label_set_text(gps_time_label, buf);
          lv_label_set_text(gps_status_label, UI_VALID);
      } else {
          lv_label_set_text(gps_coord_label, UI_INVALID);
          lv_label_set_text(gps_time_label, "--:--:--  --/--/--");
          lv_label_set_text(gps_status_label, UI_INVALID);
      }
  }

  /* 音乐播放 */
  static void lrc_load_for_song(int song_index);
  static void music_timer_start(void);
  static void music_timer_pause(void);
  static void music_timer_reset(void);

  static void music_stop(void)
  {
      if (music_pid > 0) {
          kill(music_pid, SIGKILL);
          waitpid(music_pid, NULL, 0);
          music_pid = -1;
      }
      music_timer_pause();
      music_playing = 0;
  }

  static void music_play(int song_index)
  {
      music_stop();
      lrc_load_for_song(song_index);
      music_timer_reset();
      music_pid = fork();
      if (music_pid == 0) {
          /* 子进程:播放音乐 */
          execlp("madplay", "madplay", "-Q", song_files[song_index], NULL);
          perror("execlp madplay failed");  /* exec 失败时打印原因 */
          _exit(1);
      }
      if (music_pid < 0) {
          perror("fork failed");
      } else {
          printf("Playing: %s (pid=%d)\n", song_files[song_index], music_pid);
      }
      music_playing = 1;
      current_song = song_index;
  }

  static void music_toggle_pause(void)
  {
      if (music_pid <= 0) return;
      if (music_playing) {
          kill(music_pid, SIGSTOP);
          music_timer_pause();
          music_playing = 0;
      } else {
          kill(music_pid, SIGCONT);
          music_timer_start();
          music_playing = 1;
      }
  }

  /* LRC 歌词 */
  #define LRC_MAX_LINES 128

  typedef struct {
      int time_ms;    /* 时间戳(毫秒) */
      char *text;     /* 歌词文本 */
  } LrcLine;

  static LrcLine  lrc_lines[LRC_MAX_LINES];
  static int      lrc_count = 0;
  static int      lrc_current_line = -1;

  /* 播放时间追踪 */
  static struct timespec play_start_time;
  static int             play_accumulated_ms = 0;  /* 暂停前累计的毫秒数 */

  static void lrc_clear(void)
  {
      for (int i = 0; i < lrc_count; i++) {
          free(lrc_lines[i].text);
          lrc_lines[i].text = NULL;
      }
      lrc_count = 0;
      lrc_current_line = -1;
  }

  /* 解析 LRC 文件,path 为完整路径(如 "./1.lrc") */
  static void lrc_load(const char *path)
  {
      lrc_clear();
      FILE *fp = fopen(path, "r");
      if (!fp) {
          printf("LRC not found: %s\n", path);
          return;
      }

      char line[256];
      while (fgets(line, sizeof(line), fp) && lrc_count < LRC_MAX_LINES) {
          /* 解析 [mm:ss.xx] 格式 */
          int min, sec, ms;
          if (sscanf(line, "[%d:%d.%d]", &min, &sec, &ms) >= 2) {
              int time_ms = min * 60000 + sec * 1000 + ms * 10;
              /* 找到 ] 后面的歌词文本 */
              char *p = strchr(line, ']');
              if (p) {
                  p++;
                  while (*p == ' ') p++;
                  /* 去掉换行符 */
                  size_t len = strlen(p);
                  while (len > 0 && (p[len-1] == '\n' || p[len-1] == '\r'))
                      p[--len] = '\0';
                  if (len > 0) {
                      /* 只保留汉字和ASCII字母数字(过滤所有标点符号) */
                      char clean[256];
                      int ci = 0;
                      unsigned char *u = (unsigned char *)p;
                      while (*u && ci < (int)sizeof(clean) - 4) {
                          if ((*u >= 'A' && *u <= 'Z') || (*u >= 'a' && *u <= 'z')
                              || (*u >= '0' && *u <= '9') || *u == ' ') {
                              clean[ci++] = *u++;
                          } else if (*u >= 0xE4 && *u <= 0xE9 && u[1] >= 0x80 && u[2] >= 0x80) {
                              /* UTF-8 汉字(3字节),保留 */
                              clean[ci++] = *u++;
                              clean[ci++] = *u++;
                              clean[ci++] = *u++;
                          } else {
                              u++;
                          }
                      }
                      clean[ci] = '\0';
                      if (ci > 0) {
                          lrc_lines[lrc_count].time_ms = time_ms;
                          lrc_lines[lrc_count].text = strdup(clean);
                          lrc_count++;
                      }
                  }
              }
          }
      }
      fclose(fp);
      printf("LRC loaded: %d lines from %s\n", lrc_count, path);
  }

  /* 根据 mp3 文件名自动生成 lrc 路径并加载 */
  static void lrc_load_for_song(int song_index)
  {
      char lrc_path[256];
      const char *mp3 = song_files[song_index];
      snprintf(lrc_path, sizeof(lrc_path), "%s", mp3);
      /* 将 .mp3 后缀替换为 .lrc */
      size_t len = strlen(lrc_path);
      if (len > 4 && strcmp(lrc_path + len - 4, ".mp3") == 0)
          strcpy(lrc_path + len - 4, ".lrc");
      lrc_load(lrc_path);
  }

  /* 获取当前播放时间(毫秒) */
  static int music_get_playback_ms(void)
  {
      if (!music_playing) return play_accumulated_ms;
      struct timespec now;
      clock_gettime(CLOCK_MONOTONIC, &now);
      int elapsed = (now.tv_sec - play_start_time.tv_sec) * 1000
                  + (now.tv_nsec - play_start_time.tv_nsec) / 1000000;
      return play_accumulated_ms + elapsed;
  }

  /* 开始计时(播放时调用) */
  static void music_timer_start(void)
  {
      clock_gettime(CLOCK_MONOTONIC, &play_start_time);
  }

  /* 暂停计时(暂停时调用) */
  static void music_timer_pause(void)
  {
      if (music_playing) {
          struct timespec now;
          clock_gettime(CLOCK_MONOTONIC, &now);
          play_accumulated_ms += (now.tv_sec - play_start_time.tv_sec) * 1000
                               + (now.tv_nsec - play_start_time.tv_nsec) / 1000000;
      }
  }

  /* 重置计时(切歌时调用) */
  static void music_timer_reset(void)
  {
      play_accumulated_ms = 0;
      clock_gettime(CLOCK_MONOTONIC, &play_start_time);
  }

  /* 二分查找当前歌词行 */
  static int lrc_find_line(int time_ms)
  {
      int lo = 0, hi = lrc_count - 1, result = -1;
      while (lo <= hi) {
          int mid = (lo + hi) / 2;
          if (lrc_lines[mid].time_ms <= time_ms) {
              result = mid;
              lo = mid + 1;
          } else {
              hi = mid - 1;
          }
      }
      return result;
  }

  /* 音效播放 */
  static pid_t sfx_pid = -1;

  static void sfx_stop(void)
  {
      if (sfx_pid > 0) {
          kill(sfx_pid, SIGKILL);
          waitpid(sfx_pid, NULL, 0);
          sfx_pid = -1;
      }
  }

  /* 播放一次音效(不循环) */
  static void sfx_play_once(const char *file)
  {
      sfx_stop();
      sfx_pid = fork();
      if (sfx_pid == 0) {
          execlp("madplay", "madplay", "-Q", file, NULL);
          _exit(1);
      }
  }

  /* 循环播放音效(-z 参数) */
  static void sfx_play_loop(const char *file)
  {
      sfx_stop();
      sfx_pid = fork();
      if (sfx_pid == 0) {
          execlp("madplay", "madplay", "-Q", "-z", file, NULL);
          _exit(1);
      }
  }

  /* 事件回调 */

  /* 主菜单按钮回调 */
  static void btn_go_speed_cb(lv_event_t *e)
  {
      (void)e;
      lv_screen_load(screen_speed);
  }

  static void btn_go_sensor_cb(lv_event_t *e)
  {
      (void)e;
      lv_screen_load(screen_sensor);
  }

  static void btn_go_music_cb(lv_event_t *e)
  {
      (void)e;
      lv_screen_load(screen_music);
  }

  /* 通用返回主菜单回调 */
  static void btn_back_cb(lv_event_t *e)
  {
      (void)e;
      sfx_stop();
      lv_screen_load(screen_main);
  }

  /* 速度控制 */
  static lv_timer_t *speed_timer = NULL;
  static int speed_step = 0;       /* >0 = 油门, <0 = 刹车 */

  /* 更新仪表盘显示(弧形 + 数值 + 颜色) */
  static void speed_display_update(void)
  {
      /* 左侧:速度仪表 */
      char buf[16];
      snprintf(buf, sizeof(buf), "%d", current_speed);
      lv_label_set_text(speed_value_label, buf);
      lv_arc_set_value(speed_arc, current_speed);

      lv_color_t c;
      if (current_speed < 80)
          c = lv_color_hex(0x00ff88);
      else if (current_speed < 140)
          c = lv_color_hex(0xffcc00);
      else
          c = lv_color_hex(0xff3333);
      lv_obj_set_style_arc_color(speed_arc, c, LV_PART_INDICATOR);
      lv_obj_set_style_text_color(speed_value_label, c, 0);

      /* 右侧:加速度仪表 */
      int accel = speed_step;  /* 正=加速, 负=刹车, 0=无操作 */
      snprintf(buf, sizeof(buf), "%+d", accel);
      lv_label_set_text(accel_value_label, buf);
      /* 加速度映射到 0-40 范围供弧形显示 */
      int arc_val = abs(accel);
      if (arc_val > 30) arc_val = 30;
      lv_arc_set_value(accel_arc, arc_val);

      lv_color_t ac;
      if (accel > 0)
          ac = lv_color_hex(0x00ff88);      /* 加速:绿 */
      else if (accel < 0)
          ac = lv_color_hex(0xff3333);      /* 刹车:红 */
      else
          ac = lv_color_hex(0x7f8c8d);      /* 无操作:灰 */
      lv_obj_set_style_arc_color(accel_arc, ac, LV_PART_INDICATOR);
      lv_obj_set_style_text_color(accel_value_label, ac, 0);
  }

  static int btn_held = 0;  /* 1=油门按住, -1=刹车按住, 0=没按 */

  static void speed_timer_cb(lv_timer_t *timer)
  {
      if (lv_screen_active() != screen_speed) return;  /* 只在速度界面更新 */
      if (btn_held != 0) {
          /* 按住油门/刹车:步长逐渐递增,最大30 */
          if (abs(speed_step) < 30)
              speed_step += btn_held;
          current_speed += speed_step;
      } else {
          /* 没按键:加速度缓慢归零 */
          if (speed_step > 0) speed_step--;
          else if (speed_step < 0) speed_step++;
          /* 速度自然减速 */
          if (current_speed > 0) current_speed -= 1;
      }
      if (current_speed > 200) current_speed = 200;
      if (current_speed < 0)   current_speed = 0;
      speed_display_update();
      /* 速度和加速度都为0时停止定时器 */
      if (current_speed == 0 && speed_step == 0) {
          if (speed_timer) { lv_timer_delete(speed_timer); speed_timer = NULL; }
      }
  }

  static void btn_accel_press_cb(lv_event_t *e)
  {
      (void)e;
      sfx_play_once("./jiasu.mp3");
      btn_held = 1;
      speed_step = 1;
      current_speed += 1;
      if (current_speed > 200) current_speed = 200;
      speed_display_update();
      if (speed_timer == NULL)
          speed_timer = lv_timer_create(speed_timer_cb, 300, NULL);
  }

  static void btn_accel_release_cb(lv_event_t *e)
  {
      (void)e;
      sfx_stop();
      btn_held = 0;
      /* 定时器保留,让加速度和速度自然归零 */
  }

  static void btn_brake_press_cb(lv_event_t *e)
  {
      (void)e;
      if (current_speed == 0) {
          speed_step = 0;
          speed_display_update();
          return;
      }
      sfx_play_once("./jiansu.mp3");
      btn_held = -1;
      speed_step = -5;
      current_speed -= 5;
      if (current_speed < 0) current_speed = 0;
      speed_display_update();
      if (speed_timer == NULL)
          speed_timer = lv_timer_create(speed_timer_cb, 300, NULL);
  }

  static void btn_brake_release_cb(lv_event_t *e)
  {
      (void)e;
      sfx_stop();
      btn_held = 0;
      /* 定时器保留,让加速度和速度自然归零 */
  }

  /* 音乐播放器回调 */
  static void btn_play_pause_cb(lv_event_t *e)
  {
      (void)e;
      if (music_pid <= 0) {
          /* 未在播放,开始播放当前歌曲 */
          music_play(current_song);
      } else {
          /* 切换暂停/恢复 */
          music_toggle_pause();
      }
      lv_label_set_text(play_btn_label, music_playing ? UI_PAUSE : UI_PLAY);
  }

  static void btn_prev_cb(lv_event_t *e)
  {
      (void)e;
      int prev = (current_song + 2) % 3;
      music_play(prev);
      lv_label_set_text(song_label, song_names[current_song]);
      lv_label_set_text(play_btn_label, UI_PAUSE);
  }

  static void btn_next_cb(lv_event_t *e)
  {
      (void)e;
      int next = (current_song + 1) % 3;
      music_play(next);
      lv_label_set_text(song_label, song_names[current_song]);
      lv_label_set_text(play_btn_label, UI_PAUSE);
  }

  /* 进入音乐界面时停止播放的回调(返回主菜单时停止) */
  static void btn_back_music_cb(lv_event_t *e)
  {
      (void)e;
      music_stop();
      lv_label_set_text(play_btn_label, UI_PLAY);
      lv_label_set_text(lrc_label_prev, "");
      lv_label_set_text(lrc_label_curr, "");
      lv_label_set_text(lrc_label_next, "");
      lv_screen_load(screen_main);
  }

  /* 歌词同步定时器 */
  static void lyrics_timer_cb(lv_timer_t *timer)
  {
      if (lv_screen_active() != screen_music) return;  /* 只在音乐界面更新 */
      if (lrc_count == 0 || !music_playing) return;

      int ms = music_get_playback_ms();
      int line = lrc_find_line(ms);

      if (line == lrc_current_line) return;  /* 行没变,不更新 */
      lrc_current_line = line;

      /* 更新上一行 */
      if (line > 0)
          lv_label_set_text(lrc_label_prev, lrc_lines[line - 1].text);
      else
          lv_label_set_text(lrc_label_prev, "");

      /* 更新当前行 */
      if (line >= 0)
          lv_label_set_text(lrc_label_curr, lrc_lines[line].text);
      else
          lv_label_set_text(lrc_label_curr, "");

      /* 更新下一行 */
      if (line >= 0 && line + 1 < lrc_count)
          lv_label_set_text(lrc_label_next, lrc_lines[line + 1].text);
      else
          lv_label_set_text(lrc_label_next, "");
  }

  /* 喇叭按钮回调 */
  static void btn_horn_press_cb(lv_event_t *e)
  {
      (void)e;
      sfx_play_once("./laba.mp3");
  }

  static void btn_horn_release_cb(lv_event_t *e)
  {
      (void)e;
      sfx_stop();
  }

  /* 主菜单 */

  static void create_main_menu(void)
  {
      screen_main = lv_obj_create(NULL);
      lv_obj_set_style_bg_color(screen_main, lv_color_hex(0x0d1b2a), 0);
      lv_obj_set_style_bg_opa(screen_main, LV_OPA_COVER, 0);

      /* 标题居中放大 */
      lv_obj_t *title = lv_label_create(screen_main);
      lv_label_set_text(title, UI_MAIN_TITLE);
      lv_obj_set_style_text_font(title, CHINESE_FONT, 0);
      lv_obj_set_style_text_color(title, lv_color_hex(0x00d4ff), 0);
      lv_obj_set_style_text_letter_space(title, 12, 0);
      lv_obj_set_style_text_line_space(title, 4, 0);
      lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 20);

      /* 副标题 */
      lv_obj_t *subtitle = lv_label_create(screen_main);
      lv_label_set_text(subtitle, "Xiaomi SU7 Car System");
      lv_obj_set_style_text_font(subtitle, &lv_font_montserrat_28, 0);
      lv_obj_set_style_text_color(subtitle, lv_color_hex(0x7f8c8d), 0);
      lv_obj_align(subtitle, LV_ALIGN_TOP_MID, 0, 70);

      /* 按钮样式 */
      static lv_style_t btn_style;
      lv_style_init(&btn_style);
      lv_style_set_bg_color(&btn_style, lv_color_hex(0x1a3a5c));
      lv_style_set_bg_opa(&btn_style, LV_OPA_COVER);
      lv_style_set_radius(&btn_style, 20);
      lv_style_set_border_color(&btn_style, lv_color_hex(0x00d4ff));
      lv_style_set_border_width(&btn_style, 2);
      lv_style_set_text_color(&btn_style, lv_color_hex(0xffffff));

      /* 按钮1: 油门刹车控制 */
      lv_obj_t *btn1 = lv_btn_create(screen_main);
      lv_obj_add_style(btn1, &btn_style, 0);
      lv_obj_set_size(btn1, 320, 80);
      lv_obj_align(btn1, LV_ALIGN_CENTER, 0, -75);
      lv_obj_t *label1 = lv_label_create(btn1);
      lv_label_set_text(label1, UI_BTN_SPEED);
      lv_obj_set_style_text_font(label1, CHINESE_FONT, 0);
      lv_obj_center(label1);
      lv_obj_add_event_cb(btn1, btn_go_speed_cb, LV_EVENT_CLICKED, NULL);

      /* 按钮2: 传感器仪表盘 */
      lv_obj_t *btn2 = lv_btn_create(screen_main);
      lv_obj_add_style(btn2, &btn_style, 0);
      lv_obj_set_size(btn2, 320, 80);
      lv_obj_align(btn2, LV_ALIGN_CENTER, 0, 15);
      lv_obj_t *label2 = lv_label_create(btn2);
      lv_label_set_text(label2, UI_BTN_SENSOR);
      lv_obj_set_style_text_font(label2, CHINESE_FONT, 0);
      lv_obj_center(label2);
      lv_obj_add_event_cb(btn2, btn_go_sensor_cb, LV_EVENT_CLICKED, NULL);

      /* 按钮3: 音乐播放器 */
      lv_obj_t *btn3 = lv_btn_create(screen_main);
      lv_obj_add_style(btn3, &btn_style, 0);
      lv_obj_set_size(btn3, 320, 80);
      lv_obj_align(btn3, LV_ALIGN_CENTER, 0, 105);
      lv_obj_t *label3 = lv_label_create(btn3);
      lv_label_set_text(label3, UI_BTN_MUSIC);
      lv_obj_set_style_text_font(label3, CHINESE_FONT, 0);
      lv_obj_center(label3);
      lv_obj_add_event_cb(btn3, btn_go_music_cb, LV_EVENT_CLICKED, NULL);
  }

  /* 速度控制界面 */
  /* 辅助:创建半圆弧形仪表盘 */
  static lv_obj_t *create_gauge_arc(lv_obj_t *parent, int x_off, int y_off,
                                     int range, lv_color_t bg_c, lv_color_t ind_c)
  {
      lv_obj_t *arc = lv_arc_create(parent);
      lv_obj_set_size(arc, 250, 250);
      lv_obj_align(arc, LV_ALIGN_CENTER, x_off, y_off);
      lv_arc_set_rotation(arc, 180);           /* 起始角度:左侧(开口朝下) */
      lv_arc_set_bg_angles(arc, 0, 270);
      lv_arc_set_value(arc, 0);
      lv_arc_set_range(arc, 0, range);
      lv_obj_remove_style(arc, NULL, LV_PART_KNOB);
      lv_obj_clear_flag(arc, LV_OBJ_FLAG_CLICKABLE);
      lv_obj_set_style_arc_color(arc, bg_c, LV_PART_MAIN);
      lv_obj_set_style_arc_width(arc, 16, LV_PART_MAIN);
      lv_obj_set_style_arc_color(arc, ind_c, LV_PART_INDICATOR);
      lv_obj_set_style_arc_width(arc, 16, LV_PART_INDICATOR);
      return arc;
  }

  /* 刻度数字:从左侧(0) 顺时针到右侧(最大值),开口朝下 */
  static void create_gauge_ticks(lv_obj_t *parent, int x_off, int y_off,
                                  const char **texts, int count)
  {
      for (int i = 0; i < count; i++) {
          int angle = 180 - i * (270 / (count - 1));  /* 180°起,顺时针递减 */
          double rad = angle * 3.14159 / 180.0;
          int r = 148;
          int x = (int)(x_off + r * cos(rad));
          int y = (int)(y_off - r * sin(rad));
          lv_obj_t *tick = lv_label_create(parent);
          lv_label_set_text(tick, texts[i]);
          lv_obj_set_style_text_font(tick, &lv_font_montserrat_16, 0);
          lv_obj_set_style_text_color(tick, lv_color_hex(0xaaaaaa), 0);
          lv_obj_align(tick, LV_ALIGN_CENTER, x, y);
      }
  }

  static void create_speed_screen(void)
  {
      screen_speed = lv_obj_create(NULL);
      lv_obj_set_style_bg_color(screen_speed, lv_color_hex(0x0d1b2a), 0);
      lv_obj_set_style_bg_opa(screen_speed, LV_OPA_COVER, 0);

      /* 标题 */
      lv_obj_t *title = lv_label_create(screen_speed);
      lv_label_set_text(title, UI_SPEED_TITLE);
      lv_obj_set_style_text_font(title, CHINESE_FONT, 0);
      lv_obj_set_style_text_color(title, lv_color_hex(0x00d4ff), 0);
      lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 10);

      /* 左侧速度仪表 */
      speed_arc = create_gauge_arc(screen_speed, -200, -40, 200,
                                    lv_color_hex(0x2c3e50), lv_color_hex(0x00ff88));

      static const char *speed_ticks[] = {"0", "40", "80", "120", "160", "200"};
      create_gauge_ticks(screen_speed, -200, -40, speed_ticks, 6);

      speed_value_label = lv_label_create(screen_speed);
      lv_label_set_text(speed_value_label, "0");
      lv_obj_set_style_text_font(speed_value_label, &lv_font_montserrat_48, 0);
      lv_obj_set_style_text_color(speed_value_label, lv_color_hex(0x00ff88), 0);
      lv_obj_align(speed_value_label, LV_ALIGN_CENTER, -200, -45);

      lv_obj_t *speed_unit = lv_label_create(screen_speed);
      lv_label_set_text(speed_unit, "km/h");
      lv_obj_set_style_text_font(speed_unit, &lv_font_montserrat_16, 0);
      lv_obj_set_style_text_color(speed_unit, lv_color_hex(0x7f8c8d), 0);
      lv_obj_align(speed_unit, LV_ALIGN_CENTER, -200, -10);

      lv_obj_t *speed_lbl = lv_label_create(screen_speed);
      lv_label_set_text(speed_lbl, UI_SPEED_VAL);
      lv_obj_set_style_text_font(speed_lbl, CHINESE_FONT, 0);
      lv_obj_set_style_text_color(speed_lbl, lv_color_hex(0x00d4ff), 0);
      lv_obj_align(speed_lbl, LV_ALIGN_CENTER, -200, -80);

      /* 右侧加速度仪表 */
      accel_arc = create_gauge_arc(screen_speed, 200, -40, 30,
                                    lv_color_hex(0x2c3e50), lv_color_hex(0x00ff88));

      static const char *accel_ticks[] = {"0", "6", "12", "18", "24", "30"};
      create_gauge_ticks(screen_speed, 200, -40, accel_ticks, 5);

      accel_value_label = lv_label_create(screen_speed);
      lv_label_set_text(accel_value_label, "+0");
      lv_obj_set_style_text_font(accel_value_label, &lv_font_montserrat_48, 0);
      lv_obj_set_style_text_color(accel_value_label, lv_color_hex(0x7f8c8d), 0);
      lv_obj_align(accel_value_label, LV_ALIGN_CENTER, 200, -45);

      lv_obj_t *accel_unit = lv_label_create(screen_speed);
      lv_label_set_text(accel_unit, "step");
      lv_obj_set_style_text_font(accel_unit, &lv_font_montserrat_16, 0);
      lv_obj_set_style_text_color(accel_unit, lv_color_hex(0x7f8c8d), 0);
      lv_obj_align(accel_unit, LV_ALIGN_CENTER, 200, -10);

      lv_obj_t *accel_lbl = lv_label_create(screen_speed);
      lv_label_set_text(accel_lbl, "Accel");
      lv_obj_set_style_text_font(accel_lbl, &lv_font_montserrat_16, 0);
      lv_obj_set_style_text_color(accel_lbl, lv_color_hex(0x00d4ff), 0);
      lv_obj_align(accel_lbl, LV_ALIGN_CENTER, 200, -80);

      /* 底部按钮 */
      lv_obj_t *btn_acc = lv_btn_create(screen_speed);
      lv_obj_set_size(btn_acc, 130, 50);
      lv_obj_align(btn_acc, LV_ALIGN_BOTTOM_MID, -170, -20);
      lv_obj_set_style_bg_color(btn_acc, lv_color_hex(0x27ae60), 0);
      lv_obj_set_style_radius(btn_acc, 12, 0);
      lv_obj_t *acc_label = lv_label_create(btn_acc);
      lv_label_set_text(acc_label, "油门 +");
      lv_obj_set_style_text_font(acc_label, CHINESE_FONT, 0);
      lv_obj_center(acc_label);
      lv_obj_add_event_cb(btn_acc, btn_accel_press_cb, LV_EVENT_PRESSED, NULL);
      lv_obj_add_event_cb(btn_acc, btn_accel_release_cb, LV_EVENT_RELEASED, NULL);
      lv_obj_add_event_cb(btn_acc, btn_accel_release_cb, LV_EVENT_PRESS_LOST, NULL);

      /* 喇叭按钮(油门和刹车中间) */
      lv_obj_t *btn_horn = lv_btn_create(screen_speed);
      lv_obj_set_size(btn_horn, 100, 50);
      lv_obj_align(btn_horn, LV_ALIGN_BOTTOM_MID, 0, -20);
      lv_obj_set_style_bg_color(btn_horn, lv_color_hex(0xf39c12), 0);
      lv_obj_set_style_radius(btn_horn, 12, 0);
      lv_obj_t *horn_lbl = lv_label_create(btn_horn);
      lv_label_set_text(horn_lbl, "喇叭");
      lv_obj_set_style_text_font(horn_lbl, CHINESE_FONT, 0);
      lv_obj_set_style_text_color(horn_lbl, lv_color_hex(0xffffff), 0);
      lv_obj_center(horn_lbl);
      lv_obj_add_event_cb(btn_horn, btn_horn_press_cb, LV_EVENT_PRESSED, NULL);
      lv_obj_add_event_cb(btn_horn, btn_horn_release_cb, LV_EVENT_RELEASED, NULL);
      lv_obj_add_event_cb(btn_horn, btn_horn_release_cb, LV_EVENT_PRESS_LOST, NULL);

      lv_obj_t *btn_brk = lv_btn_create(screen_speed);
      lv_obj_set_size(btn_brk, 130, 50);
      lv_obj_align(btn_brk, LV_ALIGN_BOTTOM_MID, 170, -20);
      lv_obj_set_style_bg_color(btn_brk, lv_color_hex(0xe74c3c), 0);
      lv_obj_set_style_radius(btn_brk, 12, 0);
      lv_obj_t *brk_label = lv_label_create(btn_brk);
      lv_label_set_text(brk_label, "刹车 -");
      lv_obj_set_style_text_font(brk_label, CHINESE_FONT, 0);
      lv_obj_center(brk_label);
      lv_obj_add_event_cb(btn_brk, btn_brake_press_cb, LV_EVENT_PRESSED, NULL);
      lv_obj_add_event_cb(btn_brk, btn_brake_release_cb, LV_EVENT_RELEASED, NULL);
      lv_obj_add_event_cb(btn_brk, btn_brake_release_cb, LV_EVENT_PRESS_LOST, NULL);

      lv_obj_t *btn_back = lv_btn_create(screen_speed);
      lv_obj_set_size(btn_back, 160, 40);
      lv_obj_align(btn_back, LV_ALIGN_TOP_LEFT, 10, 5);
      lv_obj_set_style_bg_color(btn_back, lv_color_hex(0x7f8c8d), 0);
      lv_obj_set_style_radius(btn_back, 8, 0);
      lv_obj_t *back_label = lv_label_create(btn_back);
      lv_label_set_text(back_label, UI_BACK);
      lv_obj_set_style_text_font(back_label, CHINESE_FONT, 0);
      lv_obj_center(back_label);
      lv_obj_add_event_cb(btn_back, btn_back_cb, LV_EVENT_CLICKED, NULL);
  }

  /* 传感器界面 */
  static void create_sensor_screen(void)
  {
      screen_sensor = lv_obj_create(NULL);
      lv_obj_set_style_bg_color(screen_sensor, lv_color_hex(0x1a2a3a), 0);
      lv_obj_set_style_bg_opa(screen_sensor, LV_OPA_COVER, 0);

      lv_obj_t *title = lv_label_create(screen_sensor);
      lv_label_set_text(title, UI_TITLE);
      lv_obj_set_style_text_font(title, CHINESE_FONT, 0);
      lv_obj_set_style_text_color(title, lv_color_hex(0xffffff), 0);
      lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 10);

      /* ---------- 返回按钮 ---------- */
      lv_obj_t *btn_back = lv_btn_create(screen_sensor);
      lv_obj_set_size(btn_back, 160, 40);
      lv_obj_align(btn_back, LV_ALIGN_TOP_LEFT, 10, 5);
      lv_obj_set_style_bg_color(btn_back, lv_color_hex(0x7f8c8d), 0);
      lv_obj_set_style_radius(btn_back, 8, 0);
      lv_obj_t *back_label = lv_label_create(btn_back);
      lv_label_set_text(back_label, UI_BACK);
      lv_obj_set_style_text_font(back_label, CHINESE_FONT, 0);
      lv_obj_center(back_label);
      lv_obj_add_event_cb(btn_back, btn_back_cb, LV_EVENT_CLICKED, NULL);

      /* ---------- 环境卡片 ---------- */
      lv_obj_t *env_card = lv_obj_create(screen_sensor);
      lv_obj_set_size(env_card, 280, 240);
      lv_obj_align(env_card, LV_ALIGN_TOP_LEFT, 10, 50);
      lv_obj_set_style_bg_color(env_card, lv_color_hex(0x2c3e50), 0);
      lv_obj_set_style_radius(env_card, 12, 0);
      lv_obj_set_style_border_color(env_card, lv_color_hex(0xffffff), 0);
      lv_obj_set_style_border_width(env_card, 2, 0);

      lv_obj_t *env_title = lv_label_create(env_card);
      lv_label_set_text(env_title, UI_ENV);
      lv_obj_set_style_text_font(env_title, CHINESE_FONT, 0);
      lv_obj_set_style_text_color(env_title, lv_color_hex(0xecf0f1), 0);
      lv_obj_align(env_title, LV_ALIGN_TOP_LEFT, 10, 10);

      lv_obj_t *temp_unit = lv_label_create(env_card);
      lv_label_set_text(temp_unit, UI_TEMP);
      lv_obj_set_style_text_font(temp_unit, CHINESE_FONT, 0);
      lv_obj_align(temp_unit, LV_ALIGN_TOP_LEFT, 10, 50);
      temp_label = lv_label_create(env_card);
      lv_obj_set_style_text_font(temp_label, &lv_font_montserrat_20, 0);
      lv_obj_align_to(temp_label, temp_unit, LV_ALIGN_OUT_RIGHT_MID, 10, 0);

      lv_obj_t *hum_unit = lv_label_create(env_card);
      lv_label_set_text(hum_unit, UI_HUMIDITY);
      lv_obj_set_style_text_font(hum_unit, CHINESE_FONT, 0);
      lv_obj_align(hum_unit, LV_ALIGN_TOP_LEFT, 10, 90);
      humidity_label = lv_label_create(env_card);
      lv_obj_set_style_text_font(humidity_label, &lv_font_montserrat_20, 0);
      lv_obj_align_to(humidity_label, hum_unit, LV_ALIGN_OUT_RIGHT_MID, 10, 0);

      lv_obj_t *pres_unit = lv_label_create(env_card);
      lv_label_set_text(pres_unit, UI_PRESSURE);
      lv_obj_set_style_text_font(pres_unit, CHINESE_FONT, 0);
      lv_obj_align(pres_unit, LV_ALIGN_TOP_LEFT, 10, 130);
      pressure_label = lv_label_create(env_card);
      lv_obj_set_style_text_font(pressure_label, &lv_font_montserrat_20, 0);
      lv_obj_align_to(pressure_label, pres_unit, LV_ALIGN_OUT_RIGHT_MID, 10, 0);

      lv_obj_t *light_unit = lv_label_create(env_card);
      lv_label_set_text(light_unit, UI_LIGHT);
      lv_obj_set_style_text_font(light_unit, CHINESE_FONT, 0);
      lv_obj_align(light_unit, LV_ALIGN_TOP_LEFT, 10, 170);
      light_label = lv_label_create(env_card);
      lv_obj_set_style_text_font(light_label, &lv_font_montserrat_20, 0);
      lv_obj_align_to(light_label, light_unit, LV_ALIGN_OUT_RIGHT_MID, 10, 0);

      /* ---------- 烟雾卡片 ---------- */
      lv_obj_t *smoke_card = lv_obj_create(screen_sensor);
      lv_obj_set_size(smoke_card, 280, 240);
      lv_obj_align(smoke_card, LV_ALIGN_TOP_RIGHT, -10, 50);
      lv_obj_set_style_bg_color(smoke_card, lv_color_hex(0x2c3e50), 0);
      lv_obj_set_style_radius(smoke_card, 12, 0);
      lv_obj_set_style_border_color(smoke_card, lv_color_hex(0xffffff), 0);
      lv_obj_set_style_border_width(smoke_card, 2, 0);

      lv_obj_t *smoke_title = lv_label_create(smoke_card);
      lv_label_set_text(smoke_title, UI_SMOKE);
      lv_obj_set_style_text_font(smoke_title, CHINESE_FONT, 0);
      lv_obj_set_style_text_color(smoke_title, lv_color_hex(0xecf0f1), 0);
      lv_obj_align(smoke_title, LV_ALIGN_TOP_LEFT, 10, 10);

      smoke_bar = lv_bar_create(smoke_card);
      lv_obj_set_size(smoke_bar, 200, 30);
      lv_obj_align(smoke_bar, LV_ALIGN_TOP_LEFT, 10, 50);
      lv_bar_set_range(smoke_bar, 0, SMOKE_MAX);
      lv_bar_set_value(smoke_bar, 0, LV_ANIM_OFF);
      lv_obj_set_style_bg_color(smoke_bar, lv_color_hex(0x444444), LV_PART_MAIN);
      lv_obj_set_style_bg_color(smoke_bar, lv_color_hex(0xff5722), LV_PART_INDICATOR);

      smoke_label = lv_label_create(smoke_card);
      lv_obj_set_style_text_font(smoke_label, &lv_font_montserrat_24, 0);
      lv_obj_set_style_text_color(smoke_label, lv_color_hex(0xffaa66), 0);
      lv_obj_align_to(smoke_label, smoke_bar, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);

      lv_obj_t *range_label = lv_label_create(smoke_card);
      lv_label_set_text(range_label, "0 - 1000");
      lv_obj_set_style_text_color(range_label, lv_color_hex(0xaaaaaa), 0);
      lv_obj_align_to(range_label, smoke_bar, LV_ALIGN_OUT_RIGHT_MID, 10, 0);

      /* ---------- GPS 卡片 ---------- */
      lv_obj_t *gps_card = lv_obj_create(screen_sensor);
      lv_obj_set_size(gps_card, lv_obj_get_width(screen_sensor) - 20, 150);
      lv_obj_align(gps_card, LV_ALIGN_TOP_MID, 0, 300);
      lv_obj_set_style_bg_color(gps_card, lv_color_hex(0x2c3e50), 0);
      lv_obj_set_style_radius(gps_card, 12, 0);
      lv_obj_set_style_border_color(gps_card, lv_color_hex(0xffffff), 0);
      lv_obj_set_style_border_width(gps_card, 3, 0);

      lv_obj_t *gps_title = lv_label_create(gps_card);
      lv_label_set_text(gps_title, UI_GPS);
      lv_obj_set_style_text_font(gps_title, CHINESE_FONT, 0);
      lv_obj_set_style_text_color(gps_title, lv_color_hex(0xecf0f1), 0);
      lv_obj_align(gps_title, LV_ALIGN_TOP_LEFT, 10, 10);

      lv_obj_t *gps_status_text = lv_label_create(gps_card);
      lv_label_set_text(gps_status_text, UI_GPS_STATUS);
      lv_obj_set_style_text_font(gps_status_text, CHINESE_FONT, 0);
      lv_obj_align(gps_status_text, LV_ALIGN_TOP_LEFT, 10, 45);
      gps_status_label = lv_label_create(gps_card);
      lv_obj_align_to(gps_status_label, gps_status_text, LV_ALIGN_OUT_RIGHT_MID, 10, 0);

      lv_obj_t *gps_coord_text = lv_label_create(gps_card);
      lv_label_set_text(gps_coord_text, UI_GPS_COORD);
      lv_obj_set_style_text_font(gps_coord_text, CHINESE_FONT, 0);
      lv_obj_align(gps_coord_text, LV_ALIGN_TOP_LEFT, 10, 85);
      gps_coord_label = lv_label_create(gps_card);
      lv_obj_set_style_text_font(gps_coord_label, &lv_font_montserrat_16, 0);
      lv_obj_align_to(gps_coord_label, gps_coord_text, LV_ALIGN_OUT_RIGHT_MID, 10, 0);

      lv_obj_t *gps_time_text = lv_label_create(gps_card);
      lv_label_set_text(gps_time_text, UI_GPS_TIME);
      lv_obj_set_style_text_font(gps_time_text, CHINESE_FONT, 0);
      lv_obj_align(gps_time_text, LV_ALIGN_TOP_RIGHT, -120, 45);
      gps_time_label = lv_label_create(gps_card);
      lv_obj_align_to(gps_time_label, gps_time_text, LV_ALIGN_OUT_RIGHT_MID, 10, 0);
  }

  /* 音乐界面 */
  static void btn_go_playlist_cb(lv_event_t *e);

  static void create_music_screen(void)
  {
      screen_music = lv_obj_create(NULL);
      lv_obj_set_style_bg_color(screen_music, lv_color_hex(0x0d1b2a), 0);
      lv_obj_set_style_bg_opa(screen_music, LV_OPA_COVER, 0);

      /* 标题 */
      lv_obj_t *title = lv_label_create(screen_music);
      lv_label_set_text(title, UI_MUSIC_TITLE);
      lv_obj_set_style_text_font(title, CHINESE_FONT, 0);
      lv_obj_set_style_text_color(title, lv_color_hex(0x00d4ff), 0);
      lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 20);

      /* 音乐图标区(大号文字代替图标) */
      lv_obj_t *music_icon = lv_label_create(screen_music);
      lv_label_set_text(music_icon, LV_SYMBOL_AUDIO);
      lv_obj_set_style_text_font(music_icon, &lv_font_montserrat_48, 0);
      lv_obj_set_style_text_color(music_icon, lv_color_hex(0x00d4ff), 0);
      lv_obj_align(music_icon, LV_ALIGN_CENTER, 0, -110);

      /* 当前歌曲名 */
      song_label = lv_label_create(screen_music);
      lv_label_set_text(song_label, song_names[0]);
      lv_obj_set_style_text_font(song_label, CHINESE_FONT, 0);
      lv_obj_set_style_text_color(song_label, lv_color_hex(0xffffff), 0);
      lv_obj_align(song_label, LV_ALIGN_CENTER, 0, -55);

      /* ---------- 歌词显示(3 行:上一行、当前行、下一行) ---------- */
      lrc_label_prev = lv_label_create(screen_music);
      lv_label_set_text(lrc_label_prev, "");
      lv_obj_set_style_text_font(lrc_label_prev, CHINESE_FONT, 0);
      lv_obj_set_style_text_color(lrc_label_prev, lv_color_hex(0x666666), 0);
      lv_obj_align(lrc_label_prev, LV_ALIGN_CENTER, 0, -25);

      lrc_label_curr = lv_label_create(screen_music);
      lv_label_set_text(lrc_label_curr, "");
      lv_obj_set_style_text_font(lrc_label_curr, CHINESE_FONT, 0);
      lv_obj_set_style_text_color(lrc_label_curr, lv_color_hex(0x00d4ff), 0);
      lv_obj_set_style_text_letter_space(lrc_label_curr, 2, 0);
      lv_obj_align(lrc_label_curr, LV_ALIGN_CENTER, 0, 5);

      lrc_label_next = lv_label_create(screen_music);
      lv_label_set_text(lrc_label_next, "");
      lv_obj_set_style_text_font(lrc_label_next, CHINESE_FONT, 0);
      lv_obj_set_style_text_color(lrc_label_next, lv_color_hex(0x666666), 0);
      lv_obj_align(lrc_label_next, LV_ALIGN_CENTER, 0, 35);

      /* ---------- 控制按钮 ---------- */
      /* 上一首 */
      lv_obj_t *btn_prev = lv_btn_create(screen_music);
      lv_obj_set_size(btn_prev, 80, 50);
      lv_obj_align(btn_prev, LV_ALIGN_CENTER, -100, 80);
      lv_obj_set_style_bg_color(btn_prev, lv_color_hex(0x1a3a5c), 0);
      lv_obj_set_style_radius(btn_prev, 12, 0);
      lv_obj_set_style_border_color(btn_prev, lv_color_hex(0x00d4ff), 0);
      lv_obj_set_style_border_width(btn_prev, 2, 0);
      lv_obj_t *prev_label = lv_label_create(btn_prev);
      lv_label_set_text(prev_label, LV_SYMBOL_PREV);
      lv_obj_set_style_text_font(prev_label, &lv_font_montserrat_24, 0);
      lv_obj_center(prev_label);
      lv_obj_add_event_cb(btn_prev, btn_prev_cb, LV_EVENT_CLICKED, NULL);

      /* 播放/暂停 */
      lv_obj_t *btn_play = lv_btn_create(screen_music);
      lv_obj_set_size(btn_play, 80, 50);
      lv_obj_align(btn_play, LV_ALIGN_CENTER, 0, 80);
      lv_obj_set_style_bg_color(btn_play, lv_color_hex(0x27ae60), 0);
      lv_obj_set_style_radius(btn_play, 12, 0);
      play_btn_label = lv_label_create(btn_play);
      lv_label_set_text(play_btn_label, UI_PLAY);
      lv_obj_set_style_text_font(play_btn_label, CHINESE_FONT, 0);
      lv_obj_center(play_btn_label);
      lv_obj_add_event_cb(btn_play, btn_play_pause_cb, LV_EVENT_CLICKED, NULL);

      /* 下一首 */
      lv_obj_t *btn_next = lv_btn_create(screen_music);
      lv_obj_set_size(btn_next, 80, 50);
      lv_obj_align(btn_next, LV_ALIGN_CENTER, 100, 80);
      lv_obj_set_style_bg_color(btn_next, lv_color_hex(0x1a3a5c), 0);
      lv_obj_set_style_radius(btn_next, 12, 0);
      lv_obj_set_style_border_color(btn_next, lv_color_hex(0x00d4ff), 0);
      lv_obj_set_style_border_width(btn_next, 2, 0);
      lv_obj_t *next_label = lv_label_create(btn_next);
      lv_label_set_text(next_label, LV_SYMBOL_NEXT);
      lv_obj_set_style_text_font(next_label, &lv_font_montserrat_24, 0);
      lv_obj_center(next_label);
      lv_obj_add_event_cb(btn_next, btn_next_cb, LV_EVENT_CLICKED, NULL);

      /* 返回按钮(音乐界面专用,会停止播放) */
      lv_obj_t *btn_back = lv_btn_create(screen_music);
      lv_obj_set_size(btn_back, 160, 40);
      lv_obj_align(btn_back, LV_ALIGN_TOP_LEFT, 10, 5);
      lv_obj_set_style_bg_color(btn_back, lv_color_hex(0x7f8c8d), 0);
      lv_obj_set_style_radius(btn_back, 8, 0);
      lv_obj_t *back_label = lv_label_create(btn_back);
      lv_label_set_text(back_label, UI_BACK);
      lv_obj_set_style_text_font(back_label, CHINESE_FONT, 0);
      lv_obj_center(back_label);
      lv_obj_add_event_cb(btn_back, btn_back_music_cb, LV_EVENT_CLICKED, NULL);

      /* 歌单按钮 */
      lv_obj_t *btn_playlist = lv_btn_create(screen_music);
      lv_obj_set_size(btn_playlist, 120, 40);
      lv_obj_align(btn_playlist, LV_ALIGN_TOP_RIGHT, -10, 5);
      lv_obj_set_style_bg_color(btn_playlist, lv_color_hex(0x1a3a5c), 0);
      lv_obj_set_style_radius(btn_playlist, 8, 0);
      lv_obj_set_style_border_color(btn_playlist, lv_color_hex(0x00d4ff), 0);
      lv_obj_set_style_border_width(btn_playlist, 1, 0);
      lv_obj_t *playlist_lbl = lv_label_create(btn_playlist);
      lv_label_set_text(playlist_lbl, "歌单");
      lv_obj_set_style_text_font(playlist_lbl, CHINESE_FONT, 0);
      lv_obj_center(playlist_lbl);
      lv_obj_add_event_cb(btn_playlist, btn_go_playlist_cb, LV_EVENT_CLICKED, NULL);
  }

  /* 歌单界面 */
  static void btn_go_playlist_cb(lv_event_t *e)
  {
      (void)e;
      lv_screen_load(screen_playlist);
  }

  static void btn_back_playlist_cb(lv_event_t *e)
  {
      (void)e;
      lv_screen_load(screen_music);
  }

  /* 点击歌曲卡片回调 */
  static void btn_song_click_cb(lv_event_t *e)
  {
      int idx = (int)(intptr_t)lv_event_get_user_data(e);
      music_play(idx);
      lv_label_set_text(song_label, song_names[current_song]);
      lv_label_set_text(play_btn_label, UI_PAUSE);
      lv_screen_load(screen_music);
  }

  static void create_playlist_screen(void)
  {
      screen_playlist = lv_obj_create(NULL);
      lv_obj_set_style_bg_color(screen_playlist, lv_color_hex(0x0d1b2a), 0);
      lv_obj_set_style_bg_opa(screen_playlist, LV_OPA_COVER, 0);

      /* 标题 */
      lv_obj_t *title = lv_label_create(screen_playlist);
      lv_label_set_text(title, "歌单");
      lv_obj_set_style_text_font(title, CHINESE_FONT, 0);
      lv_obj_set_style_text_color(title, lv_color_hex(0x00d4ff), 0);
      lv_obj_set_style_text_letter_space(title, 6, 0);
      lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 20);

      /* 歌曲信息 */
      typedef struct { const char *name; const char *artist; } SongInfo;
      static const SongInfo songs[] = {
          {"晴天",     "周杰伦"},
          {"第十幕",   "迪丽热巴"},
          {"恋人",     "李荣浩"},
      };

      int card_y = 70;
      for (int i = 0; i < 3; i++) {
          /* 歌曲卡片(可点击) */
          lv_obj_t *card = lv_btn_create(screen_playlist);
          lv_obj_set_size(card, 420, 80);
          lv_obj_align(card, LV_ALIGN_TOP_MID, 0, card_y);
          lv_obj_set_style_bg_color(card, lv_color_hex(0x1a3a5c), 0);
          lv_obj_set_style_radius(card, 12, 0);
          lv_obj_set_style_border_color(card, lv_color_hex(0x00d4ff), 0);
          lv_obj_set_style_border_width(card, 1, 0);
          lv_obj_add_event_cb(card, btn_song_click_cb, LV_EVENT_CLICKED, (void *)(intptr_t)i);

          /* 序号 */
          lv_obj_t *num = lv_label_create(card);
          char num_buf[8];
          snprintf(num_buf, sizeof(num_buf), "%d.", i + 1);
          lv_label_set_text(num, num_buf);
          lv_obj_set_style_text_font(num, &lv_font_montserrat_28, 0);
          lv_obj_set_style_text_color(num, lv_color_hex(0x00d4ff), 0);
          lv_obj_align(num, LV_ALIGN_LEFT_MID, 10, 0);

          /* 歌名 */
          lv_obj_t *name = lv_label_create(card);
          lv_label_set_text(name, songs[i].name);
          lv_obj_set_style_text_font(name, CHINESE_FONT, 0);
          lv_obj_set_style_text_color(name, lv_color_hex(0xffffff), 0);
          lv_obj_align(name, LV_ALIGN_LEFT_MID, 55, -15);

          /* 歌手 */
          lv_obj_t *artist = lv_label_create(card);
          lv_label_set_text(artist, songs[i].artist);
          lv_obj_set_style_text_font(artist, CHINESE_FONT, 0);
          lv_obj_set_style_text_color(artist, lv_color_hex(0x7f8c8d), 0);
          lv_obj_align(artist, LV_ALIGN_LEFT_MID, 55, 15);

          card_y += 95;
      }

      /* 返回按钮(回到音乐播放界面) */
      lv_obj_t *btn_back = lv_btn_create(screen_playlist);
      lv_obj_set_size(btn_back, 160, 40);
      lv_obj_align(btn_back, LV_ALIGN_TOP_LEFT, 10, 5);
      lv_obj_set_style_bg_color(btn_back, lv_color_hex(0x7f8c8d), 0);
      lv_obj_set_style_radius(btn_back, 8, 0);
      lv_obj_t *back_label = lv_label_create(btn_back);
      lv_label_set_text(back_label, UI_BACK);
      lv_obj_set_style_text_font(back_label, CHINESE_FONT, 0);
      lv_obj_center(back_label);
      lv_obj_add_event_cb(btn_back, btn_back_playlist_cb, LV_EVENT_CLICKED, NULL);
  }

  /* 主函数 */
  int main(void)
  {
      lv_init();

      lv_display_t *disp = lv_linux_fbdev_create();
      lv_linux_fbdev_set_file(disp, "/dev/fb0");
      lv_indev_t *indev = lv_evdev_create(LV_INDEV_TYPE_POINTER, "/dev/input/event0");

      /* 创建所有界面 */
      create_main_menu();
      create_speed_screen();
      create_sensor_screen();
      create_music_screen();
      create_playlist_screen();

      /* 显示主菜单 */
      lv_screen_load(screen_main);

      /* 启动传感器采集线程 */
      pthread_mutex_init(&g_sensor_data.mutex, NULL);
      pthread_t sensor_tid;
      pthread_create(&sensor_tid, NULL, sensor_thread_func, NULL);

      /* 注册 UI 更新定时器 */
      lv_timer_create(ui_update_timer, 200, NULL);
      lv_timer_create(lyrics_timer_cb, 200, NULL);

      while (1) {
          lv_timer_handler();
          usleep(5000);
      }

      music_stop();
      pthread_mutex_destroy(&g_sensor_data.mutex);
      return 0;
  }

六、项目展示

车载系统

相关推荐
a5986522 小时前
ARM服务器部署Docker云手机,多开实例性能实测
服务器·arm开发·docker·云手机
a5986523 小时前
ARM服务器搭建云手机教程|原生ARM架构,消除指令转译损耗
arm开发
云勤通5 小时前
智慧后勤系统哪家售后响应快
车载系统
云勤通6 小时前
智慧后勤系统哪个品牌靠谱
车载系统
XMAIPC_Robot1 天前
ARM+FPGA+AI船舶/工业自动化控制器EMC测试等级解析(IEC60533/IEC60945)
arm开发·人工智能·fpga开发·zynq·arm+fpga·rk3588+fpga
奔跑的架构师2 天前
[A-51]ARMv9/v8-SCMI系统架构
linux·arm开发·架构·系统架构·arm
玄芯散人2 天前
【元婴·94】ARM vs RISC-V大战
arm开发·risc-v
szxinmai主板定制专家2 天前
ZYNQ PL+PS 架构拆解|工业自动化与电力物联网高速数据采集、预处理与协议上云
arm开发·人工智能·物联网·fpga开发·数据采集卡·rk3576
szxinmai主板定制专家2 天前
告别 ARM 实时性短板:基于 ZYNQ 的电力故障录波 + 工业振动数据采集系统设计
arm开发·人工智能·fpga开发·zynq·数据采集卡·arm+fpga·rk3588+fpga