家里那块 i.MX6ULL 是单核 Cortex-A7,没有 VPU(视频硬解单元),内存 489MB。翻译一下:H.264 全靠 CPU 一颗核硬啃。某天盯着它那块 1024x600 的小屏,冒出个念头:能不能把它做成一台真正的电视?插上电自己联网、自己出画面、能换台。
终于有时间给它做完了。现在它每天早上跟我一起开机,播 360p 直播源,20 FPS 上下,能摸屏换台,也能按板载那两个小按键换台。
源码和整套 macOS 上的开发环境都在这里,AGPL-3.0 开源:
https://gitee.com/morixinguan/100ask_imx6ull_for_mac
先说明一句:本文只讨论技术实现,不提供任何直播源地址,实际使用时请使用你自己拥有合法接收权限的源。
下面这几张不是渲染图,是从板子显存里直接抓出来的真实画面(仅为技术演示)。
开机动画:

正在播放:

左上角那行白字是换台后叠 8 秒的频道名水印,到点自己消失。
点屏幕中间那块会弹出频道列表,当前台是绿的(下图里的频道名仅为技术演示):

先说清楚:这板子有多弱
i.MX6ULL 是单核 Cortex-A7,没有 VPU(视频硬解单元),内存 489MB。我上电后一条条查出来的底细:
- SoC:i.MX6ULL,单核 Cortex-A7
- 内存:489 MB
- 内核 / 根文件系统:Linux 4.9.88 / Buildroot 2020.02(glibc 2.30)
- 屏幕:fb0 = 1024x600,32bpp
- 触摸 / 按键:电容屏 + 板载两个 gpio 按键
- 网络:RTL8723BU WiFi(这颗后面坑了我一下)
- 显示输出:LCD 之外还有 HDMI,靠一颗 sii902x 转接芯片
板子还带了音频(wm8960)、SPI、I2C、看门狗这些,能玩的挺多。只是这次用不上。
思路:一行像素都不自己画
一开始我想的是拉流、解码、写屏全自己干。想了两分钟放弃了:软解 H.264 不是周末能写完的东西。
可板子上本来就跑着 ffmpeg 4.2.3。那就让它干重活。
最后写出来的主控程序不到 700 行 C,只干四件事:
- 读频道表,决定现在该播哪个地址
- 拼一条 ffmpeg 命令,fork + execvp 出去
- 收触摸、按键、信号,翻译成换台 / 菜单 / 退出
- 看护 ffmpeg 子进程(掉了重连),顺便盯着屏幕别黑、别闪光标
画出来是这样:

这么分层的几个理由:
- 解码交给 ffmpeg:自己写 H.264 解码不现实。
- 输出直接写
/dev/fb0:板子没跑 X11/Wayland,fbdev 是最短路径。
- ffmpeg 当子进程:崩了不拖垮主控,换台就是杀掉重启。
- 换台用信号:串口里一条 kill 就能换台,调试太方便了。
核心其实只有一条命令
主控最实在的产出,是拼出一条 ffmpeg 命令。真实长这样:
ffmpeg -nostdin -loglevel warning \
-rw_timeout 15000000 -probesize 10M -max_delay 800000 \
-i "https://.../index_4.m3u8" \
-map 0:v:0 -an \
-sws_flags fast_bilinear \
-vf "scale=1024:600:force_original_aspect_ratio=decrease,pad=1024:600:(ow-iw)/2:(oh-ih)/2:color=black,drawtext=text='频道名':x=36:y=28:fontsize=34:enable='lt(t,8)'" \
-pix_fmt bgra -f fbdev /dev/fb0
几个不那么显然的参数:
-rw_timeout:15 秒读不到数据就让 ffmpeg 自己退出,交给主控重连。不然它会卡在一条死链上一动不动。
-an:关音频。音频解码还要再吃一截 CPU,而这板子没接音箱。
enable='lt(t,8)':频道名水印只显示 8 秒。
-sws_flags fast_bilinear:别用默认的 bicubic,那是 ffmpeg 最慢的一档缩放。
最后一个是后面调优时才发现的关键,先记着。
开机:别让用户盯着黑屏
第一版跑起来的时候,从通电到出画面要黑着等几十秒,跟死机没区别。真机顶盒不是这样的。
改成两段:上电就先画一张静态的"正在启动",把联网、对时那几十秒盖住;播放器起来后再放一段 Logo 加进度条的动画:

阶段一长这样(联网那几十秒屏幕上就是它):

