STM32与PN532构建NFC近场通信指南(五):一种适用于STM32的通用串口通信架构及与PN532的通信实践

目录

  1. STM32与PN532构建NFC近场通信指南(一):NFC及相关知识准备
  2. STM32与PN532构建NFC近场通信指南(二):PN532及恩智浦PN系列产品简介
  3. STM32与PN532构建NFC近场通信指南(三):PN532通信协议分析
  4. STM32与PN532构建NFC近场通信指南(四):PN532握手数据流详解
  5. STM32与PN532构建NFC近场通信指南(五):一种适用于STM32的通用串口通信架构及与PN532的通信实践

STM32与PN532构建NFC近场通信指南(五):一种适用于STM32的通用串口通信架构及与PN532的通信实践

这是这篇系列教程的最后一章节,有了之前的基础,我们应该对PN532的通信流程,以及收发包的细节有了一定的认知,下面就是用STM32与PN532建立连接了。本章节的最终目标,是希望构建一套针对任意外设收发包的通用框架,而不仅限于PN532和串口通信。

STM32,HAL库,和RT-Thread

我个人对基于HAL库和RT-Thread的STM32开发情有独钟,因此项目使用STM32CubeMX生成了HAL库的KEIL工程,并在KEIL中引入了RT-Thread nano。通信框架的设计时我尽量避免了RT-Thread的信号量和事件的传递,而是采用最基础的方式,如果您能熟练使用RT-Thread及其各类线程同步技巧,那么构建通信框架是一件非常舒服的事,可以完全做成事件驱动的方式。

首先,STM32中串口的配置,此处不表,我因为PCB设计的关系,这里用的是UART5。对于串口尚不知如何初始化的朋友,可以阅读之前的几篇关于STM32的教程。

这里值得注意的是,我个人偏向于使用DMA来处理外设通信,使用STM32CubeMX能够非常容易地配置出UART和DMA关联,同时启动UART的IDLE空闲中断,这种机制配合RT-Thread信号量等能力,可以非常舒服地处理不定长数据包。这里我因为想要更好地满足通用性,并没有使用DMA和IDLE空闲中断,而是采用的UART_IRQHandlerHAL_UART_RxCpltCallback标准接收中断来处理接收到的数据包。以后有机会,我会在新的项目教程里,用RT-Thread配合DMA和空闲中断来巧妙地处理不定长数据包。

对于数据的发送,我们直接用HAL库的HAL_UART_Transmit发送函数即可;对于数据的接收,我们希望开一个足够大的BUFFER数组,然后设立两个哨兵,一个叫前哨,一个叫后哨,前哨只管往BUFFER里追加数据,后哨用来处理数据,当后哨追赶上前哨时,两个哨兵都退回到BUFFER的开头,从而实现一种简易的接收缓冲队列。前哨的处理理所当然会在中断服务函数HAL_UART_RxCpltCallback中,后哨的处理会在应用逻辑中进处理。

模块化

无论是C++,JAVA,Go这类的高级软件开发,还是JavaScript,HTML5这类的前端设计,包括我们现在玩得比较多的纯C的嵌入式开发,我们总希望编写的代码尽量模块化。比如我们今天在STM32F4上使用了PN532做一个DEMO,我们下一次可能会在STM32F1/L0甚至STC51单片机上使用PN532(毕竟只要有UART/I2C/SPI接口,软协议都一样的),如果我们编写的关于PN532的代码集中在某一个或者某几个文件中,在其他项目中能够直接引入工程做到开箱即用,那将是一件很酷的事情。

根据模块化的思维,我们在工程中新建一个pn532.cpn532.h,以及一个pn532hw.c的文件,我们的想法是在pn532.c中尽量完成所有与PN532相关协议栈的处理,以后这个文件尽量不需要改动的;在pn532hw.c文件中完成与硬件相关的耦合,并利用__weak关键字来表明,这些代码是需要与实际情况耦合的,其他工程是要酌情调整的。我们在编程的过程中都要尽可能保持这样良好的模块化思维,让工程代码变得很有条理,方便日后的维护和移植。

结构体和指针

根据上面的思路,我们应该在pn532.h文件中编写所有协议栈需要的结构体。

