嵌入式Linux下IMX6Q驱动HP 1020 plus USB打印机实战
最近在做一个工业触屏终端的项目核心板用的就是IMX6Q 开发板需求里有一条终端要直连一台HP LaserJet 1020 plus 打印机通过USB 直接打印。当时想得很简单插上线不就完了结果真上手才发现这台打印机在嵌入式 Linux 环境里属于“老牌硬骨头”供电、驱动、固件加载、数据格式转换每一环都能把人卡半天。这篇文章就把我这次从零到跑通的完整过程、原理和踩坑记录整理出来给准备在 ARM 开发板上接打印机的朋友做个参考。1. 硬核背景为什么偏偏是 HP 1020 plus 最难搞1.1 GDI 打印机到底是什么先花点时间讲清楚一个关键概念HP LaserJet 1020 plus 不是普通的 PCL/PostScript 打印机它是典型的GDIGraphics 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 Speed12 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 的精简 rootfsCUPS foo2zjs 后端在板子上跑 CUPS用 foo2zjs 的 PPD 驱动打印机有打印队列、任务管理支持网络打印扩展CUPS 依赖包多占用 Flash 和内存较大系统资源充足要求队列管理HPLIP安装 HPLIP 全套驱动对 HP 机型支持官方化ARM 交叉编译依赖极多体积大基本不推荐在开发板上用我这次最终选择的是纯命令行 foo2zjs。原因很实在终端系统用的是精简版 Linuxrootfs 总共才 512MBCUPS 全家桶装进去不仅占空间还因为依赖 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_USBy CONFIG_USB_EHCI_HCDy CONFIG_USB_PRINTERy最关键的是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 位 LinuxCortex-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 CCarm-linux-gnueabihf-gcc ARarm-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 ifsihp1020.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转成 PostScriptconvert 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的转换过程比较吃 CPUIMX6Q 四核跑起来毫无压力但如果你用的是单核 ARM9 平台一页 A4 图片转换可能要等十几秒要有心理准备。1020 plus 的打印速度上限不高加上 USB Full Speed 传输限制实际打印一页文字加转换时间总共约 8-15 秒这在工业终端场景里完全够用。6. 一些值得延伸的做法从项目角度来说这次跑通只是第一步。后续如果要做成产品我会在几个方向上继续完善系统服务化用守护脚本监听/dev/usb/lp0的状态变化实现打印机热插拔自动重载固件避免现场人员手动干预。打印任务落盘把打印任务以文件形式排队保存防止打印过程中断电导致文件丢失重新上电后继续打印。多型号兼容foo2zjs 本身支持一堆型号固件文件和解码器都是独立的改造成“按 VID/PID 自动选择固件和转换工具”并不难。我个人在整套调试下来最深的体会是这类嵌入式打印问题80% 的时间花在打印机的脾气上而不是开发板本身。HP 1020 plus 是一台十多年前的打印机但它在嵌入式 Linux 生态里的特殊性到今天依然存在。先把 GDI 打印机的原理吃透再动手焊线、编内核、写脚本顺序反了就会像我一开始那样被各种“看似灵异的现象”折腾到怀疑人生。希望这篇记录能帮你少走几个弯路。