1、项目背景:
需要一个NFC模块为一个Mifare UltraLight的NFC标签写入一个蓝牙MAC地址,让手持设备接触到NFC标签后可以自动连接一个终端的蓝牙。
2、Mifare UltraLight 介绍:
1、 容量512bit,分为16个page,每个page占4byte
2、 每个page可以通过编程的方式锁定为只读功能
3、 384位(从page4往后)用户读写区域
4、 唯一7字节物理卡号(page0前3个byte加page1)
详细可以百度下Mifare UltraLight卡。
3、模块选型
NFC模块:上百度和淘宝搜索了解,发现PN532模块非常受欢迎、资料十分多并且用途非常广泛,于是淘宝一个回来试试。
单片机:使用比较熟悉和通用的STM32F103VET6。
4、方案设计
因项目设计上希望NFC标签一直贴近NFC模块, 所以通过触发单片机控制NFC模块对标签进行单次烧写。
PN532操作步骤为:
唤醒->寻卡->写卡->读卡并校验数据->锁卡(变成只读)。
5、PN532串口驱动代码
PN532有多种硬件接口的通讯方式,我这边是选择了最简单的串口通讯方式,串口通讯主要是使用PN532手册提供的串口通讯命令。
a. 唤醒命令:
const uint8_t wake_up_buf[] = {0x55,0x55,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\
0xFF,0x03,0xFD,0xD4,0x14,0x01,0x17,0x00};
int WakeUpPN532(t_NFC_Ops *nfc_ops){
int ret = nfc_ops->uart_ops->SerialComWrite(wake_up_buf,sizeof(wake_up_buf));
return ret;
}
唤醒命令格式固定,所以定义为const直接,发送成功后PN532模块会被唤醒。
b. 寻卡命令:
int FindNFCCard(t_NFC_Ops *nfc_ops){
int ret = 0;
unsigned char buf[48] = {0};
buf[7] = d_FIND_NFCCARD_MAXNUM;
buf[8] = E_106K_PROTOCOL;
int len = NFC_CmdPackage(FIND_CARD,buf,2);
ret = nfc_ops->uart_ops->SerialComWrite(buf,len);
memset(buf,0,sizeof(buf));
ret = ReadAParseNFCCmd(nfc_ops,FIND_CARD_ACK,buf,d_NFC_RW_TIMEOUT);
return ret;
}
d_FIND_NFCCARD_MAXNUM: 为最大寻卡数
E_106K_PROTOCOL: 使用106K协议
FIND_CARD_ACK:寻卡应答
NFC_CmdPackage()(打包命令到buf,同时计算长度和校验)
ReadAParseNFCCmd()(读取NFC模块返回数据,检查是否收到期望的命令应答)
发送寻卡命令后,若PN532检测到NFC卡,会返回应答(包含卡ID)。
ps:具体函数实现可以下载代码进行查看
c.写卡命令
int WriteNFCCard_4B(t_NFC_Ops *nfc_ops,int block_num,unsigned char* data_buf,int size){
int ret = 0;
unsigned char buf[48] = {0};
if (size > d_WRITE_MAX_SIZE){
printf("write over size\r\n");
return -1;
}
buf[7] = 1;
buf[8] = WRITE_4BYTE;
buf[9] = (unsigned char)block_num;
memcpy(buf+10,data_buf,size);
int len = NFC_CmdPackage(CTL_CARD,buf,3+size);
ret = nfc_ops->uart_ops->SerialComWrite(buf,len);
Print_hex("Send: ",buf,len);
memset(buf,0,sizeof(buf));
ret = ReadAParseNFCCmd(nfc_ops,RW_DATA_ACK,buf,d_NFC_RW_TIMEOUT);
if (ret < 0){
return -1;
}
if (0 == buf[0]){ /* ¼ì²éдÊý¾ÝÊÇ·ñ³É¹¦ */
ret = 0;
}else{
ret = -1;
}
return ret;
}
PN532模块每个块有4个字节空间,使用串口命令每次只能单块写入(一次写4字节)。
根据块号,数据,数据长度对NFC卡进行数据烧写,每次发送命令写入数据后必须读取返回值确定是否写入成功。
d.读卡命令
int ReadNFCCard_16B(t_NFC_Ops *nfc_ops,uint32_t block_num,uint8_t* data_buf,uint32_t size){
int ret = 0;
unsigned char buf[64] = {0};
if (size > d_READ_MAX_SIZE){
printf("read over size\r\n");
return -1;
}
buf[7] = 1;
buf[8] = READ_16BYTE;
buf[9] = (unsigned char)block_num;
int len = NFC_CmdPackage(CTL_CARD,buf,3);
ret = nfc_ops->uart_ops->SerialComWrite(buf,len);
Print_hex("Send: ",buf,len);
memset(buf,0,sizeof(buf));
ret = ReadAParseNFCCmd(nfc_ops,RW_DATA_ACK,buf,d_NFC_RW_TIMEOUT);
if (ret < 0){
return -1;
}
if (0 == buf[0]){ /* ¼ì²é¶ÁÊý¾ÝÊÇ·ñ³É¹¦ */
ret = 0;
memcpy(data_buf,buf+1,size); /* ·µ»Ø¿éÇøÊý¾Ý */
}else{
ret = -1;
}
return ret;
}
PN532模块使用串口命令每次读可以连续读4个块(16字节)。
e.锁卡命令
const uint8_t lock_cmd[] = {0x8A,0x48,0xFF,0xFF};
int LockNFCCard(t_NFC_Ops *nfc_ops){
int ret = WriteNFCCard(nfc_ops,d_LOCK_ADD,lock_cmd,sizeof(lock_cmd));
return ret;
}
d_ LOCK_ADD等于2,Mifare UltraLight卡的块区2的后两个字节可以控制数据区的16个块区变成只读。
6、小结
目前已实现所需功能,不过还有一些不足的地方。如:
1、因PN532和标签隔了产品外壳并且标签比较小,近距离接时没有对得很准不一定能寻找到标签,有时需更加贴近才能寻卡成功。
2、代码方面在烧写标签耗时相对较长(每次发命令都串口阻塞等待100毫秒来获取应答结果)。
关于问题1,看了一下芯片手册没有找到有说明改变NFC灵敏度和功率的寄存器,不知道有没有其他大神知道如何通过更改配置来提高灵敏度。同时笔者目前正在调试PN5180这个高频模块,看能不能更换5180解决灵敏度问题。