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语言的嵌入式工程,如何设计我们的代码文件,每个文件干一些什么事,放哪些逻辑,写代码的缩进方式是什么样的,变量名称如何确定,函数名称如何确定,哪些东西放在全局变量中,我想这些才是最宝贵的一些经验之谈吧。

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