0、前言

Github项目地址:AS608

  • 项目实现了官方用户开发手册中所有列出的功能,函数声明在 as608.h中。用户可直接调用相应的函数与AS608模块进行通信。

  • 另外,项目中有一个命令行程序,可以在终端下通过命令与模块进行交互。

几天前在某宝上买了的AS608模块。店家给了相关资料,有一个AS60x指纹识别SOC用户手册V10,里面详细介绍了模块的工作方式和通信过程。简单来说,通过串口发给模块相应的指令(指令包,由指令头+参数+检校和构成),模块返回应答包,读取应答包的内容取出指令执行的结果。

先是通过usb转ttl工具(如下图),连接到笔记本电脑上。(PS. 就这玩意儿,我以前一位就是usb转3.3v或5v,临时当电源用呢,谁知道竟然是usb转TTL,用于串口通信的,长知识了)

usb-to-tll

通过软件(下图左)与AS608模块通信,确认模块工作正常。又通过XCOM软件(下图右)调试模块,并结合官方开发手册,深入理解了模块的工作流程。

win-software

但是店家并没有给树莓派使用该模块的使用说明和资料,我在网上也没有找到任何相关的资料,单片机的资料倒是不少,但根本不适用树莓派。可能很少有人在树莓派上使用该模块吧。

所以我就自己开发了一套库函数,封装了通信流程,使用时,直接调用函数即可,更为直观简便。

本来只是想在主函数中写简单的代码,测试一下函数是否正常工作,后来一想,不如直接开发一个程序,实现模块的所有功能,于是就有了项目中的命令行程序fp (fingerprint的缩写)。

usage-1

usage-2

更多功能,参见Github项目:AS608

1、十六进制?

与AS608模块的通信方式,就是向它发送16进制数据流。

以最简单的为例,读系统的基本参数,官方手册给的指令包如下

1566308020177

那吗,怎么构造指令包,用什么变量呢?

  • 一、"EF01FFFFFFF0100030F0013"

  • 二、{ 0xEF,0x0x,0xFF,0xFF,0xFF,0xFF,0x01,0x00,0x03,0x0F,0x00,0x13 }

当然,第二种是正确的,而且等价于字符串"\xEF\x0x\xFF\xFF\xFF\xFF\x01\x00\x03\x0F\x00\x13",实际上也是这么构造的

unsigned char order[12] = { 0xEF,0x0x,0xFF,0xFF,0xFF,0xFF,0x01,0x00,0x03,0x0F,0x00,0x13 }
// 或者
const unsigned char* order = "\xEF\x0x\xFF\xFF\xFF\xFF\x01\x00\x03\x0F\x00\x13";

char (或者 unsigned char)本质上是一种整数类型,只是同时又能表示字符而已。

第一种的"EF01FFFFFFF0100030F0013"在实际上存储的是{ 0x45,0x46,0x30,0x31,0x46,0x46,0x46,0x46,0x46,0x46,0x46,0x30,0x31,0x30,0x30,0x30,0x33,0x30,0x46,0x30,0x30,0x31 },所以,如果这样写的话,发送的给模块的显然是错误的指令,甚至是不能识别的。

2、位运算

由于芯片地址不是固定的,用户可以随时更改,而且指令包中可以带有参数,所以指令肯定不能写死,如:

const unsigned char* order = "\xEF\x0x\xFF\xFF\xFF\xFF\x01\x00\x03\x0F\x00\x13";

需要这样写,

unsigned char order[12] = { 0 };
// 然后填充order数组的每一位
// do something

as608.c文件中,有一个辅助函数,定义大致如下:

/*
 * 把一个无符号整数,拆分为多个单字节整数
 *    如num=0xa1b2c3d4,拆分为0xa1, 0xb2, 0xc3, 0xd4,
 *    存入 buf+0, buf+1, buf+2, buf+3 位置处
*/
void Split(uint num, uchar* buf, int count) {
  for (int i = 0; i < count; ++i) {
    *buf++ = (num & 0xff << 8*(count-i-1)) >> 8*(count-i-1);
  }
}

如何使用呢,比如,需要把芯片地址写入指令字符串order中,芯片需要占用四个字节,也就是数组的四个元素位置(order[2] order[3] order[4] order[5])。而芯片地址存储在全局变量PS_CHIP_ADDR(unsigned int类型)中。unsigned int占4个字节,unsigned char占1个字节。如何把PS_CHIP_ADDR存入order指定位置中呢。

只需调用函数 Split(PS_CHIP_ADDR, order + 2, 4)

再来看看函数是如何实现的。假设,芯片地址为0xA1B2C3D4,那么处理结果应该是

order[2] = 0xA1;
order[3] = 0xB2;
order[4] = 0xC3;
order[5] = 0xD4;

由位运算规则可知,

0xA1B2C3D4 & 0xFF000000 ---> 0xA1000000   // 提取前两位16进制数字,即八位2进制数字
0xA1000000 >> 24  --->  0x000000A1 (即 0xA1)  // 右移24位,得到0xA1,赋值给order[2]

同理,

