第二章 打印机协议基础:ESC/POS指令集解析、初始化序列、打印头控制命令

好,咱们正式开始啃协议这块硬骨头。

做打印机驱动,说白了就是跟协议打交道。你写的每一行代码,最终都会变成一串十六进制数据,通过串口或者USB发给打印机。打印机收到后,再根据协议去解析、执行。所以,理解ESC/POS协议,是咱们这行的基本功。

2.1 ESC/POS是什么?

ESC/POS是爱普生搞出来的一套打印控制语言。现在几乎所有的热敏票据打印机都在用。它不是什么高深莫测的东西,就是一套约定好的命令格式。

每条命令都以ESC(0x1B)或者GS(0x1D)开头,后面跟着功能码和参数。打印机看到0x1B,就知道“哦,后面是命令,不是要打印的字符”。

核心思想: 命令 = 前缀 + 功能码 + 参数

举个例子,你想让打印机换行。发送0x0A就行。这个0x0A就是换行命令,也叫LF(Line Feed)。简单吧?

2.2 初始化序列——每次上电的第一件事

我个人习惯,在驱动初始化或者每次打印任务开始前,第一件事就是发初始化命令。这就像你开机先重启一下路由器,把状态清干净。

初始化命令是:ESC @,也就是0x1B 0x40

// 初始化打印机
uint8_t init_cmd[] = {0x1B, 0x40};
send_to_printer(init_cmd, sizeof(init_cmd));

这条命令干了什么呢?

  • 清空打印缓冲区
  • 恢复默认的字符集和字体
  • 重置行间距和页长
  • 关闭所有装饰效果(加粗、下划线等)

注意: 初始化不会清除打印头的位置。如果你在打印中途断电,再上电初始化,纸可能还在半中间。所以,我建议初始化后,紧接着发一个切纸或者走纸命令,把纸归位。

我在项目中遇到过一个问题:某款打印机在连续打印多张小票后,突然出现乱码。排查了半天,发现是前一张小票的打印参数没复位,影响了下一张。从那以后,我每条小票开头都强制发一次ESC @,再也没出过问题。

2.3 打印头控制命令——让打印头动起来

打印头控制,说白了就是控制打印头怎么走、打多宽、打多密。这里有几个最常用的命令,我挑重点讲。

2.3.1 打印模式设置

你可以让打印机打出加粗、倒置、下划线等效果。命令格式是:ESC ! n,其中n是一个位掩码。

功能 说明
Bit 0 字体选择 0=标准字体,1=压缩字体
Bit 1 加粗 1=加粗
Bit 2 倍高 1=高度加倍
Bit 3 倍宽 1=宽度加倍
Bit 4 下划线 1=启用下划线
// 设置加粗+倍宽
uint8_t mode_cmd[] = {0x1B, 0x21, 0x0A};  // 0x0A = 0000 1010
send_to_printer(mode_cmd, sizeof(mode_cmd));

小技巧: 你想想看,如果同时设置倍高和倍宽,打印出来的字就是原来的四倍大。做促销海报小票时特别有用。

2.3.2 走纸控制

走纸命令控制纸张前进的距离。最常用的是:

  • LF (0x0A):走纸一行
  • ESC J n:走纸n/144英寸
  • ESC d n:走纸n行

嗯,这里要注意。不同打印机的行高定义可能不一样。有的打印机一行是24点,有的是30点。我曾经在适配一款国产打印机时,发现走纸距离总是不对。后来一查手册,它的行高是30点,而我按24点算的。所以,一定要看打印机的手册,别想当然。

2.3.3 切纸命令

带切刀的热敏打印机,切纸命令是:GS V m 或者 GS V m n

// 全切纸
uint8_t cut_cmd[] = {0x1D, 0x56, 0x00};
send_to_printer(cut_cmd, sizeof(cut_cmd));

这里m=0是全切,m=1是半切(留一点连着,方便撕)。

避坑指南: 我曾经在调试时,切纸命令发得太快,打印机还没走完纸就切了,结果卡纸。后来我加了一个延时,确保最后一行内容打印完成后再发切纸命令。建议至少等待200ms。

2.4 位图打印——进阶玩法

如果你要打印Logo或者二维码,就得用到位图打印命令。命令是:GS v 0 m xL xH yL yH d1...dk

这个命令稍微复杂点。参数xL xH是图片宽度(以字节为单位),yL yH是图片高度(以点为单位)。后面的d1...dk就是像素数据。

// 打印一个8x8的黑色方块
uint8_t bitmap_cmd[] = {
    0x1D, 0x76, 0x30,  // GS v 0
    0x01, 0x00,        // 宽度 = 1字节 (8点)
    0x08, 0x00,        // 高度 = 8点
    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF  // 8字节像素数据
};
send_to_printer(bitmap_cmd, sizeof(bitmap_cmd));

个人经验: 位图数据是按列排列的,不是按行。很多新手在这里栽跟头。你想想看,如果你按行排列数据,打印出来的图片就是旋转了90度的。我刚开始也犯过这个错,调试了一下午才发现。

2.5 状态查询——让打印机说话

打印机不是哑巴。你可以通过命令查询它的状态。比如:

  • DLE EOT n:查询实时状态(缺纸、开盖、卡纸等)
  • GS r n:查询打印机状态

为什么需要这个?因为POS机场景下,你不能假设打印机永远在线。顾客等着打印小票,结果打印机没纸了,你得提前知道,然后提示用户。

// 查询缺纸状态
uint8_t status_cmd[] = {0x10, 0x04, 0x01};
send_to_printer(status_cmd, sizeof(status_cmd));
uint8_t response = read_from_printer();
if (response & 0x20) {
    // 缺纸了!
    printf("Paper out!\n");
}

核心要点: 状态查询命令的返回值,不同打印机可能不一样。一定要看手册中的“状态表”。我遇到过一款打印机,缺纸时返回0x60,另一款返回0x20。所以,别偷懒,查手册。

2.6 总结一下

ESC/POS协议,说白了就是一套“你发命令,打印机执行”的规则。你只要记住:

  1. 初始化命令ESC @是保命符,每次打印前都发一次
  2. 打印模式用ESC ! n控制,位掩码要算清楚
  3. 走纸和切纸要配合好,别让打印机卡纸
  4. 位图数据按列排列,别搞反了
  5. 状态查询是必备功能,别让用户干等

下一章,咱们会深入讲串口通信的时序和波特率匹配问题。到时候我会分享一个我调试了三天三夜的坑,保证让你印象深刻。