K510 开发记录:通用 CMake 交叉编译 + DRM 显示测试

文章目录

前言

在上一篇完成了交叉编译环境的配置后,下一步我准备配置一个通用的开发工程

首先说一下我的开发方式

开发环境及开发方式

windows下,使用vscode+copilot开发,我并没有sh@[toc]

前言

在上一篇完成了交叉编译环境的配置后,下一步我准备配置一个通用的开发工程首先说一下我的开发方式# 开发环境及开发方式windows下,使用vscode+copilot开发,我并没有直接使用remote wsl,原因很简单,ubuntu20下,copilot无法连接到服务器,所以选择直接打开wsl文件,同时在容器下挂载wsl2。

实现路径为

windows下修改代码 -- 容器下编译 -- wsl2下实现scp/ssh等命令 -- 板子上运行验证

交叉编译工具链确认:gcc、sysroot、可用性

在容器内(我这里是官方 k510_env)先确认工具链存在且可用:

bash 复制代码
TC=/opt/k510_buildroot/toolchain/nds64le-linux-glibc-v5d

$TC/bin/riscv64-linux-gcc -dumpmachine
$TC/bin/riscv64-linux-gcc -print-sysroot
$TC/bin/riscv64-linux-gcc -v 2>&1 | head -10

我这里输出的关键信息类似:

  • Target: riscv64-linux
  • gcc version 7.3.0 ...
  • sysroot:
    /opt/k510_buildroot/toolchain/nds64le-linux-glibc-v5d/.../sysroot

另外,我也确认了 buildroot 生成的 sysroot(后面编译 DRM 会用到):

bash 复制代码
BR_SYSROOT=/opt/k510_buildroot/k510_crb_lp3_v1_2_defconfig/host/riscv64-buildroot-linux-gnu/sysroot
test -d "$BR_SYSROOT" && echo "BR_SYSROOT OK: $BR_SYSROOT"

cmake

我第一次准备 cmake -S -B 的时候,直接报:

bash 复制代码
bash: cmake: command not found

这其实很常见:很多编译镜像只保证 SDK 能跑,不保证你"现代工程"那套工具齐全。

解决思路只有一个:在容器里装 cmake。

如果容器里有包管理器(apt/yum),最直接:

bash 复制代码
apt-get update
apt-get install -y cmake make ninja-build pkg-config

如果镜像没有 apt,那就只能:

  • 用官方提供脚本/方式装,或
  • 改用 "开发型镜像",或
  • 自己做一个基于该镜像的 Dockerfile,把 cmake 装进去

我的原则:一旦 CMake 跑起来,后面就全是正收益。


写一个通用 toolchain-k510.cmake(以后所有项目复用)

我在 /opt/apps/cmake/ 放了一个通用 toolchain 文件:toolchain-k510.cmake,之后所有项目都能复用。

cmake 复制代码
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR riscv64)

set(TOOLCHAIN_DIR "/opt/k510_buildroot/toolchain/nds64le-linux-glibc-v5d")

set(CMAKE_C_COMPILER   "${TOOLCHAIN_DIR}/bin/riscv64-linux-gcc")
set(CMAKE_CXX_COMPILER "${TOOLCHAIN_DIR}/bin/riscv64-linux-g++")

# 工具链自带 sysroot
set(CMAKE_SYSROOT "${TOOLCHAIN_DIR}/riscv64-linux/sysroot")

# 让 CMake 在 sysroot 内找头文件/库
set(CMAKE_FIND_ROOT_PATH "${CMAKE_SYSROOT}")
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

# 避免 try_run
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

之后所有开发测试,我都可以直接使用cmake配置


用 CMake 编译第一个程序并上板验证

把之前的hello.c放到单独的hello目录下,同时改名为main.c

之后创建CMakeLists.txt

cmake 复制代码
cmake_minimum_required(VERSION 3.16)
project(hello_k510 C)
add_executable(hello_k510 main.c)

