文章目录
- 前言
- 开发环境及开发方式
- 前言
- 交叉编译工具链确认:gcc、sysroot、可用性
- cmake
- [写一个通用 toolchain-k510.cmake(以后所有项目复用)](#写一个通用 toolchain-k510.cmake(以后所有项目复用))
- [用 CMake 编译第一个程序并上板验证](#用 CMake 编译第一个程序并上板验证)
- UI学习
前言
在上一篇完成了交叉编译环境的配置后,下一步我准备配置一个通用的开发工程
首先说一下我的开发方式
开发环境及开发方式
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-linuxgcc 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)
同时可以看到板子呈现红蓝颜色变化
到这一步,最基础的开发框架搭建,就打通了