第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,让上层业务代码调用起来干净利落。
下一章,我会讲磁条卡读卡器的具体通信协议。到时候你会发现,串口基础打好了,后面的事情就顺理成章了。