编译:

bash 复制代码
cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=/opt/apps/cmake/toolchain-k510.cmake
cmake --build build -j$(nproc)

拷贝到板子运行(此处 IP 按你的实际替换):

bash 复制代码
scp build/hello_k510 root@192.168.0.104:/root/

板子:

sh 复制代码
chmod +x /root/hello_k510
/root/hello_k510
# Hello, World!

UI学习

之后,我准备学习UI部分开发,最开始准备用/dev/fb0,但是很可惜,板子上并没有

sh 复制代码
ls -l /dev/fb*
# ls: /dev/fb*: No such file or directory

[root@canaan ~ ]$ cat /proc/fb 2>/dev/null || true
[root@canaan ~ ]$

但另一方面,DRM 是存在的:

sh 复制代码
[root@canaan ~ ]$ ls -l /dev/dri
total 0
crw-rw----    1 root     root      226,   0 Jan  1  1970 card0
[root@canaan ~ ]$

并且系统里有一个 demo 在跑(典型是摄像头到屏幕):

sh 复制代码
ps -ef | grep -iE "weston|wayland|demo|camera|vo|drm" | grep -v grep
#  192 root  ... ./v4l2_drm.out -f video_drm_1920x1080.conf ...

通过AI,我写了一个最基础的刷屏程序,执行路径是

  • 打开 /dev/dri/card0
  • 找 connector / mode
  • 创建 dumb buffer + addFB
  • drmModeSetCrtc() 接管显示
  • mmap 后直接写像素(比如全屏填充某个颜色)
    这一步不追求复杂 UI,只追求一个事实: 我能从应用层直接控制屏幕输出(DRM)
    贴上drm_color.c的代码
c 复制代码
/*
 * DRM 颜色测试程序
 * 功能:使用 DRM (Direct Rendering Manager) 接口直接控制显示输出,
 *       在屏幕上显示交替闪烁的蓝色和红色画面
 */

#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>

#include <drm/drm.h>
#include <drm/drm_mode.h>
#include <xf86drm.h>
#include <xf86drmMode.h>

/*
 * die - 错误处理函数
 * @msg: 错误信息字符串
 * 
 * 功能:打印错误信息并退出程序
 * 返回值:无(程序直接退出)
 */
static void die(const char* msg) { perror(msg); exit(1); }

/*
 * make_xrgb - 生成 XRGB8888 格式的颜色值
 * @r: 红色分量 (0-255)
 * @g: 绿色分量 (0-255)
 * @b: 蓝色分量 (0-255)
 * 
 * 功能:将 RGB 颜色值转换为 XRGB8888 格式的 32 位颜色
 *       XRGB8888 格式:[31:24]=0xFF, [23:16]=R, [15:8]=G, [7:0]=B
 * 返回值:32 位 XRGB8888 格式颜色值
 */
static uint32_t make_xrgb(uint8_t r, uint8_t g, uint8_t b) {
  return (0xFFu << 24) | (r << 16) | (g << 8) | b; // XRGB8888
}

