第一章 驱动开发环境搭建

大家好,我是老张。做POS机打印机驱动开发这么多年,我最大的体会就是——环境搭不好,后面全是坑。今天咱们就从零开始,把Linux内核编译环境、交叉编译工具链、还有USB驱动框架这三个基础打牢。

说实话,很多新手一上来就急着写代码,结果编译报错半天找不到原因。我建议你先把环境搞利索,后面会省很多事。

1.1 Linux内核编译环境配置

先说说内核编译环境。POS机打印机驱动,说白了就是内核模块。你得先有个能编译内核的环境。

我个人习惯用Ubuntu 18.04或20.04 LTS。为什么?因为POS机厂商的SDK大多基于这两个版本。你想想看,要是用最新的Ubuntu 22.04,有些老工具链反而不兼容。

安装基础依赖包,直接一条命令搞定:

sudo apt-get update
sudo apt-get install build-essential libncurses-dev bison flex libssl-dev libelf-dev

这里有个坑——libssl-devlibelf-dev容易漏掉。我在项目中遇到过,编译到一半报错说找不到openssl/ssl.h,那叫一个郁闷。

然后下载内核源码。我建议用和你的目标系统相同版本的内核。比如打印机主板上跑的是Linux 4.14,你就下4.14:

wget https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.14.tar.xz
tar -xf linux-4.14.tar.xz
cd linux-4.14

配置内核时,别用默认配置。我一般这样操作:

make menuconfig

进去以后,重点检查这几个选项:

  • Device Drivers → USB support —— 必须开启
  • Device Drivers → USB support → USB Printer support —— 选上
  • Device Drivers → Staging drivers —— 有些打印机芯片驱动在这里
小技巧: 如果你不确定哪些要开,可以先 make defconfig,再手动加 USB 相关选项。我刚开始就是这么干的。

配置完保存,然后编译:

make -j4

-j4 表示用4个线程编译。你要是电脑性能好,可以调高。我第一次编译等了快一小时,后来才知道加 -j 参数。

1.2 交叉编译工具链安装

POS机打印机的主控芯片,大多是ARM架构。你不能直接在x86电脑上编译ARM的驱动。这时候就需要交叉编译工具链。

说白了,就是一套能在PC上生成ARM可执行文件的工具。

常见的工具链有:

芯片架构 工具链名称 常见场景
ARM Cortex-A7 arm-linux-gnueabihf- 全志、瑞芯微方案
ARM Cortex-A53 aarch64-linux-gnu- 高通、MTK方案
MIPS mips-linux-gnu- 老款热敏打印机

安装方法有两种。第一种,直接用apt安装:

sudo apt-get install gcc-arm-linux-gnueabihf

第二种,从芯片厂商官网下载。比如全志的SDK里自带工具链。我个人更推荐第二种,因为版本匹配度更高。

安装完后,验证一下:

arm-linux-gnueabihf-gcc --version

能显示版本号,就说明装好了。

注意: 交叉编译工具链的版本要和内核版本匹配。我曾经用gcc 9编译Linux 3.10的内核模块,结果加载时报错“invalid module format”。折腾了两天才发现是工具链版本太新。

然后设置环境变量。我习惯写到 ~/.bashrc 里:

export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-
export PATH=$PATH:/opt/gcc-arm-8.3/bin

这样每次打开终端,就不用重新设了。

1.3 USB驱动框架初探

好了,环境搭好了,咱们来看看USB驱动到底是个什么结构。

POS机打印机,绝大多数走的是USB接口。Linux内核里,USB驱动分三层:

  • USB核心层 —— 内核自带,不用你写
  • USB设备驱动层 —— 咱们要写的部分
  • USB主机控制器驱动 —— 芯片厂商提供

我们关注的是中间那层。一个最简单的USB打印机驱动,长这样:

#include <linux/module.h>
#include <linux/usb.h>

static struct usb_device_id printer_table[] = {
    { USB_DEVICE(0x0416, 0x5011) },  // 厂商ID:0x0416, 产品ID:0x5011
    { }
};
MODULE_DEVICE_TABLE(usb, printer_table);

static int printer_probe(struct usb_interface *intf, 
                         const struct usb_device_id *id)
{
    printk(KERN_INFO "Printer connected!\n");
    return 0;
}

static void printer_disconnect(struct usb_interface *intf)
{
    printk(KERN_INFO "Printer disconnected!\n");
}

static struct usb_driver printer_driver = {
    .name       = "pos_printer",
    .probe      = printer_probe,
    .disconnect = printer_disconnect,
    .id_table   = printer_table,
};

module_usb_driver(printer_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");

这段代码干了三件事:

  1. 声明了设备ID表——告诉内核这个驱动支持哪些打印机
  2. 实现了probe函数——打印机插上时调用
  3. 实现了disconnect函数——打印机拔掉时调用

嗯,这里要注意:USB_DEVICE 里的两个参数,分别是厂商ID和产品ID。你怎么知道打印机的ID是多少?用 lsusb 命令:

lsusb
Bus 001 Device 003: ID 0416:5011 Winbond Electronics Corp.

看到没?0416:5011 就是。我在项目里经常遇到新手问“我的打印机ID是多少”,其实就是这么查的。

核心要点: USB驱动的本质,就是告诉内核“当这个设备插上来时,调用我的probe函数”。剩下的数据收发,都在probe里初始化。

编译这个驱动,用Makefile:

obj-m += printer_drv.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

如果你是交叉编译,改成:

obj-m += printer_drv.o

KERNEL_DIR := /path/to/your/kernel

all:
    make -C $(KERNEL_DIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules

clean:
    make -C $(KERNEL_DIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- clean

编译出来是 .ko 文件。传到打印机主板上,用 insmod 加载:

insmod printer_drv.ko

然后用 dmesg 看内核日志,如果看到 "Printer connected!",就说明驱动加载成功了。

为什么会这样?因为probe函数被调用了。这就是USB驱动最基础的流程。

好了,第一章就到这里。环境搭好、工具链装好、框架看懂,后面咱们就可以开始写真正的打印机驱动了。下一章,我会讲怎么和打印机握手通信——说白了,就是怎么发指令让它打印。