ZStack中的编程技巧
1. 像函数一样使用的宏
//这个宏,用来被其他宏使用,构造一个正确有效的表达式。这个适合于一些离散语句的组合,不适合函数的重新命名
#define st(x) do { x } while (__LINE__ == -1)
例如:#define aps_GroupsRemaingCapacity() ( APS_MAX_GROUPS - aps_CountAllGroups() )
上述的这个宏,调用的其他函数来实现其功能,因此,不适合使用st()宏。
使用场景: aps_GroupsRemaingCapacity(); 或者 aps_GroupsRemaingCapacity() 当做函数的参数。
例如:#define AES_SET_MODE(mode) st( ENCCS &= ~0x70; ENCCS |= mode; )
上述这个宏,使用st宏来嵌套,功能为设置AES加密模式。
使用场景: 向个函数一样调用 AES_SET_MODE(AES_MODE_CTR); 即可。
2. 消息队列中的存储分配机制
在ZStack中,每个任务都在OSAL的大循环中轮训处理,任务与任务之间,任务与OSAL之间,是通过消息来传递信息的。每一个任务都对于一系列事件,系统发给每个任务的事件为UINT16 events,它采用独热码的机制,2个字节,一共可以处理16个事件。
#define SYS_EVENT_MSG 0x8000 //系统任务事件 #define ZDO_CB_MSG 0xD3 //系统任务事件里面的ZDO消息 #define AF_DATA_CONFIRM_CMD 0xFD //系统任务事件里面的接收数据指示消息 #define AF_INCOMING_MSG_CMD 0x1A //系统任务事件里面的接收数据消息 #define ZDO_STATE_CHANGE 0xD1 //系统任务事件里面的ZDO状态改变消息 #define KEY_CHANGE 0xC0 //系统任务事件里面的按键消息 其他事件为用户自定义事件,可以使用的值为0x4000,0x2000,0x1000,0x8000...依次类推,供有15个自定义事件,可以通过 osal_start_timerEx( ZDAppTaskID, ZDO_NETWORK_INIT, delay ); //延迟发起事件 osal_set_event( ZDAppTaskID, ZDO_NETWORK_INIT ); //立刻发起事件 来进行通知。
2.1 消息分配
函数原型: uint8 * osal_msg_allocate( uint16 len )
使用场景:任务调用此函数用于分配指定长度的消息缓冲。实际上,分配的长度为len加上消息头的长度
消息头结构体如下:
typedef struct { void *next; //指向下一个消息 uint16 len; uint8 dest_id; //指向当前的任务ID,分配消息时,初始化为TASK_NO_TASK } osal_msg_hdr_t; //消息头结构体 typedef struct { uint8 event; uint8 status; } osal_event_hdr_t; //事件结构体
osal_msg_allocate实际返回的指针为指向len长度的地址空间。 实际使用时,返回值经常被强制类型转换,以下面的函数为例子说明:
/********************************************************************* * @fn ZDO_SendMsgCBs * * @brief This function sends messages to registered tasks. * Local to ZDO and shouldn't be called outside of ZDO. * * @param inMsg - incoming message * * @return TRUE if sent to at least 1 task, FALSE if not */ uint8 ZDO_SendMsgCBs( zdoIncomingMsg_t *inMsg ) { uint8 ret = FALSE; ZDO_MsgCB_t *pList = zdoMsgCBs; while ( pList ) { if ( pList->clusterID == inMsg->clusterID ) //对比接收到数据中的簇ID和ZDO中注册的簇ID { zdoIncomingMsg_t *msgPtr; // Send the address to the task msgPtr = (zdoIncomingMsg_t *)osal_msg_allocate( sizeof( zdoIncomingMsg_t ) + inMsg->asduLen ); //分配消息缓存,包含消息头和数据长度 if ( msgPtr ) { // copy struct osal_memcpy( msgPtr, inMsg, sizeof( zdoIncomingMsg_t )); //分配成功,拷贝相关内容到消息中 if ( inMsg->asduLen ) { msgPtr->asdu = (byte*)(((byte*)msgPtr) + sizeof( zdoIncomingMsg_t )); osal_memcpy( msgPtr->asdu, inMsg->asdu, inMsg->asduLen ); } msgPtr->hdr.event = ZDO_CB_MSG; //声明此消息的事件类型 osal_msg_send( pList->taskID, (uint8 *)msgPtr ); //将此消息发送给指定的任务ID ret = TRUE; } } pList = (ZDO_MsgCB_t *)pList->next; // 与当前ZDO中的簇ID不符,指向下一个ZDO的注册消息 } return ( ret ); }
在osal_msg_send中,用到了几个宏,
//获得msg_ptr所指向的下一个消息
#define OSAL_MSG_NEXT(msg_ptr) ((osal_msg_hdr_t *) (msg_ptr) - 1)->next
//获得msg_ptr所指向的任务(消息分配时,初始化为TASK_NO_TASK)
#define OSAL_MSG_ID(msg_ptr) ((osal_msg_hdr_t *) (msg_ptr) - 1)->dest_id
uint8 osal_msg_send( uint8 destination_task, uint8 *msg_ptr ) { if ( msg_ptr == NULL ) return ( INVALID_MSG_POINTER ); if ( destination_task >= tasksCnt ) { osal_msg_deallocate( msg_ptr ); return ( INVALID_TASK ); } //参数有效性检查 // Check the message header if ( OSAL_MSG_NEXT( msg_ptr ) != NULL || OSAL_MSG_ID( msg_ptr ) != TASK_NO_TASK ) { osal_msg_deallocate( msg_ptr ); return ( INVALID_MSG_POINTER ); } //确保当前消息为新分配的消息 OSAL_MSG_ID( msg_ptr ) = destination_task; //将当前消息的任务指定为入口参数 // queue message osal_msg_enqueue( &osal_qHead, msg_ptr ); //将此消息插入到全局消息队列的尾部 // Signal the task that a message is waiting osal_set_event( destination_task, SYS_EVENT_MSG ); //给此任务发起一个系统消息事件 return ( SUCCESS ); }
/********************************************************************* * @fn osal_set_event * * @brief * * This function is called to set the event flags for a task. The * event passed in is OR'd into the task's event variable. * * @param uint8 task_id - receiving tasks ID * @param uint8 event_flag - what event to set * * @return SUCCESS, INVALID_TASK */ uint8 osal_set_event( uint8 task_id, uint16 event_flag ) { if ( task_id < tasksCnt ) { halIntState_t intState; HAL_ENTER_CRITICAL_SECTION(intState); // Hold off interrupts tasksEvents[task_id] |= event_flag; // Stuff the event bit(s) 给当前的task赋予事件值,以便于在OSAL大循环中可以处理 HAL_EXIT_CRITICAL_SECTION(intState); // Release interrupts return ( SUCCESS ); } else { return ( INVALID_TASK ); } }
在对于任务的事件处理中,需要用到osal_msg_receive(task_id)函数,从指定事件下获取对于的消息。然后循环取消息,循环处理,直到SYS_EVENT_MSG中的消息都处理完了才会退出。
系统事件下的消息类型结构体定义,在开始的部分都采用添加了事件头,便于后级循环处理。
OSAL事件头 事件内部的消息头结构,不同的消息有不同的数据内容,但有一点是相同的,就是消息头结构体的第一个元素一定是OSAL事件头。
以上大概就是OSAL的消息机制。网上有很多其他资料整理的很好,但是,总觉的不是自己的,自己写出来,才能真实的消化掉。
以按键消息为例子:OSAL的消息头(上)+OSAL的事件头(中)+事件内部的数据头(下)
,结合上图和以上的理解,是不是一下子很清楚啦。
消息入队:
3.定义变量的技巧
enum
{
ZDO_SRC_RTG_IND_CBID, // 0
ZDO_CONCENTRATOR_IND_CBID, // 1
ZDO_NWK_DISCOVERY_CNF_CBID, // 2
ZDO_BEACON_NOTIFY_IND_CBID, // 3
ZDO_JOIN_CNF_CBID, // 4
ZDO_LEAVE_CNF_CBID, // 5
ZDO_LEAVE_IND_CBID, // 6
MAX_ZDO_CB_FUNC // 7(枚举变量个数,数组索引0~6)
// Must be at the bottom of the list
};
上述枚举变量,前面都是名称的定义,取值从0开始,依次递增,后续,需要增加一个入口的话,直接在倒数第一个之前添加就可以了,其他的都不用改。
定义通用函数指针时,入口参数和出口参数最好都设置成void*类型,方便后续类型转换。
函数指针声明: /* ZDO Indication Callback Registration */ typedef void* (*pfnZdoCb)( void *param ); 函数指针数组定义: pfnZdoCb zdoCBFunc[MAX_ZDO_CB_FUNC]; 函数指针数组初始化 void ZDApp_InitZdoCBFunc( void ) { uint8 i; for ( i=0; i< MAX_ZDO_CB_FUNC; i++ ) { zdoCBFunc[i] = NULL; } }
自定义结构体:
一律采用如下的定义方式:
typedef struct //不需要结构体名 { osal_event_hdr_t hdr; uint8 endpoint; uint8 transID; } afDataConfirm_t; //指定重新定义的结构体名,后面加上_t,表示是一个自定义类型
在具体使用时, 使用 afDataConfirm_t *afDataConfirm;
4.宏定义
对于一些较长的宏定义,建议加上分行符来写,格式规范,便于检查。注意,最后一行,不能有分行符
#define SystemReset() \
{ \
HAL_DISABLE_INTERRUPTS(); \
HAL_SYSTEM_RESET(); \
}
在宏定义中,对于一些预编译的条件判断,可以添加#error指令,来确保在编译过程中,保持条件配置的一致性。
对于一些需要使用默认配置的选项,采用如下的定义方法:
如果不做任何其他的设定,默认是开启ADC功能的。如果用户要自定义开启或者关闭,则定义
#define HAL_ADC TRUE 或者 #define HAL_ADC FALSE 就可以了。
在条件宏定义中,采用适当的缩进比例,便于检查。
看到左边的减号了吗?那是编译器提供的折叠功能,使用起来也很方便,但是,适当的缩进,还是很有必要的,便于配对检查,不多不漏。
可以在编译时,添加对过期模块的编译检查,在软件升级时,可能需要用到。
5. 在编译中查看宏值
采用如下的宏,可以在编译时,打印出想要查看的宏的内容。这个技巧,在之前的博文里面提到过,觉的很好用,再次再提一次。
查看已定义宏内容:
输出内容:
查看未定义宏内容:
输出内容:
一般编译器都支持message伪指令,因此,这个小技巧很好用。
如果需要对一些延迟做较为精确的计算或者对时序要求比较严格的操作,在函数上面加上禁止优化的伪指令
#pragma optimize=none //指示不对A函数进行优化 void A() {
}
6. 调试
设计一个程序,重要的是它的设计流程。
所有程序都是调试出来的,不是写出来的。如何去调试,实际上是如何却解决问题。
把问题拆开来思考,
7. 用于设置寄存器的宏
以ZStack中的hal_lcd.c中的寄存器设置为例子。
使用时,直接使用LCD_DO_WRITE就可以了,像个函数一样用它。从名字上可以看出,它是往第port的第pin个管脚写val数值。LCD_DO_WRITE宏执行的功能 就是往P0.0写1.在这里,P0_0需要有相应的定义才可以正确进行,如果移植到其他处理机,做相应的更改就可以了。
一般来说,IO线都有复用的功能,建议使用不同的宏分开设置。如下图所示:
前两个是针对普通GPIO来设置的宏,功能也是类似的,向port的pin管脚输出val值。
后两个是针对peripheral function的GPIO来设置的宏。