关于结构体,指针,他们与通信协议的关系,C/C++语言的初学者可能并不能很好地驾驭,在这里推荐一本书《深入理解C指针》,看了这本书,我在日常编程中对指针的利用率有了一个很大的提高,在看一些大牛写的复杂的关于指针变化的表达式时也能做到游刃有余。在实际应用中,一种很棒的技巧就是是设计合理的结构体,包括结构体的大小,内部变量的顺序,变量的长度,这些都需要仔细计算和设计,大部分时候还需要使用位字段、1字节对齐等技巧。在数据传输或者解析时,尽量让结构体整个变成一个buffer用来传输,不要进行多余的内存拷贝操作。对于定长通信协议,这么做可以设计出一套很棒的结构体出来,后面只需要对结构体赋完值,就能直接发送出去了。

对于不定长的通信数据包,我们可以在结构体中抽象出一些特征点,然后在结构体其内部开辟一段buffer,设计一套code和decode函数,在收发前后免不了进行一些内存拷贝。下面我们来看一段关于PN532通信协议设计出来的结构体:

typedef struct t_PN532_FRAME_INFO{
    u8  Type;        /* 00:ack, ff:nack, d4:mcu->pn532(d4), d5:mcu<-pn532(d5) */        
    u8  DataSize;    /* DATA's length */    
    u8  Data[PN532_MAX_BUFFERSIZE];  /* DATA */
    u8  Cmd;
}PN532FrameInfo;

typedef struct t_PN532{
    vu32 StatusFlag;    
    vu8  RxBufferSize;
    vu8  TxBufferSize;
    vu8  RxCurrentPos;
    PN532FrameInfo RxFrameInfo;
    PN532FrameInfo TxFrameInfo;

    u8  RxBuffer[PN532_MAX_BUFFERSIZE];
    u8  TxBuffer[PN532_MAX_BUFFERSIZE];
}PN532;

上述代码第一个结构体描述了一个PN532数据帧,第二个结构体描述了对于整个工程来说的一个总体PN532结构体对象,以后只要有了这个对象,关于整个协议栈的所有情况就一目了然了,包括各个BUFFER缓冲区,以及前哨兵和后哨兵的位置。同时因为C语言没有namespace之类的命名空间,因此用一个大型结构体封装模块所需的所有零散对象,不仅能做到统一管理,又能做到不污染其他外部全局变量。

将C设计得跟JAVA一样清晰

纯C的能力通常来说被大多数人大大地低估了,人们总是自然而然地觉得C++/JAVA要比纯C更加强大,其实无论出于性能还是编程舒适性来说,纯C的能力都不逊于由此派生出去的那些高级语言。

C的结构体+函数指针,这两个元素结合起来就能实现大部分C++/JAVA中的功能。如果人为地再注意一些编程规范,比如驼峰式命名,大写函数首字母作为public函数,小写函数首字母作为private函数等等,就能让C变得像高级语言一样逻辑清晰。

数据发送

下面来看一段发送数据的代码:

Ret PN532SendBuffer(void){
    pn532.RxBufferSize = 0; pn532.RxCurrentPos = 0;
    Ret ret = Pn532hwSendBuffer(pn532.TxBuffer, pn532.TxBufferSize);
    pn532.TxBufferSize = 0;
    return ret;
}

这段代码没有直接调用HAL_UART_Transmit函数,而是调用了pn532hw.c中的Pn532hwSendBuffer,就是表明数据的发送并不跟具体的硬件外设强耦合,如果这里写死了,就无法保证pn532.c在今后协议栈移植时的不变性。再看pn532hw.cPn532hwSendBuffer的定义:

/* ========Must Implement Function======== */
__weak Ret Pn532hwSendBuffer(u8* buffer, u8 bufferSize){
    // use hardware send the buffer, len: bufferSize
    return RET_OK;
}

__weak void Pn532hwWait(u8 ms){
    HAL_Delay(ms);    //rt_thread_delay()
}