有个细节:画这张图之前,脚本先把 fb0 的分辨率改回面板原生的 1024x600。原因后面讲 HDMI 那段说。
动画默认 5 秒,摸一下屏、按个键、或者发个信号都能跳过。想换成真正的开机广告,往 /etc/iptv/ 丢一个 boot.mp4 就行,不用改代码。
交互:屏幕分三块,够用
没上 UI 框架。屏幕切三份:上三分之一上一台、下三分之一下一台、中间弹列表;板载两个按键映射成上下;串口里 kill -USR1 也能换台。
换台中间有 1 到 2 秒空档(HLS 要重新探测加缓冲,改不掉)。这段时间屏幕上还留着上一个台的最后一帧,看着就像按了没反应。所以切台时先顶一张静态提示上去,0.3 秒就能画完:

起播慢治不了,但"看得出它在切"和"以为死机了"是两种体验。
从 9.7 FPS 到 20 FPS
第一版能播了,但只有 9.7 FPS,动起来一顿一顿。我以为是解码太慢,毕竟单核。
实测之后发现完全不是。
把 640x360 的源拆开单独测(CPU 锁 792MHz):纯解码、输出到 null,能到 47 FPS;加上缩放到 1024x600,直接掉到 8 FPS。
贵的根本不是解码,是 swscale 的插值。缩放这一下吃掉了 83% 的时间。


于是按顺序做了三件事:
- 把 CPU 锁在 792MHz。默认的 ondemand 策略实测就赖在 198 / 528MHz,满载了都来不及升。9.7 到 14 FPS。
- 不放大。1:1 直通,四周补黑边。14 到 20 FPS。
- 缩放换 fast_bilinear。满屏档从 8 提到 14。
最后 360p 源稳定在 20 FPS,画面不抖。代价是画面只占屏 62%,四周有黑边。
过程中撞到好几个反直觉的结论:
- 想把写屏带宽省一半,把 fb0 切成 16bpp,结果更慢(16 vs 21 FPS)。rgb565 的转换路径没有 bgra 优化得好。
- 加
-threads 1 反而从 19 掉到 14 FPS。单核上多线程也能让解码和缩放流水重叠。
- 放大到 85% 屏宽和放大到 100% 几乎一样贵。开销花在"放大"这个动作上,跟放大多少关系不大。所以要么不放大,要么直接满屏,中间档没意义。
- 挑源比优化管用:有个国际新闻源的最低档正好是 1024x576,跟屏幕一样,不用插值,实测 13 FPS,比 640x360 放大到满屏(11 FPS)还流畅。
- HLS 的 master.m3u8 会让 ffmpeg 默认挑码率最高的变体。有个源最高档是 1080p / 4.6Mbps,这块板实测 0 帧。所以频道表里要直接写低码率的子地址。
踩过的坑(这段最值钱)
画面根本没写进去,但一切看起来都正常。 这是最坑的一条。fbdev 的像素格式得跟 fb0 位深对上,写死 bgra 时 ffmpeg 只报一句 Pixel format bgra is not supported, use rgb565le,进程照样跑,日志照样有。
而且这块板的 fb0 参数每次重启还可能变,实测出现过 1024x600/32bpp 和 1280x720/16bpp 两种。现在程序启动先读 sysfs 决定尺寸和 pix_fmt。想确认画面到底有没有写进去,md5sum /dev/fb0 连采两次看变没变最快。
看十分钟自己黑屏。 内核的 console blanking,默认 600 秒。背光其实是好的,别去调背光。sysfs 那个 consoleblank 文件在这块板是只读的,写它只会 Permission denied。真正管用的是三层:fw_setenv 加 consoleblank=0、开机脚本往 tty1 写 ESC[9;0]、播放器每 10 秒再补写一次。
画面上一直有个光标在闪。 那是 fbcon 画在 framebuffer 上的,跟 ffmpeg 抢同一块显存(printk 和 getty 的登录提示也会直接叠在视频上)。要解绑的是 vtcon1 不是 vtcon0,vtcon0 是 dummy device 的空壳。
开机后串口敲命令没反应。 busybox init 串行跑 /etc/init.d/S*。我之前把联网和对时写在前台,ntpd 没网时卡住,init 被堵死,串口再也要不到 shell(有回显,命令不执行)。整个流程必须用 ( ... ) & 放后台。
WiFi 怎么都连不上。 RTL8723BU 在 wlan0 上 carrier 恒为 0、发不出数据帧,日志反复 nolinked power save;NetworkManager 还会跟手动的 wpa_supplicant 抢接口。重载驱动关掉省电、停掉 NetworkManager、改用 wlan1,就通了。
接上电视之后,坑更深
这块板的 HDMI 不是独立显示通道。SoC 没有 HDMI 控制器,底板上是一颗 sii902x 发射芯片,把 LCDIF 的并行 RGB 信号转成 HDMI,绑的还是同一个 fb0。所以电视上看到的就是 fb0 的内容,不存在第二块显存。
插上 HDMI 的那一刻,驱动会读电视的 EDID,然后直接把 fb0 从 1024x600/32bpp 改成 1280x720/16bpp。播放器还按旧参数输出,轻则画面错位,重则一帧都写不进去。
更麻烦的是 LCD 被连累:面板物理只有 1024x600,一个物理像素对一个数据像素,不会自己缩放。你给它一路 1280x720 的信号,它只能按自己的时序去采,采不到的地方就是黑边,实测左边黑了约五分之一。
一个 fb0 接两块屏,分辨率只能二选一。所以留了个开关:默认优先 LCD,开机时把 fb0 改回面板原生时序(电视自己缩放,画面完整);想让电视点对点最清晰,设 IPTV_SCREEN=hdmi 保持 1280x720,代价是 LCD 有黑边。