int main(int argc, char** argv) {
  // 获取 DRM 设备路径,默认为 /dev/dri/card0
  const char* dev = (argc >= 2) ? argv[1] : "/dev/dri/card0";
  
  /*
   * open - 打开 DRM 设备
   * 参数:设备路径, O_RDWR(读写模式)| O_CLOEXEC(执行时关闭)
   * 功能:获取 DRM 设备的文件描述符,用于后续所有 DRM 操作
   */
  int fd = open(dev, O_RDWR | O_CLOEXEC);
  if (fd < 0) die("open drm");

  /*
   * drmModeGetResources - 获取 DRM 资源信息
   * 参数:DRM 设备文件描述符
   * 功能:获取 DRM 设备的所有可用资源,包括:
   *       - connectors(连接器):显示器接口
   *       - encoders(编码器):信号转换器
   *       - crtcs(CRTC):显示控制器
   *       - framebuffers(帧缓冲):显示内存
   * 返回值:指向资源结构体的指针,失败返回 NULL
   */
  drmModeRes* res = drmModeGetResources(fd);
  if (!res) die("drmModeGetResources");

  drmModeConnector* conn = NULL;
  uint32_t conn_id = 0;
  drmModeModeInfo mode = {0};

  // 遍历所有连接器,查找已连接的显示器
  for (int i = 0; i < res->count_connectors; i++) {
    /*
     * drmModeGetConnector - 获取连接器信息
     * 参数:DRM 设备文件描述符, 连接器 ID
     * 功能:获取指定连接器的详细信息,包括:
     *       - connection 状态(已连接/未连接)
     *       - 支持的显示模式列表
     *       - 物理尺寸等属性
     * 返回值:指向连接器结构体的指针,失败返回 NULL
     */
    drmModeConnector* c = drmModeGetConnector(fd, res->connectors[i]);
    if (!c) continue;
    // 检查连接器是否已连接显示器且有可用的显示模式
    if (c->connection == DRM_MODE_CONNECTED && c->count_modes > 0) {
      conn = c;
      conn_id = c->connector_id;
      mode = c->modes[0]; // 选择第一个可用模式(通常是首选分辨率)
      break;
    }
    /*
     * drmModeFreeConnector - 释放连接器资源
     * 参数:连接器结构体指针
     * 功能:释放 drmModeGetConnector 分配的内存
     */
    drmModeFreeConnector(c);
  }

  if (!conn) {
    fprintf(stderr, "No connected connector found.\n");
    return 2;
  }

  // 查找编码器和 CRTC
  drmModeEncoder* enc = NULL;
  /*
   * drmModeGetEncoder - 获取编码器信息
   * 参数:DRM 设备文件描述符, 编码器 ID
   * 功能:获取编码器信息,编码器负责将 CRTC 的数字信号转换为
   *       连接器所需的物理信号格式(如 HDMI、DisplayPort 等)
   * 返回值:指向编码器结构体的指针,失败返回 NULL
   */
  if (conn->encoder_id) enc = drmModeGetEncoder(fd, conn->encoder_id);

  uint32_t crtc_id = 0;
  if (enc && enc->crtc_id) {
    crtc_id = enc->crtc_id;
  } else {
    // 备用方案:如果没有找到编码器关联的 CRTC,使用第一个可用的 CRTC
    if (res->count_crtcs > 0) crtc_id = res->crtcs[0];
  }
  if (!crtc_id) {
    fprintf(stderr, "No CRTC found.\n");
    return 3;
  }

  /*
   * 创建 dumb buffer(简单缓冲区)
   * Dumb buffer 是 DRM 提供的一种简单的显存分配机制,适用于简单的图形显示
   * 不适合复杂的 3D 渲染,但足够用于 2D 显示和测试
   */
  struct drm_mode_create_dumb creq;
  memset(&creq, 0, sizeof(creq));
  creq.width = mode.hdisplay;    // 设置缓冲区宽度(水平像素数)
  creq.height = mode.vdisplay;   // 设置缓冲区高度(垂直像素数)
  creq.bpp = 32;                 // 每像素位数(32 位 = XRGB8888)

  /*
   * DRM_IOCTL_MODE_CREATE_DUMB - 创建 dumb buffer
   * 参数:DRM 设备文件描述符, 创建请求结构体
   * 功能:在显存中分配一块缓冲区,用于存储像素数据
   *       返回的 creq 结构体包含:
   *       - handle: 缓冲区句柄
   *       - pitch: 每行字节数(可能包含对齐填充)
   *       - size: 缓冲区总字节数
   * 返回值:成功返回 0,失败返回负数
   */
  if (ioctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &creq) < 0) die("CREATE_DUMB");

  uint32_t handle = creq.handle;
  uint32_t pitch = creq.pitch;
  uint64_t size = creq.size;

  /*
   * drmModeAddFB - 创建帧缓冲对象(Framebuffer)
   * 参数:DRM 设备文件描述符, 宽度, 高度, 颜色深度, 每像素位数, 
   *       行间距, 缓冲区句柄, 输出的帧缓冲 ID
   * 功能:将 dumb buffer 封装为帧缓冲对象,这样 CRTC 才能扫描输出
   *       帧缓冲是 DRM 用于显示的抽象层,连接内存缓冲区和显示硬件
   * 返回值:成功返回 0,失败返回非 0
   */
  uint32_t fb_id = 0;
  if (drmModeAddFB(fd, creq.width, creq.height, 24, 32, pitch, handle, &fb_id) != 0)
    die("drmModeAddFB");

  /*
   * 映射 dumb buffer 到用户空间
   * 这样程序就可以直接读写缓冲区内容
   */
  struct drm_mode_map_dumb mreq;
  memset(&mreq, 0, sizeof(mreq));
  mreq.handle = handle;
  
  /*
   * DRM_IOCTL_MODE_MAP_DUMB - 获取 dumb buffer 的映射偏移量
   * 参数:DRM 设备文件描述符, 映射请求结构体
   * 功能:查询缓冲区在设备内存空间中的偏移量,用于后续 mmap 调用
   * 返回值:成功返回 0,返回的 mreq.offset 包含映射偏移量
   */
  if (ioctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &mreq) < 0) die("MAP_DUMB");

  /*
   * mmap - 将 dumb buffer 映射到用户空间
   * 参数:地址(0=自动选择), 大小, 保护标志(读写), 
   *       映射标志(共享), 文件描述符, 偏移量
   * 功能:在用户进程的地址空间中创建显存的映射,允许直接访问显存
   * 返回值:成功返回映射地址,失败返回 MAP_FAILED
   */
  void* map = mmap(0, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, mreq.offset);
  if (map == MAP_FAILED) die("mmap");

  /*
   * drmModeSetCrtc - 设置 CRTC 配置并启动显示
   * 参数:DRM 设备文件描述符, CRTC ID, 帧缓冲 ID, 
   *       X 偏移, Y 偏移, 连接器 ID 数组, 连接器数量, 显示模式
   * 功能:配置显示控制器(CRTC),将指定的帧缓冲内容输出到连接器
   *       这是 DRM 中最核心的接口,完成显示管线的最终配置
   *       CRTC 会按照指定的模式(分辨率、刷新率)扫描帧缓冲并输出
   * 返回值:成功返回 0,失败返回非 0
   */
  if (drmModeSetCrtc(fd, crtc_id, fb_id, 0, 0, &conn_id, 1, &mode) != 0)
    die("drmModeSetCrtc");

  // 打印成功信息
  fprintf(stderr, "DRM OK: %ux%u @ %s (conn=%u crtc=%u fb=%u)\n",
          mode.hdisplay, mode.vdisplay, mode.name, conn_id, crtc_id, fb_id);

  /*
   * 颜色绘制循环
   * 功能:不断交替绘制蓝色和红色满屏画面
   */
  uint32_t* p = (uint32_t*)map;  // 将映射地址转换为 32 位整数指针(每像素 4 字节)
  size_t pixels = (size_t)(pitch / 4) * mode.vdisplay;  // 计算总像素数

  for (int t = 0;; t++) {
    // 根据循环次数选择颜色:偶数次显示蓝色,奇数次显示红色
    uint32_t c = (t % 2 == 0) ? make_xrgb(10, 30, 200) : make_xrgb(200, 30, 10);
    
    // 填充整个缓冲区
    for (size_t i = 0; i < pixels; i++) p[i] = c;
    
    /*
     * msync - 同步内存映射区域
     * 参数:映射地址, 大小, 同步标志(MS_SYNC=同步写入)
     * 功能:确保缓冲区的修改已经写入到实际的显存中
     *       对于某些硬件,这一步是必须的,否则显示可能不更新
     * 返回值:成功返回 0,失败返回 -1
     */
    msync(map, size, MS_SYNC);
    
    // 等待 1 秒后切换颜色
    sleep(1);
  }

  return 0;
}