可以看出,这里依然没有真正用HAL_UART_Transmit实现发包,而是用一个__weak弱定义关键字描述了一个空函数,并且在注释中提醒开发者必须要实现这一函数。于是上层应用,可以在工程中任意位置重新强定义这个函数,真正地实现数据包的发送。另外注意观察到上面的Pn532hwWait函数,因为在编写代码时,不可避免地会遇到一些地方需要延时等待,在使用RTOS和不使用RTOS的情况下,可能延时的处理不尽一致,所以这里的延时函数也使用__weak定义了,只不过这里没有用空函数定义,而是默认使用标准的HAL延时来处理。

数据接收

这里数据的接收底层用中断实现的,那么模块是怎么跟底层外设建立连接的呢,我们从高层往底层依次看:

Ret PN532ReceiveBuffer(PN532FrameInfo* recvFrameInfo, u8 timeoutMS){
    Pn532hwWait(2);
    Ret ret = waitFrame(timeoutMS);
    if(ret != RET_OK)
        return ret;

    /* now, bufferSize > currentPos, process the buffer */
    return popFrame(recvFrameInfo);
}

Ret waitFrame(u16 ms){
    if(pn532.RxCurrentPos > pn532.RxBufferSize){
        pn532.RxCurrentPos = 0;
        pn532.RxBufferSize = 0;
        return RET_ERROR_LO;
    }

    waitCycle = 0;
    while(pn532.RxCurrentPos == pn532.RxBufferSize){
        Pn532hwWait(1);
        waitCycle++;
        if(waitCycle > ms)
            return RET_ERROR_TO;
    }
    return RET_OK;
}

可以看到,模块层通过两个哨兵RxCurrentPosRxBufferSize,判断系统里现在有没有尚未处理掉的收取数据,其实模块层并没有具体地关心底层数据的接收,而是靠哨兵的站位来判断有没有数据收取,那么逻辑就清晰了,硬件层只需要追加数据进入buffer并移动前哨站位,就可以实现数据的收取和处理了。看pn532hw.c中的下面函数:

/* must called by upon Application */
Ret Pn532hwRecvBuffer(u8* buffer, u8 bufferSize){
    /* protect the buffer not overflow */
    if(pn532.RxBufferSize + bufferSize > PN532_MAX_BUFFERSIZE)
        return RET_ERROR_ME;    

    /* wait for lock */
    while(pn532.StatusFlag & PN532_STATUS_RXBUSY);
    /* lock */
    pn532.StatusFlag |= PN532_STATUS_RXBUSY;
    memcpy((u8*)(&(pn532.RxBuffer[pn532.RxBufferSize])), buffer, bufferSize);
    pn532.RxBufferSize += bufferSize;
    /* unlock */
    pn532.StatusFlag &= ~PN532_STATUS_RXBUSY;

    return RET_OK;
}

这个函数没有进行弱定义,是一个不让覆盖的函数,同时注释中还写了这个函数必须被上层应用所调用,模块才能正常接收数据。这个函数中最重要的就两行代码:

memcpy((u8*)(&(pn532.RxBuffer[pn532.RxBufferSize])), buffer, bufferSize);
pn532.RxBufferSize += bufferSize;

其他都是一些保护性的代码,防止资源竞争引发的数据混乱。
我们在更高层的用户代码中编写串口中断服务函数,在其中调用这个函数,如下:

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){
    if(huart->Instance == UART5){        
        Pn532hwRecvBuffer(Uart5RecvBuffer, 1);                    // callback the PN532 1 byte received.        
        HAL_UART_Receive_IT(&huart5, Uart5RecvBuffer, 1);        // re enable UART5 RX
    }
}

OK,现在数据的接收和发送都架构好了,我们可以正常收发了。看看我们的pn532.h中都描述了怎样的函数定义:

/* ========Public Function======== */
Ret PN532Wakeup(void);
Ret PN532SearchCard(u32* uuid);
Ret PN532Authen(u32 uuid, u8 section, u8* pwd);
Ret PN532Read16Byte(u8 section, u8* data);
Ret PN532Write16Byte(u8 section, u8* data);

Ret  PN532Transmit(u8 cmdCode, u8* sendData, u8 sendDataSize);
Ret  PN532TransmitReceive(u8 cmdCode, u8* sendData, u8 sendDataSize, u8* recvData, u8* recvDataSize, u8 waitMS);
Ret  PN532SendBuffer(void);
Ret  PN532ReceiveBuffer(PN532FrameInfo* recvFrameInfo, u8 timeoutMS);