频道从哪来
网上流传的那些电视台 m3u8 聚合列表基本都是未授权转发的盗链源,别用,也别放进频道表。用你自己有接收权限的源:
- 家里运营商 IPTV 的组播地址(
rtp://239.x.x.x:xxxx):你已经付过费的业务,从自家 IPTV 盒子的设置里查,或者用运营商公开的频道列表文件就能拿到,最稳也最合规。
- 电视台官网 / 官方 CDN 上免费公开的那部分直播地址。
试过一个国际新闻台的官方 HLS,最低档正好 1024x576,播起来最舒服(下图仅为技术演示):

顺便提醒:电视台自己公开的那批海外公共频道,在普通家庭宽带下基本连不上,别在这上面耗时间。
还能往哪走
板子上挂着一个 PXP 硬件 2D 加速器(/dev/pxp_device),缩放和色彩转换本来可以甩给它,只是板上 gstreamer 没装对应插件。自己写 PXP 调用(mmap + ioctl)预计还能再翻一倍。
想省事呢就换带 VPU 的平台。i.MX6ULL 天生没这个单元,360p 下 20 FPS 已经是这颗核的诚实水平。
别的方向:用 LVGL 画更讲究的 OSD(得处理它跟 ffmpeg 抢 fb0 的问题,双缓冲或者 overlay);音频其实已经支持 alsa,只是这块板没接音箱,默认关着。
说回这块板子
我买这块 100ask(韦东山老师团队)的 i.MX6ULL 本来是想学驱动的,结果先拿它当了一回电视。
做完这个项目之后,我觉得它特别适合干这种事,原因很实在:
- 屏幕、电容触摸、按键、WiFi、HDMI、音频全都在板子上,插上就能用,不用额外买一堆模块。做"看得见摸得着"的项目,这点太重要了。
- 资料是公开的。我全程查的是 100askTeam 在 GitHub 上的那群仓库,Linux 4.9.88 内核源码、裸机例程、驱动实验源码和文档、还有《嵌入式 Linux 应用开发完全手册》都在里面,对着板子上的实际情况一条条核对,都能对上。
- 它足够"弱",弱到你必须去理解每一层。就这么一块没有 VPU 的单核 A7,逼着我把 framebuffer 像素格式、evdev 输入、HLS 分片、swscale 缩放全都摸了一遍——这些恰好就是嵌入式 Linux 应用开发最核心的那几条路。
板子的资料和社区都还在更新。碰到卡住的地方,能翻到别人踩过的记录,比一个人对着 datasheet 硬扛强太多。
真正值钱的其实不是那 700 行代码,是踩坑清单里那十几条。每一条都是熬出来的。
想自己动手
工程就在 examples/03_iptv/:一个 660 行的 main.c、一份频道表、一个开机脚本。整个仓库(含 macOS 上的交叉编译工具链、串口和 ZMODEM 下发脚本,还有另外两个例程)在这儿,AGPL-3.0 开源:
https://gitee.com/morixinguan/100ask_imx6ull_for_mac
编译和部署:
make -C examples/03_iptv deploy
推上去之后 /etc/init.d/S95iptv start 就开始播了。
软件分层、数据流、进程模型、每个模块怎么实现的,还有完整踩坑清单,都写在 examples/03_iptv/ARCHITECTURE.md 里,配了流程图,可以对着源码看。
祝你的板子也别再吃灰。这类偏底层的折腾过程,在云栈社区也能找到不少同好分享的踩坑记录,碰到问题不妨去翻一翻。
免责声明:本文为个人技术实践分享,只讲播放器的实现原理,不提供也不推荐任何直播源地址。文中出现的频道画面仅用于说明技术效果,相关权利归原权利人所有。实际使用时请使用自己拥有合法接收权限的源,并遵守当地法律法规。本文未附加任何购买链接。