0xA1B2C3D4 & 0x00FF0000 ---> 0x00B20000  // 提取前第3~4位16进制数字,即八位2进制数字
0x00B20000 >> 16  --->  0x00000B2 (即 0xB2) // 右移16位,得到0xB2,赋值给order[3]
0xA1B2C3D4 & 0x0000FF00 ---> 0x0000C300  // 提取前第5~6位16进制数字,即八位2进制数字
0x0000C300 >> 8  --->  0x00000C3 (即 0xC3) // 右移8位,得到0xC3,赋值给order[4]
0xA1B2C3D4 & 0x000000FF ---> 0x000000D4  // 提取前第7~8位16进制数字,即八位2进制数字
0x000000D4 >> 0  --->  0x00000D4 (即 0xD4) // 右移0位,得到0xD4,赋值给order[5]

另外:还有一个与该函数相反操作的函数,用于从 模块的应答包 中取出数据,如把用两个unsigned char存储的数值赋值给一个int类型数据。

/*
 * 把多个单字节整数(最多4个),合并为一个unsigned int整数
 *      如0xa1, 0xb2, 0xc3, 0xd4, 合并为0xa1b2c3d4
*/
bool Merge(uint* num, const uchar* startAddr, int count) {
  *num = 0;
  for (int i = 0; i < count; ++i)
    *num += (int)(startAddr[i]) << (8*(count-i-1)); 

  return true;
}

PS. 这里返回true看似无意义,实际是为了将函数能够写在 && 和 || 两边。(返回类型为void的不能)

程序中经常需要执行一连串的函数,任何一个被调函数返回 false ,则主调函数就返回false, 如:

 bool foo(int* val) {
     // do something ....
     
     return (RecvReply(g_reply, 14) && 
             Check(g_reply, 14) &&
             Merge(val, g_reply+10, 2));
}

3、运算符优先级

+ 的优先级比 >><<

int ret = num1 + num2 >> 8;

等价于

int ret = (num1 + num2) >> 8;  // √
int ret = num1 + (num2 >> 8);  // ×

即先执行加法,再将结果右移8位。并非先把num2右移8位,在加上num1!!!

4. fgets() 遇上 feof()

linux平台下,使用vim向文件 data 中写入如下内容(共3行)

address=0xffffffff
password=none
serial=/dev/ttyAMA0

文件格式为:内容+EOF 。
其中 EOF为vim文件内容的结束标志。

当用C语言读取文件内容时,文件指针要指向字符EOF之后才能判断文件已经结束。

当用fgets()读完文件最后一行后,文件指针指向EOF字符,但此时使用feof()判断,发现文件并未结束,而实际上已经结束,只有再次调用feof(),才会返回false

假如现在要输出文件data中的内容

int main() {
    FILE* fp = fopen("./data", "r");
    
    char buf[32] = { 0 };
    int lineCount = 0;
    while (!feof(fp)) {
        fgets(buf, 32, fp);
        printf("%s", buf);
        lineCount++;
    }
    
    printf("Line count is %d\n", lineCount);
    return 0;
}

在windows下这么写没有问题,但是,在Linux系统下

输出结果为

address=0xffffffff
password=none
serial=/dev/ttyAMA0
serial=/dev/ttyAMA0
Line count is 4

显然,最后一行输出了两次 !!

所以,应该这么写while循环

while (!feof(fp)) {
    fgets(buf, 32, fp);

    if (feof(fp))   // 判断一下是否读到了EOF字符(此时文件指针在EOF之后)
        break;

    printf("%s", buf);
    lineCount++;
}

4、不定参数

type value = va_arg(ap, type);  // 取出一个参数

type绝对不能为以下类型:
char、signed char、unsigned char short、unsigned short signed short、short int、signed short int、unsigned short int float

float类型的实际参数将提升到double
charshort和相应的signedunsigned类型的实际参数提升到int
如果int不能存储原值,则提升到unsigned int

5、进度条

通过 printf("\r") 实现。'\r'可以将光标移动到本行开头,继续打印的字符将覆盖本行的内容。

这里给出一个通用的函数,如果程序中有循环语句,调用该函数就能打印进度条。

/*
 * 显示进度条
 * 参数:done(已完成的量)  all(总量)
*/
void PrintProcess(int done, int all) {
  // 进度条,0~100%,50个字符
  char process[64] = { 0 };
  double stepSize = (double)all / 100;
  int doneStep = done / stepSize;
  
  // 如果步数是偶数,更新进度条
  // 因为用50个字符显示100个进度值
  for (int i = 0; i < doneStep / 2 - 1; ++i)
    process[i] = '=';
  process[doneStep/2 - 1] = '>';

  printf("\rProcess:[%-50s]%d%% ", process, doneStep);
  if (done < all)
    fflush(stdout);
  else
    printf("\n");
}
for (int i = 1; i <= 180; ++i) {
    PrintProcess(i, 180);
    usleep(100000); // 暂停0.1s
}

输出结果如下

1566359283952

1566359857572

END

Author:iC

Email:leopard.c@outlook.com

Thanks for Reading !

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