/* ========Private Function======== */
u8   getDCS(u8 preSum, u8* dataBuffer, u8 dataBufferSize);
u8     getLCS(u8 len);
void codeTxBuffer(void);
Ret  checkRxBufferHeader(void);
Ret  waitFrame(u16 ms);
Ret  popFrame(PN532FrameInfo* frame);

这其中一些具体的实现方式不太重要,大家可以根据自己的应用场景和逻辑去构造,重要的编程思想和模块化的解耦处理,以及代码书写的习惯。这里就抛转引玉了,相信一定还有更多更优秀的架构方式。

小结

这一章我们其实没有太多的关心PN532的协议,也没有带着大家在STM32的基础上一步一步实现PN532的协议,甚至没有过多的代码细节。但这一集所讲的内容恰恰是我最希望阐述的编程思想,面对一个纯C语言的嵌入式工程,如何设计我们的代码文件,每个文件干一些什么事,放哪些逻辑,写代码的缩进方式是什么样的,变量名称如何确定,函数名称如何确定,哪些东西放在全局变量中,我想这些才是最宝贵的一些经验之谈吧。

那么本系列教程就结束了,以后有机会还会继续写一些面向项目的,面向某一具体模块的编程教程,希望大家喜欢和支持。

STM32与PN532构建NFC近场通信指南(四):PN532握手数据流详解

目录

  1. STM32与PN532构建NFC近场通信指南(一):NFC及相关知识准备
  2. STM32与PN532构建NFC近场通信指南(二):PN532及恩智浦PN系列产品简介
  3. STM32与PN532构建NFC近场通信指南(三):PN532通信协议分析
  4. STM32与PN532构建NFC近场通信指南(四):PN532握手数据流详解
  5. STM32与PN532构建NFC近场通信指南(五):一种适用于STM32的通用串口通信架构及与PN532的通信实践

STM32与PN532构建NFC近场通信指南(四):PN532握手数据流详解

本章为系列指南第四章,主要介绍上位机与PN532进行Mifare S50卡片读写全流程的通信数据流。重点需要阅读PN532 User Manual.pdf的第7章,目录如下:

芯片指令集

上一章中,我们了解了PN532的通信协议,其数据帧格式中有一段数据负载区域DATA,从PD0到PDn。在PN532UM 第7章中有如下描述:

如上图,对于0xD4发送包,将DATA区域细化成了Command Code+Optional Input Data,同时在列表中列举了所有的Command Code。对于0xD5接收包,则是Command Code+1 + Optional Output Data

请求版本号测试

我们可以通过以下方法构造一个简单的数据包,来请求PN532的内部固件版本号:

  • TFI + DATA区域:0xD4 0x02
  • LEN: 0x02
  • LCS: 0xFE
  • DCS: 0x00 - checksum = 0x00 - (D4+02) = 0x00 - D6 = 0x2A

包结构构造为:00 00 FF 02 FE D4 02 2A 00

将PN532模块的4根引脚与PC-USB-CH340模块(或者其他USB-TTL串口设备)相连,使用串口调试助手等软件进行上位机模拟:

对照通信协议,我们可以很容易理解:发包后成功收到了一个6字节ACK包,随后收到了一个Reponse数据,解析内部DATA为:03 32 01 06 07,其中03是发送指令02的+1响应,后面32 01 06 07为返回的固件版本号。

继续根据协议解析为:

  • 32:IC,跟文档描述一致,为0x32
  • 01:Ver, 大版本号
  • 06:Rev, 小版本号
  • 07:Support,二进制是0000 0111,表示支持ISO18092,ISO14443B,ISO14443A

OK,至此我们已经成功的理解了所有通信时所涉及的知识点,并且打通了第一次与PN532的通信。下面就是根据实际业务需求,进行数据包的请求和接受了。

Mifare S50卡的读写流程

在本系列教程的第二讲STM32与PN532构建NFC近场通信指南(二):PN532及恩智浦PN系列产品简介中,我们提到,对于Mifare S50卡的读写流程,大致上分为以下步骤:

  • 寻卡
  • 认证
  • 读写卡
  • 释放(不重要)

