第一章 驱动开发环境搭建
大家好,我是老张。做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-dev和libelf-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 -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
能显示版本号,就说明装好了。
然后设置环境变量。我习惯写到 ~/.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");
这段代码干了三件事:
- 声明了设备ID表——告诉内核这个驱动支持哪些打印机
- 实现了probe函数——打印机插上时调用
- 实现了disconnect函数——打印机拔掉时调用
嗯,这里要注意:USB_DEVICE 里的两个参数,分别是厂商ID和产品ID。你怎么知道打印机的ID是多少?用 lsusb 命令:
lsusb
Bus 001 Device 003: ID 0416:5011 Winbond Electronics Corp.
看到没?0416:5011 就是。我在项目里经常遇到新手问“我的打印机ID是多少”,其实就是这么查的。
编译这个驱动,用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驱动最基础的流程。
好了,第一章就到这里。环境搭好、工具链装好、框架看懂,后面咱们就可以开始写真正的打印机驱动了。下一章,我会讲怎么和打印机握手通信——说白了,就是怎么发指令让它打印。