做了一个使用CC2530和ESP8266模块连接,检测温湿度和光照强度,通过ESP8266上传数据并且接受指令控制led。遇见很多坑,废寝忘食的全部解决了,特在此记录一下以供以后参考。
一般情况编写zigbee程序用的是IAR8050,对于for语句有一个很隐秘的坑,就是如果用for语句构建的delay延迟函数会在编译烧录到单片机里却发现没有延迟,延迟函数似乎失效了。我上网找了一下发现是iar有个功能是编译时候会优化程序。
如图所示,这里我已经改成NONE了,之前是high,由于延时函数往往是单纯的for的循环计算,在高程度优化后,for语句以为内部没有内容,就会在编译的时候删掉,从而延迟失效。
根据CC2530的寄存器手册可知,如图所示
U0,U1两个串口分别各自有两套引脚方案,U0是P02,P03或者P14,P15,U1是P04和P05或者P16和P17,由于我的仿真器要用到P1口,而且U0连接着TTL转USB,所以和ESP8266的串口通讯需要用U1,但是在zstack中U1默认是用P16和P17作为TX和RX。而且zstack只能用一个USART,因为在zstack工程中,如图所示,使用U0或U1是靠配置中的宏定义ZTOOL_P2或者ZTOOL_P1确定的。
两者似乎不能同时存在,如果同时存在也是优先于ZTOOL_P2,在写串口函数HalUARTWrite中我们可以看到如下图。
使用哪个串口是由HAL_UART_DMA的值确定的,去寻找其宏定义如下图
由上图可以看到宏定义的值还是由ZTOOL_P2或者P1决定的,而且如果定义了P2,则P1似乎没有作用了。
在zstack中,U1默认使用P1口,而且没有选择为P0口的选项,如果使用U1,则zstack会默认配置P1IO口,通过HAL_uart.c文件可以找到串口初始化函数,默认使用DMA方式的串口。
进入这个函数后可以看到,zstack正是在这里配置了串口的IO口,分别赋值了PERCFG,PxSEL,ADCCFG,这几个关于IO口配置的寄存器。在给PERCFG赋值时,在使用U1的情况下,原来的程序是PERCFG |= HAL_UART_PERCFG_BIT;
查看HAL_UART_PERCFG_BIT的宏定义可以看到在注释中TI已经说U1就是设置在P1上的。
从数据手册中查看PERCFG的内容可以看到U1就是01,U2的设置就是02,而上文提到的PERCFG |= HAL_UART_PERCFG_BIT;就是在PERCFG第一位赋值1,我改成了PERCFG &= ~HAL_UART_PERCFG_BIT;这样U1串口就可以使用P04,P05作为引脚了。
在下两条语句ADCCFG &= ~HAL_UART_Px_RX_TX;PxSEL? |= HAL_UART_Px_RX_TX;中就是配置RX和TX的IO口,一个是避免作为ADC引脚,一个是设置为外设功能。如下图所示
查看HAL_UART_Px_RX_TX的宏定义如下图。
原本其值为0xC0,也就是11000000,即P16和P17,现在是P04和P05,所以改为0X30。‘
还有一个地方就是PxSEL和其他x有关的值,查找宏定义如下图,将原来的P1改为P0,下面从UxCSR开始则不改,因为是涉及到串口的配置。
配置完以上部分就可以使用U1了,同样U0的P1口类似。
一开始,我是放在编写了一个初始化子函数然后放在SampleApp_Init( uint8 task_id )中的,后来发现根本无法完成初始化/(ㄒoㄒ)/~~。因为之前用过32连接8266,有相关的经验,但是32的库函数和ZSTACK有一个很大的差别,就是ZSTACK串口默认用DMA,所以串口接收和发送不会产生中断,众所周知(*^_^*),ESP8266模块通过AT指令来完成初始化,为了能确定是否每一条AT指令配置成功,需要检测ESP8266是否传来“OK”,在32中,可以通过while语句循环发送AT指令并通过串口中断检测是否传来“OK”,而在zstack中则不可以用while语句,因为串口的接收不会产生中断,所以会卡死在while中。所以不能放在SampleApp_Init( uint8 task_id )中。至于怎么解决,在后面会写到。
Zstack是很经典的轮询方式来执行程序的,通过循环检测一系列任务是否需要执行来工作的,这就导致使用HalUARTWrite函数其实是将待发送内容放到指定的DMA读取空间,并没有放到串口上。在之后的一遍的任务轮询时,会检测到有DMA的任务,这里才将待发送内容通过DMA放到串口上。这就导致了使用HalUARTWrite并不能立马从串口发送出去,不具有及时性,编程时需要考虑到这一点。如果可以实时仿真的话,会发现运行完串口发送函数,串口并没有发送,就是这个原因。
根据该函数的定义如下图可知。
串口的接受同样使用的时DMA,在外部通过串口发送信息到单片机的那一瞬间,DMA已经将内容从串口接收到一个区域(具体在哪不知道,也许是随机的,也许是固定的)。而HalUARTRead()函数实际上是将该区域的内容放到一个程序中定义的区域(如字符串数组)中去。HalUARTRead(uint8 port, uint8 *buf, uint16 len)函数中有三个参数,port是串口号,我这里用的1,buf是程序定义区域的首地址,len是读取最大长度。
那么问题来了,如果设置的读取最大长度是10,可是发送来了20个字节的信息,那么会选取哪部分呢,这个函数不是会选取20中一部分10字节的内容,而是从buf的首地址开始,往后依次写10个字节,然后继续从首地址开始,将后十个字节写上去,覆盖了前十个字节,所以在编写程序使用该函数时,需要注意到这一点。
又因为采用轮询制执行程序,所以在读取接收的内容之前,DMA会将在此之间传入串口的任意字符读取并储存,并且按照接收先后顺序储存,具体能储存多少字符暂时未知。所以为了防止想要接收的字符串被覆盖,需要提前预估接收最大值len的值。
前面说到因为轮询制,而不能产生中断,所以不能将初始化子函数放在SampleApp_Init( uint8 task_id )中,为了解决不能用while语句导致的初始化无法进行,我选择将初始化函数放在SampleApp_ProcessEvent()中作为一个事件来处理,如下图所示。
因为协调器与ESP8266相连,所以只有协调器需要初始化ESP8266,由于初始化8266需要数个AT指令,所以我以轮询为循环,在一次次循环中检测AT指令的回复和发送AT指令。通过ESP_flag值的改变来确定每一次循环需要执行哪一条AT指令,并且在一次循环中获取串口接收内容,判断是否进行下一条AT指令。
由于我使用的是服务器模式,且使用为多连接,所以ESP8266不能使用透传方式发送,需要先发送指令AT+CIPSEND=【客户端ID号】,【字符串长度】,在收到OK>后传输内容,如下图所示。
在这里我依然利用轮询作为循环,先发送AT指令,在第二次轮询接收到OK后发送内容。关于如何利用轮询作为循环,在后面还会讲到。
strstsr函数是检测第一项参数的字符串内是否有第二项参数的字符串。但是需要注意的是,strstr没有规定检测到多少位,那如果这样的话,岂不是如果没一直没有要寻找的字符串,将会一直寻找下去吗?为了避免这样的情况发生,strstr规定当检测到某一位又0即字符“\0”,则停止检测,并返回NULL,如果在此之前已经找到目标字符串,则返回地址。
由于strstr遇到0就会退出并返回NULL,如果我们要寻找的字符串在0之后,则会明明有却无法检测出来。比如ESP8266在执行完AT指令后会将执行的AT指令和执行结果(OK或ERROR)一起送入串口,但很不巧,AT指令和结果中间可能出现0,导致我们无法通过strstr函数得知是否有OK,为了防止这样的情况发生,我补充了一个修正函数。
void correct_string(uint8 *buff, uint16 len)
{
uint16 i;
for(i=0;i<len-1;i++)
{
if((* (buff+i))==0)
{
(* (buff+i))=1;
}
}
(* (buff+len-1))=0;
}
该修正函数会使在指定地址开始的指定长度区域中0改为1,这样就可以避免strstr函数检测到0,并在最后一位置0,这样则防止strstr运行时间过长。
如下图所示,在接收函数SampleApp_MessageMSGCB()中我是用发送函数像ESP8266发送信息。
由于发送分两步执行,所以启用事件发生时钟函数:
osal_start_timerEx( SampleApp_TaskID,
SAMPLEAPP_ESP_SEND_EVT,
SAMPLEAPP_ESP_SEND_EVT_TIMEOUT );
使在指定时间后再执行一次esp8266_send();如下图所示,
如果发送成功则清除ESP8266发送缓冲区espdata的内容,如果发送失败则重新发送AT+CIPSEND。
zstack中事件的添加是在SampleApp.h中完成的,如下图所示。
分别配置了事件ID和事件发生时间,其中需要注意的是事件的设置,Zstack规定每一个任务最多16个事件,为什么是16个事件呢(???(???(???*)?这里我们可以从函数SampleApp_ProcessEvent()中看出端倪,如下图所示。
单片机是如何知道到底是哪个事件发生了呢,从语句
if ( events & SAMPLEAPP_ESP_CONFIG_EVT )我们可以看到,zstack通过将events和事件ID对比,进行与运算,如果结果为1则是该事件。因为是与运算,所以若要event和某个事件ID与之后为1,和其他事件为0,则只能在16位二进制中只有1位为1,所以只能最多有16个事件,如果有仿真条件的话,可以看到当为系统事件时,即
? if ( events & SYS_EVENT_MSG ),events的值为32768,二进制就是1000 0000 0000 0000。所以在添加事件时候ID只能为0X0001,0X0002,0X0004,0X0008等,不能为其他值,比如0X0003,如果这样的话,就会执行事件ID为0X0002的处理程序。
如果用过Zstack的ADC的伙伴们一定会对Zstack超级烂的宏定义有所印象,这也是一个老生常谈的问题了,在这里我就顺带总结一下并给出自己的理解。
ADC读取电压函数HalAdcRead (uint8 channel, uint8 resolution)最有争议的部分如下图。
那就是分辨率的问题,根据Zstack的宏定义可知分辨率分别为8,10,12,14。然而,根据数据手册如下图所示可知分辨率为7,9,10,12。实际上TI工作人员可能高估了CC2530的adc的性能,后来实验发现只能最高12位。
所以下面的代码要修改为这样:
switch (resolution)
{
case HAL_ADC_RESOLUTION_8:
reading >>= 9;
break;
case HAL_ADC_RESOLUTION_10:
reading >>= 7;
break;
case HAL_ADC_RESOLUTION_12:
reading >>= 6;
break;
case HAL_ADC_RESOLUTION_14:
default:
reading >>= 4;
break;
}
因为ADC转换的值放在以下寄存器,可知最后两位时保留的,不能用,Zstack是通过将ADCH的值左移八位加上ADCL的值,而真正有效的值是从最高位15位往低位方向的分辨率对应的位数,比如12分辨率就是4位到15位,所以程序中又右移了4位。
接下来就是ADC的计算,毕竟我们不可能直接输出一个16位的变量,所以我们必须将其转化为某个电压,一般来说,就是(adc的16位变量)×(参考电压)/(分辨率的十进制)。但是,(●ˇ?ˇ●),注意ADC的变量是带符号的,也就是说12位分辨率输出的变量最高位为符号位,所以其实真正的值是后面11位,所以这11位才是真正的分辨率,所以要除以2^(11)即2048而不是4096。
以上就是我最近做的一个项目踩过的坑和总结的经验,还包括了2023年学习Zigbee的一些经验。如果还有机会,还能有新的经验和教训我将继续更新。希望本文对各位有所帮助~( ?? ω ?? )y