第1章:串口通信基础

做POS机磁条卡读卡器驱动,串口通信是绕不开的第一道坎。说白了,磁条卡读卡器跟主控芯片之间,绝大多数走的都是串口协议。我刚开始接触这个领域时,也以为串口很简单——不就是发数据收数据嘛。结果第一次调通读卡器,发现数据全是乱码,折腾了一整天。嗯,问题就出在串口参数配置上。

1.1 波特率:通信的“节拍器”

波特率,就是每秒传输多少个比特位。常见的值有9600、19200、38400、115200等。POS机磁条卡读卡器,我见过最多的用的是9600和115200。

为什么这两个值最常见?因为磁条卡数据量不大,三轨数据加起来也就几百字节,9600完全够用。但有些高速读卡器,为了减少用户等待时间,会用到115200。

关键点:发送端和接收端的波特率必须完全一致。差一点点都不行。

我在项目中遇到过一件事:读卡器偶尔丢数据,查了半天,发现是主控晶振精度不够,导致实际波特率跟设定值差了2%。从那以后,我选型时都会看晶振的精度,至少±1%以内才放心。

1.2 数据位:一次传多少数据

数据位,就是每个数据包携带的有效数据位数。常见的有7位和8位。

  • 7位数据位:老式设备用得多,比如一些ASCII码传输的场景
  • 8位数据位:现代设备的标准,磁条卡读卡器几乎全是8位

你想想看,磁条卡数据里包含二进制信息,7位只能表示0-127,根本不够用。所以,我建议你直接固定用8位数据位,省心。

1.3 停止位:帧结束的标志

停止位,用来告诉接收方“这一帧数据结束了”。常见的有1位、1.5位和2位。

停止位 适用场景
1位 大多数现代设备,包括磁条卡读卡器
1.5位 老式设备,现在很少见
2位 对时序要求苛刻的环境,比如长距离传输

我个人习惯,磁条卡读卡器一律用1位停止位。为什么?因为读卡器跟主控通常在同一块板子上,距离短,干扰小,1位足够。用2位反而浪费带宽。

1.4 校验位:数据正确性的“保险丝”

校验位,用来检测数据传输过程中有没有出错。常见的有无校验、奇校验、偶校验。

  • 无校验:不添加校验位,传输效率最高
  • 奇校验:保证数据中1的个数为奇数
  • 偶校验:保证数据中1的个数为偶数

我的建议:磁条卡读卡器驱动,用无校验就够了。因为磁条卡数据本身有校验机制(LRC校验),串口层再加校验,有点多余。我曾经试过奇校验,结果发现读卡器返回的数据偶尔会多一个字节,排查下来是校验位配置不一致导致的。

1.5 串口打开/关闭/读写API封装

好了,理论知识讲完了。接下来是实战——怎么把串口操作封装成好用的API。

我见过很多新手,直接在业务代码里写open、read、write,结果代码乱成一锅粥。正确的做法是:封装成独立的串口驱动层。

1.5.1 打开串口

int serial_open(const char *port, int baudrate, int data_bits, 
                int stop_bits, int parity)
{
    int fd;
    struct termios options;

    // 打开设备文件
    fd = open(port, O_RDWR | O_NOCTTY | O_NDELAY);
    if (fd < 0) {
        return -1;
    }

    // 获取当前参数
    tcgetattr(fd, &options);

    // 设置波特率
    cfsetispeed(&options, baudrate);
    cfsetospeed(&options, baudrate);

    // 设置数据位
    options.c_cflag &= ~CSIZE;
    switch (data_bits) {
        case 7: options.c_cflag |= CS7; break;
        case 8: options.c_cflag |= CS8; break;
        default: return -1;
    }

    // 设置停止位
    if (stop_bits == 2) {
        options.c_cflag |= CSTOPB;
    } else {
        options.c_cflag &= ~CSTOPB;
    }

    // 设置校验位
    switch (parity) {
        case 'N': // 无校验
            options.c_cflag &= ~PARENB;
            break;
        case 'O': // 奇校验
            options.c_cflag |= PARENB;
            options.c_cflag |= PARODD;
            break;
        case 'E': // 偶校验
            options.c_cflag |= PARENB;
            options.c_cflag &= ~PARODD;
            break;
        default: return -1;
    }

    // 应用设置
    tcsetattr(fd, TCSANOW, &options);

    return fd;
}

注意:打开串口时,一定要用O_NOCTTY标志,防止串口成为控制终端。我曾经忘了加这个,结果程序一运行,键盘输入全跑到串口上去了,调试了半天才发现。

1.5.2 关闭串口

int serial_close(int fd)
{
    if (fd >= 0) {
        close(fd);
        return 0;
    }
    return -1;
}

关闭串口看似简单,但有个坑:如果你在读写过程中强制关闭,可能会丢失缓冲区里的数据。我建议在关闭前,先清空缓冲区。

int serial_close_safe(int fd)
{
    if (fd >= 0) {
        tcflush(fd, TCIOFLUSH);  // 清空输入输出缓冲区
        close(fd);
        return 0;
    }
    return -1;
}

1.5.3 读写串口

int serial_write(int fd, const unsigned char *data, int len)
{
    int ret;

    ret = write(fd, data, len);
    if (ret != len) {
        // 写入失败或未写完
        return -1;
    }
    return ret;
}

int serial_read(int fd, unsigned char *buf, int buf_len, int timeout_ms)
{
    fd_set read_fds;
    struct timeval timeout;
    int ret;

    FD_ZERO(&read_fds);
    FD_SET(fd, &read_fds);

    timeout.tv_sec = timeout_ms / 1000;
    timeout.tv_usec = (timeout_ms % 1000) * 1000;

    ret = select(fd + 1, &read_fds, NULL, NULL, &timeout);
    if (ret <= 0) {
        return -1;  // 超时或出错
    }

    if (FD_ISSET(fd, &read_fds)) {
        return read(fd, buf, buf_len);
    }

    return -1;
}

经验之谈:读串口时,千万别用阻塞式read,否则程序会卡死。用select加超时,是嵌入式开发的标准做法。我刚开始做驱动时,就吃过这个亏——读卡器没插卡,程序一直卡在read上,整个系统都动不了。

1.6 完整的串口配置示例

把上面这些串起来,一个典型的磁条卡读卡器串口初始化代码长这样:

int main()
{
    int fd;
    unsigned char cmd[] = {0x02, 0x30, 0x03};
    unsigned char buf[256];

    // 打开串口:波特率9600,8位数据位,1位停止位,无校验
    fd = serial_open("/dev/ttyS0", B9600, 8, 1, 'N');
    if (fd < 0) {
        printf("串口打开失败\n");
        return -1;
    }

    // 发送读卡命令
    serial_write(fd, cmd, sizeof(cmd));

    // 等待读卡器返回数据
    int len = serial_read(fd, buf, sizeof(buf), 1000);
    if (len > 0) {
        printf("收到 %d 字节数据\n", len);
    }

    serial_close_safe(fd);
    return 0;
}

嗯,到这里,串口通信的基础就讲完了。说白了,串口配置就是四个参数:波特率、数据位、停止位、校验位。只要这四个参数跟读卡器对上,通信就成功了一半。剩下的,就是封装好打开、关闭、读写的API,让上层业务代码调用起来干净利落。

下一章,我会讲磁条卡读卡器的具体通信协议。到时候你会发现,串口基础打好了,后面的事情就顺理成章了。