之后编译报错,找不到drm.h

我在容器 sysroot 里确认了:

bash 复制代码
SYSROOT=/opt/k510_buildroot/k510_crb_lp3_v1_2_defconfig/host/riscv64-buildroot-linux-gnu/sysroot

ls $SYSROOT/usr/include/xf86drm.h
ls $SYSROOT/usr/include/xf86drmMode.h
ls $SYSROOT/usr/lib/libdrm.so*

这些都存在,但编译时报:

复制代码
fatal error: drm.h: No such file or directory
#include <drm.h>

继续查才发现 drm.h 实际在:

bash 复制代码
ls -l $SYSROOT/usr/include/libdrm/drm.h
ls -l $SYSROOT/usr/include/drm/drm.h

也就是说:头文件路径不是默认 include 根目录,CMake 需要显式加 include 目录。

解决办法:在 CMakeLists.txt 加 include dirs

cmake 复制代码
add_executable(drm_color drm_color.c)

target_include_directories(drm_color PRIVATE
  /opt/k510_buildroot/k510_crb_lp3_v1_2_defconfig/host/riscv64-buildroot-linux-gnu/sysroot/usr/include/libdrm
  /opt/k510_buildroot/k510_crb_lp3_v1_2_defconfig/host/riscv64-buildroot-linux-gnu/sysroot/usr/include/drm
)