那么就按照这个步骤来看看上位机应该如何与PN532进行通信:

寻卡


打开手册到7.3.5章节,这一章的InListPassiveTarget指令就是寻卡的意思,可以看到它的Command Code是0x4A,后面还有MaxTgBrTy两个参数,根据它的描述即可构建以下包:
00 00 FF 04 FC D4 4A 01 00 E1 00
如果此时有一张Mifare S50卡在附近,我们将得到以下响应:
ACK:00 00 FF 00 FF 00
Response:

header   len lcs tfi cmd  NbTg tg sens_res sel_res  nfcIDLength NFCID       dcs tail
00 00 FF 0C  F4  D5  4B   01   01 00 04    08       04          39 01 0B B1 D8  00

其中39 01 0B B1是这张卡片的UUID,这个我们需要寄到本本上,后面还需要使用。如果在MCU中,那就设置一个32bit度的变量,将其缓存起来。

认证

搜到卡片后,下面一步就是用密码来进行登录认证。Mifare S50出厂密码一般是FF FF FF FF FF FF,个别情况下,还有A0 A1 A2 A3 A4 A512 34 56 12 34 56这几种情况。我们假定现在的密码是FF FF FF FF FF FF

打开文档7.3.8章,这一章讲InDataExchange指令。这个指令可以与NFC对象进行数据交互,我们需要实现的认证操作,作为这个指令的子指令集包含在其中,后面的数据读写指令也包含在这个指令集中。

仔细阅读手册的Page130页,这里罗列了我们需要的几个子指令集,包括认证和16字节的读写。并且还非常人性化地提供了几个example,我们仿照样例,构建一个对于2号数据块的认证包:

header    len  lcs  tfi  cmd  tg  mifareAuth3 sectionNum Pwd               uid         dcs tail
00 00 FF  0F   F1   D4   40   01  60          02         FF FF FF FF FF FF 39 01 0B B1 99  00

如果一切正常,密码认证通过后,我们将得到以下响应:
ACK:00 00 FF 00 FF 00
Response:

header   len  lcs  tfi  cmdResp data dcs tail
00 00 FF 03   FD   D5   41      00   EA  00

读写

根据文档,我们将很容易推断出16Byte的读写指令数据包,如下:
Request:

header   len lcs tfi cmd tg mifareRead sectionNum dcs tail
00 00 FF 05  FB  D4  40  01 30         02         B9  00

ACK:00 00 FF 00 FF 00
Response:

header   len  lcs  tfi  cmdResp errCode data                                            dcs tail
00 00 FF 13   ED   D5   41      00      00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 EA  00

Request:

header   len lcs tfi cmd tg mifareWrite sectionNum data                                            dcs tail
00 00 FF 15  EB  D4  40  01 A0          02         01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 C1  00

ACK:00 00 FF 00 FF 00
Response:00 00 FF 03 FD D5 41 00 EA 00

小结

这一章我们对PN532的通信流程和相关指令集有了一次详细的梳理,根据我们的应用场景,设计出一套读写Mifare S50的最简数据流。下一章我们将用STM32作为上位机,使用串口作为通信介接口,与PN532进行通信,值得注意的是,下一章我们将探讨设计出一套通用的数据流处理框架,无论是使用UART, I2C, SPI都能平滑使用的读写数据流的架构,并不仅限于与PN532通信。

STM32与PN532构建NFC近场通信指南(三):PN532通信协议分析

目录

  1. STM32与PN532构建NFC近场通信指南(一):NFC及相关知识准备
  2. STM32与PN532构建NFC近场通信指南(二):PN532及恩智浦PN系列产品简介
  3. STM32与PN532构建NFC近场通信指南(三):PN532通信协议分析
  4. STM32与PN532构建NFC近场通信指南(四):PN532握手数据流详解
  5. STM32与PN532构建NFC近场通信指南(五):一种适用于STM32的通用串口通信架构及与PN532的通信实践

STM32与PN532构建NFC近场通信指南(二):PN532串口指令通信流程

本章为系列指南第三章,主要介绍上位机与PN532的通信协议,本章我们需要重点阅读PN532 User Manual.pdf手册的第6章,该章节目录如下:

数据帧格式

