本帖最后由 suyong_yq 于 2016-2-1 01:48 编辑
一、方案名称:
基于GD32的无线透传模块的设计与实现
二、方案介绍:
通过使用2.4G射频通信模块nRF24L01进行无线收发,在主控单片机(GD32)上进行协议解析,转换成UART串口帧,再通过串口输出。实际上,GD32 Colibri-F207ZE开发板的UART又通过专门的芯片转换成了USB,最终只要通过USB连接到电脑上,运行电脑上的超级终端,就可以实现无线通信了。当然,核心的功能还是射频和串口之间的透传。
无线透传模块的设计可以用来解决已有应用系统的有线通信线路向无线通信线路改造的问题,即“剪短串口线”。当使用本无线透传模块建立无线连接时,完全不需要更改任何原有系统的程序,只要直接将模块对接到原有用于通信的串口接口即可。
三、方案结构框图:
见后正文
四、设计应用描述及心得总结:
见后正文
正文
0 概述
1 应用需求
2 设计分析与实现
2.1 多线程实现并行工作两个数据流
2.2 串行数据流同定长数据包之间转换实现透传
3 程序开发
3.1 两个线程的实现
3.2 “封包”及“拆包”过程的实现
3.3 串口中断服务中发送过程和接收过程的实现
4 演示效果
4.1 接线
4.2 运行程序
5 获取项目代码
《基于GD32的无线透传模块的设计与实现》
0 概述
主控单片机GD32通过SPI通信引擎驱动2.4G无线通信模块nRF24L01进行无线收发,在主控单片机内部进行协议解析,将定长32字节的无线数据包与串行的UART串口字节流之间相互转换,实现了并行工作的无线接收转串口发送、串口接收转无线发送的两个数据流。
实际上,GD32 Colibri-F207ZE开发板的UART通过专门的芯片(CH340G)转换成了USB通信,最终只要通过USB接口将GD32开发板连接到电脑上,运行电脑上的超级终端,就可以演示无线通信的功能了。
1 应用需求
无线透传模块的设计可以用来解决已有应用系统中有线通信线路向无线通信线路改造的问题,即“剪短串口线”。当使用本无线透传模块建立无线连接时,完全不需要更改任何原有系统的程序,只要直接将模块对接到原有用于通信的UART串口接口即可,如图1所示。
图1(a) 有线通信线路
图1(b) 无线通信
“透传”的概念在于,功能模块通过两个透传模块进行无线通信时,在应用程序中同直接对接串行接口没有任何区别,不需要考虑无线通信过程中对定长数据包的“封包”和“拆包”,可以实时地发送单个字符,也可以连续发送字符串。当然,这个过程中的“封包”和“拆包”的处理就得由无线透传模块本身解决了。
2 设计分析与实现
具体分析无线透传模块的设计,主要是以无线模块与串行接口引擎之间的数据流为操作对象,那么就必须考虑解决好两个问题的具体设计:
- 如何安排应用中的数据流?
- 如何在程序中组织数据结构?
解决这两个问题的过程,也就是设计完成无线透传应用的过程。
2.1 多线程实现并行工作两个数据流
在无线透传模块的应用中,有两个具体的数据流:
- 无线接收->串口发送
- 串口接收-> 无线发送
在NRF24L01P无线通信模块的单元测试程序(调试系统初期,验证无线模块是否能够实现无线数据包的收发)中,可以很容易地在单线程main函数的实现中完成单独的通信数据流的功能,具体地,就是在循环程序中不断等待接收来自无线或是串口引擎的数据,然后从另一个通信接口发送出去。但是在无线透模块中,要让两个数据流相对独立地工作,并行地传递数据,就必须要考虑在应用程序中实现“多线程”的解决方案了。单片机实现“多线程”一般考虑用RTOS(实时操作系统),从软件上时分复用CPU模拟出多个线程完成各自不同的任务,但是对于比较简单的应用,使用RTOS未免“大动干戈”了,况且RTOS本身对系统的硬件资源也有相当的要求。
考虑到本设计的功能相对简单(但是非常实用),本设计打算利用硬件的“中断”机制来实现多线程。事实上,此处使用中断也是必然的,即使在使用RTOS的前提下,要异步地接收获取外部数据也必须使用中断触发捕获数据的事件,索性直接在裸板程序中使用中断实现数据捕获和数据处理,简单、直接、高效。
在设计程序时,主循环本身可以作为一个在后台工作的线程,利用“中断”抢占主循环的特性开辟出另一个线程。考虑到NRF24L01P无线模块的控制需要GPIO和SPI的混合使用,而UART引擎的工作内容相对单一,便于由硬件实现,本设计选择将UART通信部分的功能交由中断线程实现,而由CPU主管的主循环负责无线模块的通信。在具体开发中,对无线模块的使用还涉及到对数据包进行“封包”和“拆包”的过程,这些操作都需要比较灵活的控制,同时交给主循环进程完成也能分担硬件中断处理程序的负载,减少硬件实时性的损失。
由于在主循环中有比较多的处理内容,两个数据流都会产生数据积压,具体地:在无线接收->串口发送数据流中,无线接收端每次接收到的是一个32字节的数据包,这些数据被逐个发送到UART串行通信总线上需要一定的时间,在此期间,未发送的数据必须被保存在单片机的内存中;在串口接收->无线发送的数据流中,由于无线模块的操作周期相对较长,当无线模块正在接收、发送当前数据包及处理数据其它事务时,此阶段单片机从UART串行总线上捕获到大量的数据,在下一次发送过程启动前也得缓存在单片机的内存中。
综合考虑对数据的流处理特性,本设计考虑使用FIFO缓冲队列分别缓存保存在单片机内部的两个串行数据流。但是,考虑到无线模块的数据包是“块状”的,仍为无线模块开辟了整块的缓冲区。实际上,无线模块的发送和接收过程不像UART一样是并发的,在任何时刻,无线模块只能发送或者接收,因此只有需要发送或者接收时才需要用整块内存缓存数据包,而未操作的数据仍暂存到串行的FIFO中,因此只要为无线模块安排一个块缓冲区即可。
综上所述,基于中断实现多线程的两个并行工作的数据流如图2所示。
图2 无线透传应用中的两个数据流
从图2中可以看出,“无线接收->串口发送”数据流(图中的下半部分)首先从无线模块捕获到空间电磁波中的数据开始,无线模块在监测到无线通信捕获事件后,由应用程序调用无线模块的驱动程序从无线模块中读出整个数据包并存入无线通信数据包缓存区中,然后调用拆包处理将无线接收数据包中的有效数据解析出来,再向UART串口发送缓存FIFO中填充有效数据,最终通过串口发送处理过程中将FIFO中的数据发送到UART串行总线上;“串口接收-> 无线发送”数据流(图中的上半部分)首先由串口接收处理过程捕获到串行总线上的数据,然后将它们填充到UART串口接收缓存FIFO中,当封包过程监测到串口接收FIFO中有数待发时,随即从其中取数并封包,之后调用无线模块驱动将整包的数据通过无线模块发送到空间中。
2.2 串行数据流同定长数据包之间转换实现透传
在实现透传功能的时候必然面临一个问题,什么时候“封包”发送射频数据?操作无线模块是一个比较费时间的过程,主控单片机同无线模块通信发送一包数据的时间肯定要比UART串口接收一个字节数据的时间要长很多,因此以常规的数据中继“收一个数发一个数”的的设计思路显然不能再此处应用(实际上,在透传类应用中,只要转换的两个总线通信速率有较大的时间差,并且至少一方的数据通信过程需要组成特定的帧格式通信,例如I2C等,简单数据中继的设计思路都不能满足需求)。那么就必须使用“公交车”模型处理数据的搬运。
“公交车”模型是专门为处理数据帧格式转换实现数据透传的一个算法模型,使用乘客在公交车站等待特定一路的公交车周期断续抵达载客的场景描述地数据进行组帧并处理(发送)的过程。
【乘客&公交车站】
乘客在不确定的时间点抵达公交车站等车,若是将每位乘客看做是从串行总线上接收到的单个数据,那么公交车站就是暂存数据的串行缓冲区,缓存即将通过公交车发送的乘客。如果在资源紧张的情况下,甚至可以为乘客以抵达车站的时间排序,当公交车到来之时,最早抵达车站的乘客优先乘车,这样公交车站就进一步明确使用了FIFO数据结构。实际上,在实现算法的时候总是希望使用FIFO的,因为它描述了一种实现缓冲区的确切手段,并且易于操作。
【公交车】
公交车以一个不稳定的周期抵达公交车站载客。注意,此处假设的“不稳定的周期”是有意义的,公交车大抵会按照比较稳定的周期从始发站发车,但行程中的很多不确定因素会影响到抵达某个公交车站的时间点,这些不确定因素可能包括道路的拥挤情况、红绿灯、之前车站乘客登车的延误,公交车司机自身情绪变化对驾驶过程的影响等等。
公交车本身的最大载客量是固定的(受限于座位的数量,并且不允许超载),并且在公交车站停靠的时间有限,必须尽快载客离开,一方面要减少对后续行程的延误,另一方面为下一班车腾出停车位。若等待乘车的乘客数量刚好等于最大载客量,自然是让全部乘客上车然后马上开车出发;若等待乘车的乘客数量大于最大载客量,则满载乘客乘客后开车出发,没有上车的乘客继续等待下一班公交车;若等待乘车的乘客数量小于最大载客量,则让全部乘客上车后,再带着部分空座位驶离车站出发。
同时,公交车需要确切地直到车上乘客的数量以便于向后续的处理过程交接(比如这些乘客需要转车,公交车A需要告诉公交车B自己车上有多少乘客,以预留足够的空位接收),当公交车满乘的时候,搭载乘客的数量都是固定,及公交车本身的最大载客量。但是公交车中未满乘时,就需要特别对有效的载客量进行专门的计数。实际上满乘的情况同未满乘的情况是一致的,只是用满乘这个条件确定了有效载客量刚好等于最大载客量。
在透传应用中,无线模块通信的固定长度的数据帧就是公交车,固定长度的数据帧通过无线模块发出后将被另一个无线模块捕获到,下载数据包并解析其中的有效数据,从而还原回串行序列通过串行通信总线发出。对应于将串行缓存中的数据“封包”的“上车过程”,接收固定长度的数据包并还原序列化的“拆包”就是是“下车过程”。发送端同接收端唯一的交互内容就是一个固定长度的数据包,为了能够让接收端在“拆包”过程中准确地还原出发送端“封包”时的数据,就必须在“封包”过程中加载足够的信息。对于本设计的传输内容——一段串行数据序列,完整描述其信息的要素有三个:
- 数据内容
- 排列顺序
- 序列长度
具体地,就是要设计数据帧的封装格式以描述上述三个信息要素。在本设计中设计无线通信数据帧格式如表1所示,其中N=31,为无线模块定长数据包限定的固定长度(从0开始计数到31,总长32)。
表1 无线定长通信帧格式
对应于传输数据序列的三个信息要素,数据内容被保存在“有效载荷内容字段”,其内容的排列顺序按照串行数据进入串行缓冲区的时间先后与字节编号从小到大排列一致,串行数据序列的序列长度被保存在“有效载荷长度”,即定长帧的首个字节。
发送端从串行队列中取得串行数据队列时,从串行的FIFO缓冲区中逐个取出数据依次向定长帧的“有效载荷内容”字段中填充,并对填充数据进行计数。当串行FIFO缓冲区的数据被暂时取空,或是达到了有效载荷长度的上限,则停止填充,然后将对“有效载荷内容”中填充数据的计数值写入到“有效载荷长度”字段中。填充域中保留的内容是无效载荷,接收方不会解析,发送方就不用管它。
接收端收到数据进行解析,首先读取确定的第一个字节确定有效载荷,然后依次读取有效载荷长度指定的若干个数据字节,对于填充域中的内容直接舍弃。
3 程序开发
3.1 两个线程的实现
在程序的具体实现时,设计了两个执行主线:
- 在main函数的超循环中,完成无线模块的收发及定长数据包的“封包”与“拆包”工作。
- 在UART串口中断服务程序中,处理UART串行总线上数据的发送和捕获工作;
无线透传应用的流程图如图3所示。
图 3(a) main函数的执行流程
图 3(b) 串口中断服务的执行流程
3.2 “封包”及“拆包”过程的实现
(1)“拆包”过程
当在main函数的超循环中检测到无线模块捕获到一个数据包,并将这个数据包读取到内存中后,执行“拆包”过程,从无线模块捕获到的数据包中解析出有效的串行数据序列,并将它们装入串口发送FIFO缓冲队列中,等待串口中断服务程序将这些数据发送到串行通信总线上。
代码1- while (1)
- {
- /* RF Rx Process. */
- if (NRF24L01_GetRxPackageReadyFlag())
- {
- NRF24L01_GetRxPackage(gRfXferBuff);
- NRF24L01_ClearRxPackageReadyFlag();
- /* Move the data to Uart Tx Buffer. */
- for (i = 0U; i < gRfXferBuff[0]; i++)
- {
- if (!RBUF_IsFull(&gDrvUartTxBufStruct))
- {
- RBUF_PutDataIn(&gDrvUartTxBufStruct,
- gRfXferBuff[1U+i]);
- }
- }
- /* Enable the Uart Tx interrupt to transfer the data. */
- if (!DRV_UART_IsTxInterruptEnabled())
- {
- DRV_UART_EnableTxInterrupt(true);
- }
- }
- ...
- }
复制代码 在“拆包”过程完成后,检查串口中断服务的发送过程是否已经被启动:若是已经被启动,那么在将待发数据填充到串口发送FIFO缓冲队列之后,就不需要额外的操作了,串口中断服务会自动持续地将新加入的数据也发送到串行通信总线上;若是尚未启动,则此处手动启动发送中断,触发发送中断服务,启动其中的发送过程发送数据。在串口中断服务程序中,在向串行总线上搬数时,当发现串口发送FIFO缓冲队列即将搬空后,会关闭串口发送中断,清除发送过程的触发条件。
(2)“封包”过程
在main函数的超循环中会周期查看串口接收FIFO缓存队列中是否存在串口中断服务在串行通信总线上捕获的数据,一旦发现有数,就尽快将它们取出并打包成数据帧通过无线模块发送到空间中去。
代码2- while (1)
- {
- ...
- /* RF Tx Process. */
- i = 0U;
- while ( (!RBUF_IsEmpty(&gDrvUartRxBufStruct))
- && (i < (NRF24L01_XFER_PACKAGE_LENGTH-1U)) )
- {
- gRfXferBuff[1U+i] = RBUF_GetDataOut(&gDrvUartRxBufStruct);
- i++;
- }
- /* Send the package only when the tx buffer is not empty. */
- if (i != 0U)
- {
- gRfXferBuff[0U] = i;
- NRF24L01_TxPackage(gRfXferBuff);
- }
- }
复制代码 这里对发送过程做了一点优化。并不是每次检查串口接收FIFO缓存队列后都会调用无线模块驱动程序,毕竟在本设计中使用的无线模块发送过程需要独占CPU不短的一段时间(无线模块的驱动程序使用轮询模式实现)。若是从串口接收FIFO缓存队列中读数的计数值为0,即串口接收FIFO缓存队列为空,则不需要调用无线接收过程,从而可以尽快地将可能从串行总线上捕获的数据发送到空间中去。
3.3 串口中断服务中发送过程和接收过程的实现
由于串口发送和接收事件共享同一个中断入口,设计串口中断服务程序需要实现发送和捕获两个过程。实际上,串口的发送和接收是两个完全独立的过程,在产生发送或捕获事件时,另一个事件完全有可能同时发生,因此在共享的中断服务程序中要同时考虑到两类事件。分别设计串口引擎的发送和捕获处理过程时,可各自实现。
(1)捕获(接收)过程
捕获过程的处理逻辑相对简单,一旦串口引擎从串行通信总线上捕获到数并触发串口中断服务,那么串口中断服务中的接收过程就从串口接收寄存器中将捕获到的数读出(读操作自动清除铺货标志位)并搬运到接收FIFO缓存队列中。
代码3- void DRV_UART_IRQ_HANDLER_FUNC(void)
- {
- uint8_t tmp8;
- /* Rx Process. */
- if(USART_GetIntBitState( DRV_UART_INSTANCE, USART_INT_RBNE) != RESET)
- {
- tmp8 = (uint8_t)(USART_DataReceive(DRV_UART_INSTANCE) & 0x7F); /* Read Rx buffer to clear the flag. */
- if (!RBUF_IsFull(&gDrvUartRxBufStruct))
- {
- RBUF_PutDataIn(&gDrvUartRxBufStruct, tmp8);
- }
- }
- ...
- }
复制代码 在确定接收FIFO队列的长度时需要考虑其它进程消化此FIFO的速度,为了防止FIFO的溢出,应用程序应尽快从FIFO中读出数据,要么就得为FIFO预留更多的空间。
(2)发送过程
发送过程的处理与捕获过程相似,从串口发送FIFO缓存队列中搬数到发送缓冲寄存器中,但在触发条件上需要多考虑一点。在本设计中,发送过程的触发条件是发送缓冲寄存器有空位可以存放待发数据,但是当没数可发或者发送完成时时,发送缓冲缓冲寄存器也是空的,若是不加控制,此时也会不断触发中断,但却没有有效的数据可以填充发送缓冲寄存器以清除触发条件。
为了避免陷入无穷无尽的无效触发环节,就得加入对触发条件的控制。在向发送FIFO缓存队列填数后才能启动发送过程,此时触发发送条件,串口中断服务程序中的发送过程有数可以“喂”发送缓冲寄存器。每次进入串口中断服务程序,发送过程只会“喂”一个待发数据,在此过程中,若是有新的待发数据被加入到发送FIFO缓存队列,发送过程将会持续被触发,直至发送FIFO缓存队列被取空。每次发送过程从发送FIFO缓存队列中取数之后,都会检查发送FIFO是否为空,若是为空则关闭发送中断,那么在本次发送过程结束后,紧接着发送缓冲寄存器再为空时将不再会触发发送过程。
代码4- void DRV_UART_IRQ_HANDLER_FUNC(void)
- {
- ...
- /* Tx Process. */
- if( gDrvEnTxBufEmptyInterrupt && (USART_GetIntBitState(DRV_UART_INSTANCE, USART_INT_TBE) != RESET) )
- {
- USART_DataSend(DRV_UART_INSTANCE,
- RBUF_GetDataOut(&gDrvUartTxBufStruct));
- if (RBUF_IsEmpty(&gDrvUartTxBufStruct))
- {
- DRV_UART_EnableTxInterrupt(false);
- }
- }
- }
复制代码 实际上,目前比较流行的关闭发送中断的处理方式有两种:一种是本设计用到的这种,在每次发送过程中从发送FIFO中取数之后都再查看一下FIFO是否为空,以决定本次发送过程执行完毕之后是否结束整个发送过程,可以称为“最后一个顺手关门”的做法;另一种是在发送过程开始的时候,在从FIFO中取数之前检查FIFO是否为空,若是无数可取,则关闭发送中断结束发送过程,可以称为“开门之后再关门”的做法。若是使用“开门之后再关门”的做法,则发送过程的程序代码将会如下所示:
代码5- void DRV_UART_IRQ_HANDLER_FUNC(void)
- {
- ...
- /* Tx Process. */
- if( gDrvEnTxBufEmptyInterrupt && (USART_GetIntBitState(DRV_UART_INSTANCE, USART_INT_TBE) != RESET) )
- {
- if (RBUF_IsEmpty(&gDrvUartTxBufStruct))
- {
- DRV_UART_EnableTxInterrupt(false);
- }
- else
- {
- USART_DataSend(DRV_UART_INSTANCE, RBUF_GetDataOut(&gDrvUartTxBufStruct));
- }
- }
- }
复制代码 后者相对前者而言,控制逻辑是简单并列的,代码实现相对简单,但代价是需要多触发一次中断启动发送过程,最后一次的发送过程没有发送任何数据,只是关闭了中断,这个多出来的一次中断执行过程可能会为后续的开发中带来很多隐患,由此在本设计中选择了更安全的“最后一个顺手关门”做法。
另外,在进入发送过程前判断发送过程是否被触发,即检查发送中断是否打开才能进入发送过程,也是特别设计的。由于同捕获事件共享一个中断服务,所以若是检查这个条件,发送过程也可能被捕获事件触发。有意思的是,若是在程序逻辑的设计上采用“开门之后再关门”的做法,除了多了几次“开次门再关门”的过程,执行内容本身也不会出错,但若使用“最后一个顺手关门”的做法,在从空的发送FIFO缓存队列中取数时就会出现错误。对发送中断打开状态的检测实际上实对发送过程的一个保护机制,“最后一个顺手关门”的做法提前对整个发送过程提供了保护,而“开门之后再关门”的做法在开始操作发送FIFO缓存队列的时候才启动保护,而对另一个关闭发送中断的条件分支却没有提供保护,比较而言,前者也是更合理的选择。
4 演示效果
本设计需要两个同样的无线透传模块“配对”才能演示通信过程,两个节点互为收发。由于条件有限,在演示过程中只使用了一块基于GD32-Colibri-F207ZE开发板实现的无线透传模块,另一个节点使用了基于FRDM-KL02Z开发板实现的同样的无线透传模块。实际上,本设计最早也是基于FRDM-KL02Z开发板设计实现原型开发,之后才移植到GD32-Colibri-F207ZE开发板上,这也说明了本无线透传模块的设计具有良好的可移植性,只要在对底层硬件功能的调用进行适配,不改变上层应用,就可以实现同样的功能。若是有两块GD32-Colibri-F207ZE开发板实现的无线透传模块节点,向其中写入同一个程序即可实现两个节点的对传。
4.1 接线
GD32-Colibri-F207ZE开发板同NRF24L01P无线模块信号接线如表2所示。
表2 单片机与无线模块信号对接
接线完成如图4所示。
图4 GD32-Colibri-F207ZE开发板同NRF24L01P无线模块接线
4.2 运行程序
分别为两块开发板下载好程序之后,同时将串口(两块开发板都各自实现了用USB模拟UART串口的功能)对接到电脑上,在电脑上分别为两个串口打开通信终端,在彼此的终端界面中输出字符,都可以在对方的终端界面中显示出来。在演示实验中,电脑通过UART串行总线将字符发送一个节点中,该节点将串行字符通过无线发送到空间被另一个节点捕获,另一个节点从无线数据包提取字符并通过UART串行总线传回电脑。演示过程如图5、图6所示。
图6-5 两个无线透传节点
图6-6 无线对传终端界面
5 获取项目代码
本设计的相关代码已上传至开源代码的Git服务器,项目地址为:
https://git.oschina.net/suyong_yq/MyFwSDK_GD32F207.git
|