target_link_libraries(drm_color drm)

重新编译通过:

bash 复制代码
cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=/opt/apps/cmake/toolchain-k510.cmake
cmake --build build -j$(nproc)

之后再板子上运行

sh 复制代码
[root@canaan ~ ]$ ./drm_color
DRM OK: 1080x1920 @ 1080x1920 (conn=49 crtc=57 fb=59)

同时可以看到板子呈现红蓝颜色变化

到这一步,最基础的开发框架搭建,就打通了

相关推荐
MounRiver_Studio1 天前
RISC-V IDE MRS2进阶分享(三):MRS语言服务器
ide·mcu·risc-v·嵌入式开发
MounRiver_Studio1 天前
RISC-V IDE MRS2进阶分享(四):CH32H417双核芯片项目开发
ide·mcu·risc-v·嵌入式开发
龙智DevSecOps解决方案4 天前
C语言安全编码指南:MISRA C、CERT C、CWE 与 C Secure 标准对比与Perforce QAC应用详解
嵌入式开发·代码安全·perforce qac·c语言安全·misrac
charlie1145141916 天前
嵌入式C++开发——RAII 在驱动 / 外设管理中的应用
开发语言·c++·笔记·嵌入式开发·工程实践
Tronlong创龙10 天前
DR1 系列评估板 OpenAMP 双核 ARM 通信案例开发手册
开发板·嵌入式开发·硬件开发·工业控制
CodeCraft Studio11 天前
从框架到体验:Qt + Qtitan 构建制造业嵌入式UI整体解决方案
开发语言·qt·ui·gui·嵌入式开发·hmi·制造业嵌入式ui
ElfBoard12 天前
ElfBoard技术贴|如何在ELF-RK3506开发板上构建AI编程环境
c语言·开发语言·单片机·嵌入式硬件·智能路由器·ai编程·嵌入式开发
MounRiver_Studio17 天前
RISC-V IDE MRS2进阶分享(一):picolibc C标准库简介与使用
ide·mcu·risc-v·嵌入式开发
MounRiver_Studio17 天前
RISC-V IDE MRS2使用笔记(十四):用户反馈功能
ide·mcu·risc-v·嵌入式开发