最近在做一个工业触屏终端的项目,核心板用的就是IMX6Q 开发板,需求里有一条:终端要直连一台HP LaserJet 1020 plus 打印机,通过USB 直接打印。当时想得很简单,插上线不就完了?结果真上手才发现,这台打印机在嵌入式 Linux 环境里属于“老牌硬骨头”,供电、驱动、固件加载、数据格式转换,每一环都能把人卡半天。这篇文章就把我这次从零到跑通的完整过程、原理和踩坑记录整理出来,给准备在 ARM 开发板上接打印机的朋友做个参考。
1. 硬核背景:为什么偏偏是 HP 1020 plus 最难搞
1.1 GDI 打印机到底是什么
先花点时间讲清楚一个关键概念:HP LaserJet 1020 plus 不是普通的 PCL/PostScript 打印机,它是典型的GDI(Graphics Device Interface)打印机,国内也叫“主机渲染打印机”或“Windows 打印机”。
普通激光打印机(PCL/PS)自己具备页面解释能力,电脑只要把标准打印数据丢给它,打印机内部的处理器负责把矢量图形、字体转换成点阵,再去控制激光扫描单元。而 GDI 打印机为了降低成本,把“解释页面”这件事全部推给主机。Windows 下的打印流程其实是:GDI 驱动在电脑内存里把文档渲染成打印机专用的点阵数据格式(对 1020 来说就是 ZJS 语言),然后通过 USB 直接把点阵流发给打印机。打印机内部几乎不做任何计算,只负责把收到的点阵往硒鼓上写。
HP 1020 plus 用的固件和页面描述语言是ZJS,在 Linux 下没有对应的原生内核驱动。你不能像普通 USB 打印机那样直接用cat file > /dev/usb/lp0把文本或 PDF 塞过去,因为打印机根本不理解这些格式。
更麻烦的是,1020 plus 上电后 RAM 里是空的,必须先向它写入固件程序,它才会进入可打印状态。这一步在 Windows 下由安装驱动时的“加载固件”自动完成,而嵌入式 Linux 里所有事情都得自己来。这就是为什么这块板子接 1020 会比接一台 HP P1007 或兄弟 1218W 难得多——后两种同样是 GDI 打印机,但不少型号在 Linux 下有现成驱动,固件加载也被 CUPS 封装好了。
1.2 IMX6Q 开发板的 USB 链路
IMX6Q 是 NXP i.MX6Quad 系列,四核 Cortex-A9,板子上的 USB Host 控制器走 EHCI 协议,Linux 内核里对应的驱动很成熟。表面上看,接一台 USB Full Speed(12 Mbps)的打印机没有任何障碍,但有几个硬件层面的细节会影响后面排查:
- IMX6Q 开发板一般提供两个 USB 口:一个USB Host,一个USB OTG。打印机这类设备必须接 Host 口,OTG 口在默认配置下不支持 Host 模式(除非改设备树/内核配置)。
- 很多评估板的 USB Host 供电能力只有 500mA 甚至更低。1020 plus 待机电流不大,但打印瞬间加热定影单元时电流能冲到 2A 以上,直接插在开发板 USB 口上大概率会掉电重启。
- USB 枚举如果失败,
lsusb里看不到设备,/dev/usb/lp0也不会出现。这种情况下很多新手会疯狂查内核配置,但真正原因只是供电不足。
原理层面搞清楚以后,方案选型就顺理成章了。
2. 方案选型与硬件准备
2.1 三条技术路线对比
针对“IMX6Q 开发板 + HP 1020 plus + USB 直连”,实际可行的方案有三条:
| 方案 | 实现方式 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|---|
| 纯命令行 + foo2zjs | 手动加载固件,用 foo2zjs 转换数据并写入设备节点 | 依赖最少,内存占用低,适合嵌入式精简系统 | 需要自行处理文本/PDF 转换流程,配置稍繁琐 | 无 CUPS 的精简 rootfs |
| CUPS + foo2zjs 后端 | 在板子上跑 CUPS,用 foo2zjs 的 PPD 驱动打印机 | 有打印队列、任务管理,支持网络打印扩展 | CUPS 依赖包多,占用 Flash 和内存较大 | 系统资源充足,要求队列管理 |
| HPLIP | 安装 HPLIP 全套驱动 | 对 HP 机型支持官方化 | ARM 交叉编译依赖极多,体积大 | 基本不推荐在开发板上用 |
我这次最终选择的是纯命令行 + foo2zjs。原因很实在:终端系统用的是精简版 Linux,rootfs 总共才 512MB,CUPS 全家桶装进去不仅占空间,还因为依赖 DBus、Avahi 之类的东西容易把系统搞乱。命令行方案只要三个核心东西:固件文件、foo2zjs 转换工具、设备节点。
2.2 硬件准备:别在供电上省钱
这是整个项目里最容易被忽视的一环。HP LaserJet 1020 plus 的电源适配器是外置的,打印机本身通过 USB 口和主机通信,但通信用的电和加热用的电是两套系统。打印机内部有独立的电源板,可它需要先“感知”到 USB 主机端提供的 5V 信号才能完成枚举。
所以严格来说,板子 USB 口不是给打印机供电的唯一来源,但 USB 口的 5V 必须稳定。实际测试中我发现:直接插开发板 Host 口,打印第一页时板子偶发重启;换成一个带外部电源的 USB Hub之后,问题彻底消失。建议准备一个质量靠谱的 USB 2.0 Hub,独立供电,打印机插 Hub 上,Hub 再连开发板。这一步能帮你省掉后面 80% 的疑难杂症。
2.3 内核配置要求
在编译内核或使用板厂提供的 BSP 时,确保以下配置已开启:
CONFIG_USB=y CONFIG_USB_EHCI_HCD=y CONFIG_USB_PRINTER=y最关键的是CONFIG_USB_PRINTER,对应内核里的usblp模块。这个模块会在打印机枚举成功后创建/dev/usb/lp0设备节点。很多 BSP 默认只开了存储类(U 盘)支持,usblp并没有编进去,导致lsusb能看到打印机但/dev/usb/lp0不存在。
验证方法很简单:插上打印机后执行dmesg | grep usb,正常会看到类似:
usb 1-1.3: new full-speed USB device number 4 using ehci-hcd usblp: USB Bidirectional printer dev 4 if 0 alt 0 proto 2 vid 03F0 pid 2B17 usblp0: USB Bidirectional printer dev 4 if 0 alt 0 proto 2 vid 03F0 pid 2B17看到usblp0这行,说明内核侧已经就绪。
3. 交叉编译与板端准备
3.1 获取工具链和源码
这次的开发板跑的是 ARM 32 位 Linux(Cortex-A9 对应 armv7l 架构),交叉编译工具链我用的arm-linux-gnueabihf-gcc。如果你的环境是 Yocto,可以用 SDK 里自带的工具链;如果用的是 Buildroot,直接使用make sdk导出的工具链。
foo2zjs 是开源项目,获取源码后交叉编译。注意这个包不是标准的 autotools 工程,而是一个带 Makefile 的源码树,编译方式比普通软件要“原始”一些。下载解压后进入目录:
tar xf foo2zjs.tar.gz cd foo2zjs3.2 交叉编译 foo2zjs
默认的make会用主机 GCC 编译出 x86 版本,在 ARM 板子上跑不了。交叉编译时直接指定 CC 环境变量即可:
make clean make CC=arm-linux-gnueabihf-gcc AR=arm-linux-gnueabihf-ar编译完成后,目录下会生成foo2zjs、foo2hp、foo2xqx等一堆可执行文件。用file命令确认架构:
file foo2zjs # 输出应该是 ELF 32-bit LSB executable, ARM, EABI5make install在交叉编译环境下不太好直接执行,因为它会把文件安装到宿主机目录里。我采用更可控的做法:从生成结果中挑出需要的二进制和 PPD 文件,手动拷贝到板子的 rootfs 对应路径。
具体需要的文件包括:
foo2zjs(核心转换工具,1020 走的就是它)foo2hp(部分 HP 型号的转换后端,按需拷贝)sihp1020.img(打印机固件文件,后面细说)HP-LaserJet_1020.ppd(如果后续想接 CUPS 会用得上)
把这些文件丢到板子的/usr/local/foo2zjs/目录下,电源管理方面要注意chmod +x权限。
3.3 获取打印机固件
这是最容易踩坑的环节。1020 plus 的固件不会自己出现在 Linux 内核里,需要单独下载。foo2zjs 源码目录里带了一个getweb.py脚本,可以自动从固件仓库抓取对应型号的固件文件:
python3 getweb.py 1020执行成功后,目录下会得到固件文件,把它重命名为sihp1020.img并拷贝到板子上。
如果板子上没有 Python 环境,也可以在 PC 上下载好再传过去。固件文件很小,通常只有几十 KB,放到/usr/local/foo2zjs/sihp1020.img即可。
4. 打印链路实现:从文件到纸面
4.1 设备识别与固件加载
板子上电后,先确认 USB 枚举和设备节点:
lsusb dmesg | grep usblp ls -l /dev/usb/lp0lsusb里能看到类似Bus 001 Device 004: ID 03f0:2b17 HP LaserJet 1020的信息,/dev/usb/lp0存在,下面进入固件加载环节。
固件加载是整个打印过程的重中之重。每次打印机重新上电,都必须先加载固件,否则后续写入的任何数据都会被打印机丢弃。加载命令:
cat /usr/local/foo2zjs/sihp1020.img > /dev/usb/lp0注意这里不能用echo或普通重定向直接写文本,固件是二进制文件。加载成功的标志是:打印机面板上的状态指示灯从闪烁变成常亮,随后会有短暂的“初始化动作”声。如果灯一直闪或者完全没反应,说明固件没加载成功。
有些资料里会推荐用dd if=sihp1020.img of=/dev/usb/lp0,效果一样。我习惯用cat配合重定向,写完可以通过dmesg看到usblp的读写反馈。
4.2 文本、PDF 与图片的打印流程
固件加载成功之后,打印机已经“活”了,但还不能直接扔数据。1020 plus 只认 ZJS 格式的点阵数据流。你需要把要打印的内容先转换成 PostScript,再用foo2zjs把 PostScript 转换为 ZJS,最后写入设备节点。
打印普通文本文件:
enscript -B -p /tmp/output.ps /etc/issue foo2zjs /tmp/output.ps > /tmp/output.zjs cat /tmp/output.zjs > /dev/usb/lp0enscript用来把文本文件转成 PostScript,-B表示不打印页眉页码。如果板子上没装 enscript,也可以用a2ps替代,格式参数略有不同。
打印 PDF 文件:
pdftoppm -ps /tmp/document.pdf /tmp/document_output foo2zjs /tmp/document_output.ps > /tmp/document_output.zjs cat /tmp/document_output.zjs > /dev/usb/lp0注意pdftoppm默认输出的是 PPM 图像,加-ps参数才会输出 PostScript。如果你的 PDF 是扫描件,这种方式渲染效果最好;如果是文字型 PDF,也可以直接用pdftops转成 PostScript 再进 foo2zjs。
打印图片:
图片推荐先转成 PDF 再走上面的流程,或者用 ImageMagick 的convert转成 PostScript:
convert photo.jpg /tmp/photo.ps foo2zjs /tmp/photo.ps > /tmp/photo.zjs cat /tmp/photo.zjs > /dev/usb/lp0foo2zjs输出的.zjs文件大小和打印内容的复杂程度直接相关,纯文本一页大概几十 KB,图片一页可能到几 MB。写入速度受 USB Full Speed 限制,大文件需要耐心等。
4.3 封装一键打印脚本
命令行一条条敲太容易出错,我习惯把流程封装成一个脚本,放在/usr/local/bin/print里。脚本逻辑很简单:先判断设备节点是否存在,不存在就尝试重新加载usblp模块;然后加载固件(如果超过 30 秒没有打印任务会自动跳过?其实固件每次上电只需一次,脚本里做成可选参数);最后根据文件类型选择转换路径并发送。
#!/bin/sh DEV=/dev/usb/lp0 FIRMWARE=/usr/local/foo2zjs/sihp1020.img load_fw() { if [ -c "$DEV" ]; then cat "$FIRMWARE" > "$DEV" sleep 2 fi } send_ps() { foo2zjs "$1" > /tmp/print.job.zjs cat /tmp/print.job.zjs > "$DEV" } case "$1" in fw) load_fw ;; txt) enscript -B -p /tmp/print.ps "$2"; send_ps /tmp/print.ps ;; pdf) pdftoppm -ps "$2" /tmp/print_out; send_ps /tmp/print_out.ps ;; img) convert "$2" /tmp/print.ps; send_ps /tmp/print.ps ;; *) echo "Usage: print fw|txt|pdf|img FILE"; exit 1 ;; esac实际使用中我会把“加载固件”这个动作单独拆出来,因为固件重复加载有时会导致打印机卡在初始化状态。最好的策略是:打印机插电后,在系统启动脚本里执行一次固件加载,之后打印任务只需要做转换和发送。
4.4 可选:CUPS 集成方式
如果后续需求升级到“多任务排队、web 管理、网络打印”,可以考虑在板子上跑 CUPS。CUPS 配 foo2zjs 的思路是:把foo2zjs作为打印后端,PPD 文件用源码包里自带的HP-LaserJet_1020.ppd。添加打印机时:
lpadmin -p hp1020 -E -v usb://dev/usb/lp0 -P /usr/local/foo2zjs/HP-LaserJet_1020.ppdCUPS 的usb后端在很多嵌入式系统里可能无法自动枚举设备,这时候用-v file:/dev/usb/lp0作为设备 URI 也能工作,但要注意 CUPS 的file后端默认会把任务写到文件而不是直接送设备,需要在cupsd.conf里确认启用了FileDevice Yes。考虑到嵌入式环境的实际体量,我最终没有启用 CUPS,纯命令行方案已经足够稳定。
5. 常见问题与排查记录
这一节把项目过程中真正遇到过的坑和对应的排查思路整理成速查表,后面再做类似项目可以直接对照。
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
lsusb看不到打印机 | USB 供电不足 | 换带外部电源的 USB Hub;确认接在 Host 口而非 OTG 口 |
lsusb能看到但/dev/usb/lp0不存在 | 内核没开CONFIG_USB_PRINTER或usblp模块未加载 | modprobe usblp;重新编译内核 |
| 固件加载后指示灯仍闪烁 | 固件文件不对或传输被中断 | 确认文件名是sihp1020.img;用ls -l检查文件大小是否与官网一致;重新插拔打印机再试 |
| 打印乱码 | 固件未加载成功,或数据没有经过 foo2zjs 转换 | 先加载固件;确认发送的是.zjs数据流而非原始文本 |
| 打印任务发出后打印机无动作 | 数据格式正确但设备节点写入权限不对 | 检查/dev/usb/lp0权限,必要时chmod 666;确认没有进程占用设备 |
| 打印中途停止,板子报 USB 错误 | 供电不稳或 USB 线质量差 | 换短线、换屏蔽线,打印机独立供电 |
| 打印页面只有一半 | 数据量太大,USB 缓冲区溢出 | 将大文件拆分成多个小任务,或降低打印分辨率 |
| 第二次打印时打印机不响应 | 部分固件在特定状态下不会自动复位 | 重新插拔打印机 USB 线,再加载一次固件 |
几个实操心得:
- 打印前先发一次固件这个动作永远不要省。我在调试时遇到过“第一次打印正常,第二次没反应”,折腾半天才发现是打印机重新枚举后固件丢了。后来我干脆写了一个监控脚本,打印机一插上就自动加载固件,一劳永逸。
foo2zjs的转换过程比较吃 CPU,IMX6Q 四核跑起来毫无压力,但如果你用的是单核 ARM9 平台,一页 A4 图片转换可能要等十几秒,要有心理准备。- 1020 plus 的打印速度上限不高,加上 USB Full Speed 传输限制,实际打印一页文字加转换时间总共约 8-15 秒,这在工业终端场景里完全够用。
6. 一些值得延伸的做法
从项目角度来说,这次跑通只是第一步。后续如果要做成产品,我会在几个方向上继续完善:
- 系统服务化:用守护脚本监听
/dev/usb/lp0的状态变化,实现打印机热插拔自动重载固件,避免现场人员手动干预。 - 打印任务落盘:把打印任务以文件形式排队保存,防止打印过程中断电导致文件丢失,重新上电后继续打印。
- 多型号兼容:foo2zjs 本身支持一堆型号,固件文件和解码器都是独立的,改造成“按 VID/PID 自动选择固件和转换工具”并不难。
我个人在整套调试下来最深的体会是:这类嵌入式打印问题,80% 的时间花在打印机的脾气上,而不是开发板本身。HP 1020 plus 是一台十多年前的打印机,但它在嵌入式 Linux 生态里的特殊性到今天依然存在。先把 GDI 打印机的原理吃透,再动手焊线、编内核、写脚本,顺序反了就会像我一开始那样,被各种“看似灵异的现象”折腾到怀疑人生。希望这篇记录能帮你少走几个弯路。