嵌入式Linux下V4L2显示编程:实现摄像头画面的实时预览与叠加OSD
摄像头预览不是open设备→read帧→写屏幕。V4L2标准路径是:驱动分配缓冲→mmap映射用户空间→QBUF入队→STREAMON启动→DQBUF取帧→处理→再QBUF。 OSD叠加也别每帧用OpenCV重画全图,简单场景写帧缓冲,量产走DRM plane硬件合成。
一、V4L2采集初始化
标准流程五步不能省:
int fd = open("/dev/video0", O_RDWR);
struct v4l2_capability cap;
ioctl(fd, VIDIOC_QUERYCAP, &cap); // 确认VIDEO_CAPTURE
struct v4l2_format fmt = {0};
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 640;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; // 或MJPEG
ioctl(fd, VIDIOC_S_FMT, &fmt);
/* 以驱动返回的width/height为准,别硬信自己设的值 */
struct v4l2_requestbuffers req = {0};
req.count = 4; req.type = fmt.type;
req.memory = V4L2_MEMORY_MMAP;
ioctl(fd, VIDIOC_REQBUFS, &req);
格式优先级:MIPI CAM支持NV12/YUYV就别强制MJPEG;USB UVC高分辨率YUYV会撑爆带宽再换MJPEG。
二、mmap缓冲区
零拷贝关键:不让内核态数据copy到用户态。
struct buf { void*start; unsigned len; } bufs[4];
for(int i=0;i<req.count;i++){
struct v4l2_buffer b={0};
b.type=fmt.type; b.memory=V4L2_MEMORY_MMAP; b.index=i;
ioctl(fd, VIDIOC_QUERYBUF, &b);
bufs[i].start = mmap(NULL,b.length,PROT_READ|PROT_WRITE,
MAP_SHARED,fd,b.m.offset);
bufs[i].len = b.length;
ioctl(fd, VIDIOC_QBUF, &b); // 先全入队
}
enum v4l2_buf_type t = fmt.type;
ioctl(fd, VIDIOC_STREAMON, &t);
缓冲不回队,驱动几下就没空buffer,采集循环卡死。
三、预览到Framebuffer
小屏/fb0方案最直接:把YUYV转RGB565写显存。
int fb = open("/dev/fb0", O_RDWR);
struct fb_var_screeninfo v;
ioctl(fb, FBIOGET_VSCREENINFO, &v);
unsigned stride = v.xres * 2; // RGB565
uint16_t *fbmem = mmap(NULL, v.yres*stride, PROT_WRITE,
MAP_SHARED, fb, 0);
void yuyv_to_rgb565(const uint8_t*y, uint16_t*d, int w,int h){
for(int i=0;i<w*h;i++){
int y0=y[i*2], u=y[i*2+1], v0=y[i*2+3];
int r = y0+1.4075*(v0-128);
int g = y0-0.3455*(u-128)-0.7169*(v0-128);
int b = y0+1.779*(u-128);
d[i]=((r>>3)<<11)|((g>>2)<<5)|(b>>3);
}
}
while(run){
fd_set fds; FD_ZERO(&fds); FD_SET(fd,&fds);
select(fd+1,&fds,NULL,NULL,NULL);
struct v4l2_buffer b={0};
b.type=fmt.type; b.memory=V4L2_MEMORY_MMAP;
ioctl(fd, VIDIOC_DQBUF, &b); // 取帧
yuyv_to_rgb565(bufs[b.index].start, fbmem, 640,480);
draw_osd(fbmem); // 叠加OSD
ioctl(fd, VIDIOC_QBUF, &b); // 归还
}
坑点:
摄像头分辨率≠LCD分辨率,取min_w/min_h拷贝,换行按各自stride走
YUYV不能直接memcpy进RGB565显存,必花屏
stride不对→斜条纹
转换放主循环吃CPU,NEON加速或改DRM零拷贝
四、OSD叠加:软件方案
在写fb前直接改像素,最适合时间水印、准星、报警框。
void osd_put_text(uint16_t*fb,int stride,int x,int y,const char*s){
/* 取5x7字模,逐像素写 */
for(int row=0;row<7;row++){
uint8_t line = font5x7[(int)s[0]*7+row];
for(int col=0;col<5;col++){
if(line&(1<<(4-col)))
fb[(y+row)*stride/2 + x+col] = 0xFFFF;
}
}
}
void draw_osd(uint16_t*fb){
static int cnt=0; char buf[16];
snprintf(buf,sizeof(buf),"CAM %d",cnt++);
osd_put_text(fb, 640 * 2, 10,10, buf);
/* 画报警框 */
for(int x=100;x<300;x++)
fb[100 * 640 + x]=0xF800, fb[200 * 640 + x]=0xF800;
}
工业场景别每帧重绘全屏OSD,只改变化区(时间字符串、框位置),其余保留视频层。
五、DRM/KMS硬件叠加(量产方案)
fb0软合成在i.MX8/RK平台已经淘汰,正确做法是视频走V4L2 buffer→DRM plane,OSD走另一plane,显示控制器硬件混合,CPU零拷贝。
V4L2 DQBUF拿到dma-buf fd
drmModeAddFB2把视频帧挂到video plane
UI层用ARGB plane画文字/按钮
atomic commit一次合成
伪流程:
drm_handle_event(drm_fd, &ev);
drmModeSetPlane(drm_fd, video_plane, crtc_id,
drm_fb_video, 0,
0,0,W,H, 0,0,W<<16,H<<16);
/* OSD plane单独更新 */
优点:预览不占CPU、无撕裂、支持缩放到全屏;缺点是要驱支DRM plane和dma-buf。
六、V4L2 Video Overlay接口
部分芯片(如全志VIN)支持V4L2_BUF_TYPE_VIDEO_OVERLAY,直接把OSD bitmap交给驱动合成:
fmt.type = V4L2_BUF_TYPE_VIDEO_OVERLAY;
fmt.fmt.win.w.left = 10; fmt.fmt.win.w.top = 10;
fmt.fmt.win.w.width = 128; fmt.fmt.win.w.height = 16;
fmt.fmt.win.global_alpha = 200;
ioctl(fd, VIDIOC_S_FMT, &fmt);
ioctl(fd, VIDIOC_OVERLAY, 1); // 使能
前提是驱动实现overlay能力,不是所有USB cam都有。
七、落坑清单
S_FMT后不回读参数→实际分辨率错
YUYV当RGB565直接拷→花屏
stride按width算→宽高不对齐时斜移
缓冲DQ后忘了QBUF→几帧后卡死
OSD全屏重画→预览掉帧
fb0方案上1080p→CPU转格式爆掉
不select直接DQBUF→非阻塞返回EAGAIN没处理
DRM平台还用fb0→多层UI叠不出
时间字符串每帧sprintf→无关开销
八、落地顺序
v4l2-ctl确认格式/分辨率支持
REQBUFS+mmap+QBUF+STREAMON跑通取帧
fb0先出图(YUYV转RGB565)
OSD在fb上画文字/框
只更新变化区降CPU
量产平台改DRM plane:视频层+OSD层
dma-buf共享,去掉memcpy
嵌入式Linux摄像头预览的本质:V4L2管采、显示子系统管出、OSD管叠加层。调试口诀——黑屏看DRM connector,花屏看stride和格式,卡顿看buffer数量和拷贝次数,OSD抖就看是不是每帧重画全屏。 小项目写fb够用,车载/工业HMI上量必须走硬件plane,不然CPU全耗在YUV转换上了。





