资讯动态

IMX6Q嵌入式Linux下USB直连GDI打印机:libusb固件上传与打印实现

发布时间:2026/10/21 8:24:46 来源:尧图企业网站定制
手头正好在做IMX6Q开发板相关的项目最近碰到一个需求让板子通过USB直连HP LaserJet 1020 plus打印机实现本地打印。别看这打印机是老的GDI机型在没有网络、没有打印服务器的小型嵌入式环境里用USB直接拉线打印反而最省事不依赖路由器、不依赖驱动共享接上电就能用。我前前后后折腾了两三天把整套链路从内核配置、用户态通信到数据分块传输都捋了一遍这里把过程完整记下来。1. 整体设计思路与方案选型1.1 为什么选USB直连而不是网络打印IMX6Q这个平台跑Linux很方便常见的方案无非是走网络打印IPP/LPR或者USB直连。我一开始想的是给板子接一个打印共享器或者走Network Printer但实测下来有几个现实问题一是板子位置经常变动网口不一定每个现场都有二是很多打印共享器对GDI打印机支持不友好LaserJet 1020 plus本质上是个靠宿主机CPU渲染的GDI打印机很多网络打印盒反而会卡在驱动和固件下发环节三是设备直接USB连线省掉了一层网络协议转换延迟更低出问题也好排查。所以最终确定IMX6Q开发板的USB Host口直接连打印机USB口板子作为打印机的控制端。这样打印机固件、渲染数据全部由开发板侧完成链路最短。1.2 为什么LaserJet 1020 plus比较特殊LaserJet 1020 plus在Linux下不是标准的PostScript打印机也不是PCL打印机而是典型的GDI打印机也就是所谓的winprinter。这种打印机内部没有完整的页面描述语言解释器所有光栅化工作全交给宿主机然后通过专用的控制协议把渲染好的位图数据下发给打印机。换句话说你必须先有合适的驱动和合适的固件加载流程打印机才会真正干活。在Linux下这款打印机有对应的开源驱动通常通过foo2zjs或foo2hp实现需要先给打印机上传固件再送打印数据。这个固件不是给CPU用的是给打印机内部那颗控制芯片用的不上传固件打印机即使插上电也不会响应打印任务。整个流程里大约有几百KB的固件数据要先通过USB控制端点发到设备端。1.3 USB通信链路选择IMX6Q的USB Host控制器是标准的EHCI/HSIC跑Linux 4.x/5.x内核都没问题。应用层和打印机通信用的是usbfs也就是通过libusb直接访问USB设备而不走内核的usblp驱动。这样做的原因有两条直接通过libusb可以拿到完整的读/写/控制权限方便上传固件、发送打印数据、读取状态。usblp驱动在某些内核版本里对GDI打印机处理不友好有的固件上传时序会被内核打断造成打印机卡死。绕过内核驱动走用户态libusb时序完全由用户态自己控制出问题也好定位。不过走libusb前要先确认USB设备有没有被内核的usblp或者打印机驱动占用。我用的内核默认把usblp编译成了模块没插打印机前不会加载所以没有冲突。2. 核心细节解析与实操要点2.1 IMX6Q侧的内核配置和USB检测开发板用的是某厂商的IMX6Q核心板底板上有一个USB Host A型口刚好可以用来接打印机。先确认内核里这几个选项要开CONFIG_USBy CONFIG_USB_EHCI_HCDy CONFIG_USB_PRINTERy CONFIG_USB_SERIALy CONFIG_FW_LOADERyCONFIG_USB_PRINTER就是usblp的开关默认编译成模块也行但如果你不想让内核占用打印机干脆不加载usblp模块或者把它disable掉我个人建议直接编成模块不用时就不加载。开机后先用这个命令确认打印机被枚举出来lsusb正常情况下会看到一行类似Bus 001 Device 004: ID 03f0:2b17 Hewlett-Packard LaserJet 1020注意这个ID03f0是HP的Vendor ID2b17是LaserJet 1020 plus的Product ID。不同批次可能会有细微区别比如有的显示2b16或2b19但只要是03f0开头的HP设备基本就是它了。然后再看一下内核日志dmesg | grep -i usb能看到类似usb 1-1: new high-speed USB device number 4 using ci_hdrc usb 1-1: New USB device found, idVendor03f0, idProduct2b17 usb 1-1: New USB device strings: Mfr1, Product2, SerialNumber3 usb 1-1: Product: LaserJet 1020如果这块没出来先检查接线和电源。有的USB Hub供电不足识别不到打印机最好插开发板原生USB口或者额外供电的Hub。2.2 固件加载流程和关键参数LaserJet 1020 plus在Linux下使用foo2hp驱动时需要先把固件文件比如sihp1020.img写入打印机。这个固件文件可以在开源驱动包里找到也可以在安装foo2zjs时自动下载到本地目录。固件上传逻辑是这样的打印机上电后内部主控处于等待固件的bootloader状态此时设备接口显示为一个vendor-specific设备。只有当固件上传完毕打印机才会重启内部核心枚举出正常的打印机接口。所以整个流程里第一个动作不是发打印数据而是上传固件。我用的命令如下cat /usr/share/foo2zjs/firmware/sihp1020.img /dev/usb/lp0其实这句话就是把固件当做普通数据流写到usblp设备节点。但在用libusb方案时不能依赖/dev/usb/lp0这个节点而是直接用libusb的控制传输或批量传输端点发送固件数据。固件文件大小大约几十KB到一百多KB不等。传输时可以考虑分块一部分一部分地send避免一次性把几百KB塞进内核的USB缓冲区。实测下来64KB左右一块比较稳定。2.3 用户态打印数据通道固件传完后打印机重新枚举这时会出现一个正常的打印机接口。这个时候通过lsusb -v可以看到接口描述符里有批量输出端点 Bulk OUT Endpoint和批量输入端点Bulk IN Endpoint。LaserJet 1020 plus数据通道用的就是批量传输。打印数据传输的套路很简单把渲染好的数据比如位图或PCL数据通过批量OUT端点发给打印机然后通过批量IN端点读取打印机返回状态。对于一些双向通信指令还需要控制传输来发送。在我这边用户态选用了libusb-1.0加Python绑定原因是不用编译C程序就能快速验证通信逻辑。Python调用libusb很直观而且错误信息可读性好调试方便。2.4 为什么不用CUPS和驱动插件肯定有人问直接把foo2hp编译好灌进CUPS不就不用自己写通信了吗理论上是省事但嵌入式板子内存小、Flash小CUPS在IMX6Q上虽然能跑却会多出一堆依赖。打印任务少时CUPS的调度、队列管理反而变成了开销。加上打印机是USB直连不是网络发现CUPS的优势发挥不出来。我在这个项目里采用了一条更轻量的链路应用层Python脚本直接调用libusb把PRN文件预渲染好的打印数据发给打印机。PRN文件怎么来可以先在PC上用驱动打印到文件也可以直接用foo2hp命令行工具把PostScript/PDF转成PRN。只要格式是对的打印机只管收数据。3. 实操过程与核心环节实现3.1 初始化libusb并查找打印机先装好依赖库IMX6Q的根文件系统是Debian系的直接apt装apt-get install python3-usb对应的底层库是libusb-1.0-0Python包会依赖它。装好后先用一个小脚本枚举设备import usb.core import usb.util dev usb.core.find(idVendor0x03f0, idProduct0x2b17) if dev is None: raise ValueError(Printer not found) print(fFound printer: bus {dev.bus}, address {dev.address})如果返回None大概率是固件还没上传打印机当前还处在等待固件的状态或者被usblp驱动占用了。检查方法看lsusb如果设备显示的还是HP的一般vendor接口而不是打印机接口说明固件没上传成功。3.2 上传固件时序固件上传前先确认设备处在原始状态lsusb -v -d 03f0:2b17 | grep bInterfaceClass如果看到的Interface Class是Vendor Specific0xff那就是固件没加载。此时可以通过libusb控制传输写固件。固件传输代码大致如下FIRMWARE_PATH /usr/share/foo2zjs/firmware/sihp1020.img CHUNK_SIZE 64 * 1024 with open(FIRMWARE_PATH, rb) as f: while True: chunk f.read(CHUNK_SIZE) if not chunk: break dev.write(0x02, chunk, timeout5000) # 0x02是OUT端点注意端点号LaserJet 1020 plus上传固件时用的端点和正常打印时的端点可能不一样。有的固件上传走的是端点2正常打印数据走的也是端点2但更稳妥的做法是先通过usb.control_transfer配置好打印机的备用设置再写数据。我踩过一个坑直接用write(0x02)上传固件结果打印机没反应。后来用USB分析仪抓包才发现固件上传前必须先发送一个特定的控制请求把打印机从“等待固件”状态切换到“接受数据”状态。这个请求在foo2zjs源码里有定义是一组vendor-specific的control transfer。补上这行dev.ctrl_transfer(bmRequestType0x40, bRequest0x01, wValue0x00, wIndex0x00, data_or_wLength0)再发固件就正常了。控制请求这一块不同的固件版本可能会有些差异如果遇到上传失败多试几个bRequest值或者参考一下同型号其他机型的初始化序列。3.3 打印数据传输固件上传完打印机会重新枚举为正常的打印机设备。这时别急着重新find设备建议等一两秒再调用usb.core.find重新获取设备句柄。我用sleep(2)稳一点。然后看打印机的接口描述符正常的LaserJet 1020 plus枚举出来有两个接口一个是控制接口一个是打印数据接口。打印数据接口里有一个批量OUT端点一个批量IN端点。一般OUT端点是0x02IN端点是0x81或0x82。我用代码打印一下for cfg in dev: print(cfg) for intf in cfg: print(intf) for ep in intf: print(ep)实测看到OUT端点是0x02IN端点是0x81。端点确认后打印数据直接往OUT端点写即可。举个例子把已有的PRN文件直接灌进打印机import usb.core import usb.util import time dev usb.core.find(idVendor0x03f0, idProduct0x2b17) if dev is None: raise ValueError(Printer not found) # 设置配置 dev.set_configuration() # 找到打印数据接口 intf dev.get_active_configuration()[(0, 0)] out_ep usb.util.find_descriptor( intf, custom_matchlambda e: usb.util.endpoint_direction(e.bEndpointAddress) usb.util.ENDPOINT_OUT ) PRN_FILE /tmp/test.prn with open(PRN_FILE, rb) as f: chunk_size 16 * 1024 while True: chunk f.read(chunk_size) if not chunk: break out_ep.write(chunk, timeout10000)这里的chunk_size我选的16KB不算大也不算小。打印机的批量端点一次最大可以处理512字节但实际测试发现一次写16KB也不会报错USB控制器会自动拆包。如果你写的时候遇到timeout或者pipe error把chunk_size降到4KB很多兼容性问题都能解决。3.4 状态读取和错误判断打印过程中除了发数据还要关注打印机的状态。LaserJet 1020 plus的IN端点可以用来读取状态字节。常见状态寄存器值在网上能找到比如0x00表示空闲0x10表示正在打印0x80表示错误。我调试时的读取代码try: status dev.read(0x81, 1, timeout1000) print(fPrinter status: {status[0]:02x}) except usb.core.USBTimeoutError: print(No status available (normal if idle))注意读状态时如果设备没有主动上报IN端点会超时这是正常的。不需要每次发数据都读状态那样反而拖慢速度。一般打印任务结束后读一次状态或者打印机卡纸、缺纸时状态寄存器会有变化。3.5 权限和设备节点处理在嵌入式系统里跑Python脚本经常遇到“permission denied”的USB访问错误。这个原因一般是udev规则没配置好。开发板上的当前用户不是root/dev/bus/usb/下的设备文件默认只允许root访问。解决办法是写一条udev规则让普通用户也能访问HP打印机cat /etc/udev/rules.d/50-hp-printer.rules EOF SUBSYSTEMusb, ATTR{idVendor}03f0, ATTR{idProduct}2b17, MODE0666 EOF写完reload一下规则udevadm control --reload-rules udevadm trigger之后重新插拔打印机就算不切rootPython脚本也能正常找到设备。补充一点如果开发板根文件系统是只读的或者/etc是tmpfs这种udev规则要放到启动脚本里动态生成否则重启后丢失。4. 踩坑记录与排查心得4.1 固件上传成功后打印机还是没反应这是我遇到过最诡异的问题。固件明明写入成功lsusb也显示打印机重新枚举了但发打印数据时打印机只是灯亮不进纸也不出纸。后来排查了很久发现问题出在固件上传后的延时上。打印机重新枚举后内部核心固件其实还需要一小段时间完成初始化这个时候如果立刻发打印数据打印机控制芯片还没准备好数据就丢了。解决办法是每次重新枚举后固定sleep 3秒再发数据。实测3秒是稳定值2秒偶尔会失败。4.2 内核usblp驱动抢占设备开发板内核里如果加载了usblp模块并且固件上传比较慢usblp驱动可能会提前把设备绑定到/dev/usb/lp0。这样一来libusb再打开设备就会报“Resource Busy”。最简单的处理办法用libusb前先卸载usblpmodprobe -r usblp或者直接把usblp模块blacklist掉避免内核自动探测打印机。加上这次项目里根本不需要/dev/usb/lp0这个节点blacklist最省事echo blacklist usblp /etc/modprobe.d/blacklist-usblp.conf4.3 打印中途卡住任务卡死大文件打印时一次性把几十MB数据写进端点的风险是缓冲区溢出或者超时。我一开始用的是整个PRN文件写入结果打印到一半经常出现timeout。后来改成固定16KB分块写并且每次写完检查USB错误码。如果遇到pipe error说明打印机已经中止了当前作业必须重新初始化打印机状态不能继续往下写。这个坑在批量传输时特别典型原因是打印机内部缓冲区满了或者遇到了无法解析的数据。4.4 打印质量异常出现乱码或半张空白这种情况要先区分是数据格式问题还是通信问题。如果你给打印机的是PCL数据或者GDI数据格式错了打印机根本不会出纸。乱码、半张空白很大概率是你发的内容不是预期的打印语言。我的做法是在PC上先用官方Windows驱动把测试页打印到文件拿到一个标准的PRN文件再在板子上用脚本原样发送。如果PRN文件打印正常说明通信链路没问题如果不正常再排查USB层面。这个思路非常重要可以直接把问题隔离在协议层还是传输层省掉很多无谓的调试时间。4.5 开发板休眠或USB总线复位后打印失效IMX6Q开发板如果开启了电源管理USB Host控制器在一段时间无操作后可能会进入低功耗模式。打印机再次发送数据时总线还在唤醒过程中批量传输会失败。我的处理办法是在打印脚本开头重新claim接口或者调用usb.util.claim_interface强制占用接口。甚至简单粗暴一点在打印前执行一次echo on /sys/bus/usb/devices/1-1/power/control把USB设备的电源管理关掉避免总线自动挂起。5. 运维实践与状态监控打印机在产线上不是只打印一次就完事实际使用中需要监控状态、处理卡纸、判断缺纸。这里用到的小技巧也顺手分享一下。5.1 用脚本监控打印机连接状态打印机偶尔会被误拔或者USB线松掉需要自动检测并告警。我用一个小脚本定时检查#!/bin/bash while true; do if lsusb -d 03f0:2b17 /dev/null 21; then echo Printer connected else echo Printer disconnected # 可以在这里加蜂鸣器或LED告警 fi sleep 10 done打印机设备还在枚举中但已经死机的情况这个脚本检测不到需要额外通过IN端点读取状态来确认。5.2 打印任务的日志记录每次打印任务完成后建议把文件名、大小、时间戳、状态等记录下来。这在排查问题时非常有用。比如我发现某类文件打印失败一看日志全是文件大小超过32MB的大任务。于是果断在脚本里加大分块数限制单次任务大小故障率立刻降下来。日志代码很简单import datetime log_line f{datetime.datetime.now()} {filename} {filesize} {status}\n with open(/var/log/printer.log, a) as f: f.write(log_line)5.3 队列管理和多任务串行如果打印脚本被多个进程同时调用比如一个监控进程和一个手动打印任务同时触发很可能出现两个进程同时写OUT端点导致数据交织打印乱码。解决方法是加一个简单的文件锁import fcntl lock_file open(/var/run/printer.lock, w) fcntl.flock(lock_file, fcntl.LOCK_EX) # 打印逻辑 fcntl.flock(lock_file, fcntl.LOCK_UN)实测下来这个锁很有效多个打印任务会排队执行不会互相干扰。5.4 最后一个小技巧在IMX6Q上跑USB打印很多人忽略了USB线质量。尽量用带磁环的USB线线长控制在2米以内不要用那种细细的充电线。打印机工作时电流波动大劣质线会让USB总线电压跌落出现间歇性device disconnect。这个问题我折腾的时候也遇到过排查了很久最后换线解决。另外就是如果打印机长期不用可以给打印脚本加一个热启动逻辑发送前先开一个虚拟打印任务让打印机内部初始化完毕再发真正的数据。这个方法虽然看起来有点多余但对老款GDI打印机来说能明显提高第一次打印的成功率。试试就知道。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