NFC图片刷写器制作
之前做的无源NFC墨水屏制作发现对于一些手机识别不太好(应该是天线设计的问题),我已经做成了几个设备,然后邮寄给我朋友发现他们手机识别效果不好,这是我做 NFC图片刷写器的直接原因。毕竟如果不能更换图片,我不如直接给我朋友发一张滴胶封的纸 : (效果演示。
1. 前言
之前做的 无源NFC墨水屏制作 发现对于一些手机识别不太好(应该是天线设计的问题),我已经做成了几个设备,然后邮寄给我朋友发现他们手机识别效果不好,这是我做 NFC图片刷写器的直接原因。毕竟如果不能更换图片,我不如直接给我朋友发一张滴胶封的纸 : (

2. 硬件选择与设计
2.1 硬件选择
NFC传输部分,我考虑用现成的PN523设备:
其通过上面的拨码开关可切换以下三种模式:
| 模式 | 作用 |
|---|---|
| HSU | 大部分解密IC卡都是通过此模式完成的,因为不需要主控MCU,直接用个USB-TTL小板即可与这个板子进行通信, 通过串口发送数据帧完成对目标卡的读写 |
| I2C | 需要主控MCU, 与这块板子进行I2C连接,发送数据帧实现对目标卡的读写 |
| SPI | 与I2C相同,只不过使用的协议是SPI |
主控MCU使用ESP-12模块,联网比较方便,传输图片就可以跨平台使用了。
要使用MCU的 HSU模式也需要有一个 USB传TTL的芯片(ch34c)。
2.2 硬件设计
ESP-12模块通过SPI协议与pn532小红板相连, 还要保留其HSU模式,这样我还可以使用这个设备对IC卡解密(有很多配套的软件),所有还需要一个 USB转ttl芯片(ch340c),ch340c还可以给 ESP-12模块进行下载程序调试日志。
所有这里使用一个单刀双置开关,用于切换ch340c 与 ESP-12模块连接 和 PN532小红板的连接。


原理图没什么可说的,很简单,关键在于PCB的绘制螺丝孔位置,我前后打了俩次板子,总是对不齐,开始时我用尺子测量,导致俩个定位孔总是有偏差。
后来我的小红板进行拍照,然后进行透视变换最后等比例放到PCB绘制中才完美的进行孔位对齐。(如果有更好的办法也希望评论区大佬给我些思路)
底板 通过排针和底座与 pn532小红板相连,并且我加入了螺柱,螺柱型号为: M2*11+4
2.3 其他硬件问题
在底板做成后,我发现识别信号很不好,导致我改版了几次,考虑因为底板铺铜影响识别,去除了底板铺铜,但是效果依然不好,应该是esp12模块上的屏蔽罩是一块完整导体,所有这里我还用了 铁氧防磁 贴,贴在了 小红板下方,识别效果立刻有显著提升:

3. 软件部分
其实值得说的就是软件部分,因为网上对 pn532对ntag读写资料很少,也花费了我很长时间。这里我用Arduino框架+ Adafruit_PN532 库实现。
软件部分主要实现的功能:
- 通过网络对esp12f模组发送图片数据
- esp12f接收到数据图片将数据发送给pn532小红板(spi协议)
3.1 esp8266写数据
向 nt3h2111芯片的里面写入数据:
nfc.ntag2xx_WritePage(address, data);
nfc.ntag2xx_ReadPage(address, data);
可以通过这俩个函数对 nt3h2111 进行读写数据,不过其限制写入的地址:
uint8_t Adafruit_PN532::ntag2xx_ReadPage(uint8_t page, uint8_t *buffer) {
// TAG Type PAGES USER START USER STOP
// -------- ----- ---------- ---------
// NTAG 203 42 4 39
// NTAG 213 45 4 39
// NTAG 215 135 4 129
// NTAG 216 231 4 225
if (page >= 231) {
#ifdef MIFAREDEBUG
PN532DEBUGPRINT.println(F("Page value out of range"));
#endif
return 0;
}
#ifdef MIFAREDEBUG
PN532DEBUGPRINT.print(F("Reading page "));
PN532DEBUGPRINT.println(page);
#endif
如果大于0xe7 就会报错 Page value out of range。所以这里可以通过这个函数去改写NDEF 数据(EEPROM中),而SRAM在开启passthrough后其对应NFC内存布局地址是 F0-FF:

在NFC接口下,不同于I2C接口16字节数据, 其一个地址对应4个字节,所以16*4 = 64 字节正好也是 64字节SRAM的大小。
所以可以对 Adafruit_PN532 框架的代码进行修改,突破 0xe7 的限制(注释即可)。
3.2 启用FAST WRITE
参考:https://community.nxp.com/t5/NFC/I-can-t-read-the-session-register-in-NT3H1101-from-RF-side-with/td-p/558556
FAST WRITE指令 是非标准的数据交换指令,如果要实现这个功能就需要调用更底层的函数:
nfc.sendCommandCheckAck(cmd, cmdlen, timeout)
对于调试Adafruit_PN532 库 需要开启 #define PN532DEBUG
这样可以看到更底层的数据帧发送的情况:
Sending : 0x0, 0x0, 0xFF, 0x45, 0xBB, 0xD4, 0x42, 0xA6, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF6, 0xAD, 0x6A, 0xEA, 0xAA, 0xD7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFA, 0xD6, 0xB7, 0x55, 0x55, 0x57, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF5, 0x5B, 0x5A, 0xAA, 0xAA, 0xAF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF5, 0x6A, 0xAA, 0xB5, 0xB5, 0x57, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF2, 0xAA, 0xAA, 0xAA, 0xD5, 0x2F, 0xC, 0x0,
41

数据帧构造:
uint8_t data[68];
data[0] = 0x42; // FAST_WRITE 指令代码
data[1] = 0xA6; // Cmd
data[2] = 0xF0; // START_ADDR
data[3] = 0xFF; // END_ADDR
data中剩余的64字节就是图片数据,值得一提的是,后面的CRC 无需构造,这个是库处理的:
void Adafruit_PN532::writecommand(uint8_t *cmd, uint8_t cmdlen) {
if (spi_dev) {
// SPI command write.
uint8_t checksum;
uint8_t packet[9 + cmdlen];
uint8_t *p = packet;
cmdlen++;
p[0] = PN532_SPI_DATAWRITE;
p++;
p[0] = PN532_PREAMBLE;
p++;
p[0] = PN532_STARTCODE1;
p++;
p[0] = PN532_STARTCODE2;
p++;
checksum = PN532_PREAMBLE + PN532_STARTCODE1 + PN532_STARTCODE2;
p[0] = cmdlen;
p++;
p[0] = ~cmdlen + 1;
p++;
p[0] = PN532_HOSTTOPN532;
p++;
checksum += PN532_HOSTTOPN532;
for (uint8_t i = 0; i < cmdlen - 1; i++) {
p[0] = cmd[i];
p++;
checksum += cmd[i];
}
p[0] = ~checksum;
p++;
p[0] = PN532_POSTAMBLE;
p++;
#ifdef PN532DEBUG
Serial.print("Sending : ");
for (int i = 1; i < 8 + cmdlen; i++) {
Serial.print("0x");
Serial.print(packet[i], HEX);
Serial.print(", ");
}
Serial.println();
#endif
spi_dev->write(packet, 8 + cmdlen);
} else if (i2c_dev || ser_dev) {
// I2C or Serial command write.
uint8_t packet[8 + cmdlen];
uint8_t LEN = cmdlen + 1;
packet[0] = PN532_PREAMBLE;
packet[1] = PN532_STARTCODE1;
packet[2] = PN532_STARTCODE2;
packet[3] = LEN;
packet[4] = ~LEN + 1;
packet[5] = PN532_HOSTTOPN532;
uint8_t sum = 0;
for (uint8_t i = 0; i < cmdlen; i++) {
packet[6 + i] = cmd[i];
sum += cmd[i];
}
packet[6 + cmdlen] = ~(PN532_HOSTTOPN532 + sum) + 1;
packet[7 + cmdlen] = PN532_POSTAMBLE;
#ifdef PN532DEBUG
Serial.print("Sending : ");
for (int i = 1; i < 8 + cmdlen; i++) {
Serial.print("0x");
Serial.print(packet[i], HEX);
Serial.print(", ");
}
Serial.println();
#endif
if (i2c_dev) {
i2c_dev->write(packet, 8 + cmdlen);
} else {
ser_dev->write(packet, 8 + cmdlen);
}
}
}
我们只需要实现对图片数据的 64字节切片,构建这个数据帧最后再使用 sendCommandCheckAck 进行发送数据,就可以实现图片数据的传输。
3.3 网络通信部分实现
这里直接借用了微雪的部分源码,对其进行了删减以及修改:
const Route routes[] = {
{"/", "text/html", MAIN_page},
{"/scriptA.js", "application/javascript", scriptA},
{"/scriptB.js", "application/javascript", scriptB},
{"/scriptC.js", "application/javascript", scriptC},
{"/scriptD.js", "application/javascript", scriptD},
{"/styles.css", "text/css", styles}
};
通过浏览中的开发者工具直接复制 微雪 部分的html js, 省下了解决转义字符的时间,以及方便我临时调试js,调试好后再集成回mcu中:
#include <pgmspace.h>
const char MAIN_page[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>PriceTag</title>
<link rel='icon' href='data:;base64,='>
<link rel='stylesheet' href='styles.css'>
<script src='scriptA.js'></script>
'''
'''
)rawliteral";
关于web端部分我主要修改了上传图片的api接口,原始的微雪程序中,对图片数据进行了切片传输,并且js部分与 墨水屏型号 耦合度较高,所以这一部分全部删除了。因为我所需要传输的图像数据无关与墨水屏的型号(只有图像大小的区别),对于驱动墨水屏的逻辑是stm32中实现的(具体参考: 无源NFC墨水屏制作)。
js 接口:
function uploadImage() {
var c = getElm('canvas');
var w = dispW = c.width;
var h = dispH = c.height;
var p = c.getContext('2d').getImageData(0, 0, w, h);
var a = new Array(w * h);
var i = 0;
for (var y = 0; y < h; y++)
for (var x = 0; x < w; x++,
i++) {
a[i] = getVal(p, i << 2);
}
xhReq = new XMLHttpRequest();
rqPrf = 'http://' + getElm('ip_addr').value + '/upload';
xhReq.open('POST', rqPrf, true);
const uint8Data = new Uint8Array(packBitsToBytes(a));
xhReq.send(uint8Data);
}
这里直接使用post请求对整张图片数据进行传输,因为一张2.13寸墨水屏图像数据:104*212/8=2756 字节,这个大小对于esp8266是足够的, 内存是足够的。
esp8266 接收端:
server.on("/upload", HTTP_POST, []() {
digitalWrite(LED, LOW);
String imgData = server.arg("plain");
if(imgData.length() == 0) {
server.send(400, "text/plain", "No data received");
return;
}
Serial.println(imgData.length());
// for (size_t i = 0; i < imgData.length(); i++)
// {
// Serial.print(imgData[i]);
// }
const uint8_t* imgRaw = (const uint8_t*)imgData.c_str();
server.send(200, "text/plain", "ok");
displayImg(imgRaw, imgData.length());
digitalWrite(LED, HIGH);
});
另外新增了一个对图像缩放的功能(原始的微雪程序只能对图片进行裁剪):
3.4 HSU 与 SPI模式 切换问题
这个底板设备,当初设计时我就想能用到其HSU的功能,可以对 IC卡进行解密,但是我在切换到HSU模式时发现,无论如何上位机软件也无法检查到我的pn532 设备。怀疑是 esp8266 对 spi引脚的OUTPUT模式导致pn532不能进入 HSU模式。所以增加了模式切换部分代码:
void setup() {
Serial.begin(115200);
nfc.begin();
uint32_t versiondata = nfc.getFirmwareVersion();
if (! versiondata) {
Serial.print("Didn't find PN53x board");
useFlag = false;
}
pinMode(LED, OUTPUT);
digitalWrite(LED, LOW);
if (useFlag){
Serial.print("Found chip PN5"); Serial.println((versiondata>>24) & 0xFF, HEX);
Serial.print("Firmware ver. "); Serial.print((versiondata>>16) & 0xFF, DEC);
Serial.print('.'); Serial.println((versiondata>>8) & 0xFF, DEC);
Serial.println("Waiting for an ISO14443A Card ...");
setupWifi();
}else{
pinMode(PN532_SCK, INPUT);
pinMode(PN532_MOSI, INPUT);
pinMode(PN532_SS, INPUT);
pinMode(PN532_MISO, INPUT);
}
}
原理很简单,如果spi接口检测不到 pn532小红板,就将spi的几个引脚置为 INPUT模式,果然问题就解决了。
注意:每次切换模式是,在拨动pn532上面的拨码开关后,以及我设计的底板上的双刀双置开关后需要对设备重新 上下电才可以完成模式的切换,这个应该是pn532固件的问题,其不支持在上电过程中完成模式的切换,所以这里我的esp8266程序也没有实现上电切换模式部分。
3.5 指示灯状态
在硬件设计中我放置了LED用于标示板子的状态:
- 上电默认常亮状态 。
- 如果检查到pn532 小红板,指示灯熄灭。
- 检测不到 pn532小红板,指示灯规律闪烁 (用于切换HSU模式)。
- 接收到图片数据,指示灯常亮。
- 传输图片数据过程中,指示灯闪烁。
- 图片传输完成,指示灯熄灭。
4. 开源地址占位
https://oshwhub.com/wshuo426/pn532-di-ban-_-wu-pu-tong
更多推荐



所有评论(0)