任何成体系的通信协议都是由数据帧构成的,数据帧结构会在整个通信协议大部分环节中保持一致,个别协议会偶尔出现几个特殊帧指令用来实现特殊功能。试想这次由我们自己创作构建一套通信协议,无非包含这几个方面:包头,长度,负载,校验,包尾;如果是针对读写寄存器的帧结构,那么还包括:指令(读/写),寄存器地址,值(返回/写入)等;如果有1:N甚至N:M的总线架构,那么还要包括目标地址Target和来源地址Source(类似以太网帧)。

类似的,PN532与上位机之间通信协议也会有上面那些元素,让我们打开文档看一看,在PN532UM.pdf 6.2.1 Frames structure章节就是描述的数据帧格式,如下:

上图可以很容易看出:

  • 包头为00 00 FF(Preamble前缀 + Start of Packet Code起始代码),包尾为00(Postamble后缀)
  • 不定长数据帧,len为一个字节,len的计算从TFI(指令代码)到PDn(数据串最后一个byte)
  • LCS,用来提高LEN传输的准确性,计算公式为:LCS + LEN = 0x00
  • TFI,指令代码,只有两个指令,0xD4代表MCU->PN532; 0xD5代表PN532->MCU
  • DATA,数据负载
  • DCS,1字节校验,计算公式为:[TFI + PD0 + PD1 + … + PDn + DCS] = 0x00

OK,PN532的数据帧格式相对比较简单,相比我们上面自己设想的通信协议只是多了一个LCS的概念,从计算公式上来看,这是LEN的相反数。另外,数据负载段的校验数DCS,也是使用和校验的相反数来表示。

在PN532UM 的 6.2.1.2章节,介绍了一种长数据的扩展数据帧格式,我们基本上用不着,忽略。
在PN532UM 的 6.2.1.3- 6.2.1.5章节,介绍了ACK响应包的结构,NACK异常响应包的结构,以及出错包的结构,这里就不赘述了,毕竟我们总是期望能得到正确的响应。ACK包结构如下,非常简单:

在PN532UM 的 6.2.1.6章节,介绍了Preamble前导码和Postamble后导码的概念,这两个均是由不确定的字节数构成,PN532总是从Start Code00 FF来识别一个数据包的开头。

至此,我们对PN532的数据帧结构有了一个了解。

会话结构

PN532UM 6.2.2介绍了上位机与PN532之间的会话流程,如下:

如图,这是一次标准的数据帧收发流程,首先由主控MCU向PN532发送Command Packet,PN532接受到以后会立刻发送一个ACK给主控,然后经过一段时间处理,PN532会再发送一次Response Packet给主控,至于主控想不想再发一个ACK给PN532,这个PN532不强求。这是一种类似TCP连接的过程。图中右边的P70_IRQ是一根中断信号线的电平变化过程,如果使用UART来收发数据帧的话,这个信号可以忽略,使用串口RX中断就足够了,如果使用485芯片来处理,这根线可以来做485的使能开关。

PN532UM 6.2.2章节的后续内容还描述了发生错误时的会话流程,这里不表了,不重要。

接口差异

虽然在软件协议上面,数据帧格式都是一样的,但PN532提供的三种接口方式,UART,SPI,I2C在使用过程中还是有一些差异的。具体的接口使用的通信时序,在6.2.3,6.2.4,6.2.5以及6.3章节有详细的介绍,这里我们使用UART在115200波特率与PN532进行通信,没有特别的注意事项,此处不表了。

小结

这一章我们对PN532的通信协议有了一个总体的认知,详细了解了它的数据帧格式。下一章我们将对具体的指令集进行分析,并构建一整套的握手通信流程。

丁丁生于 1987.07.01 ,30岁,英文ID:newflydd
  • 现居住地 江苏 ● 泰州 ● 姜堰
  • 创建了 Jblog 开源博客系统
  • 坚持十余年的 独立博客 作者
  • 大学本科毕业后就职于 中国电信江苏泰州分公司,前两年从事Oracle数据库DBA工作,两年后公司精简技术人员,被安排到农村担任支局长(其本质是搞销售),于2016年因志向不合从国企辞职,在小城镇找了一份程序员的工作。
  • 在 Git OSChina 上积极参与开源社区
  •