contiki SLIP代码分析
前言
本文通过分析slip_radio代码,结合C语法,体会contiki系统的文件的编写风格,理解contiki中的SLIP协议。
正文
对于Contiki OS,一个任务(线程)slip_radio的加载比较常见的方法是使用AUTOSTART_PROCESSES
对于一个线程,通常可以看到三部分
1,//线程声明PROCESS(xxxx_process, "xxxx process");
2,//线程加载AUTOSTART_PROCESSES(&xxxx_process);
3,//线程实体PROCESS_THREAD(xxxx_process, ev, data)
对于slip_radio_process,具体有
//线程声明
PROCESS(slip_radio_process, "Slip radio process");
//线程加载
AUTOSTART_PROCESSES(&slip_radio_process);
//线程实体
PROCESS_THREAD(slip_radio_process, ev, data)
PROCESS_THREAD(slip_radio_process, ev, data)
{
static struct etimer et;
PROCESS_BEGIN();
init();//slip初始化
NETSTACK_RDC.off(1);
#ifdef SLIP_RADIO_CONF_SENSORS
SLIP_RADIO_CONF_SENSORS.init(); //传感器
#endif
printf("Slip Radio started...\n");
etimer_set(&et, CLOCK_SECOND * 3); //设置事件定时器
while(1) {
PROCESS_YIELD();
if(etimer_expired(&et)) {
etimer_reset(&et);
#ifdef SLIP_RADIO_CONF_SENSORS
SLIP_RADIO_CONF_SENSORS.send();
#endif
}
}
PROCESS_END();
}
在slip_radio_process线程实体中,无操作是如下格式
PROCESS_BEGIN();
etimer_set(&et, CLOCK_SECOND * 3);
while(1) {
PROCESS_YIELD();
if(etimer_expired(&et)) {
etimer_reset(&et);
}
这里注意集中应用程序上的分析,暂不分析OS
初始化init();
在init()中可以看到
static void
init(void)
{
#ifndef BAUD2UBR
#define BAUD2UBR(baud) baud
#endif
slip_arch_init(BAUD2UBR(115200));
process_start(&slip_process, NULL);
slip_set_input_callback(slip_input_callback); //设置输入回调函数
//input_callback=slip_input_callback 映射
packet_pos = 0;
}
第一初始化硬件UART 115200
第二启动线程process_start(&slip_process, NULL);
第三设置输入回调函数,就是调用哪个函数处理输入的消息。这里分析一下怎么设置的
void
slip_set_input_callback(void (*c)(void))
{
input_callback = c;
}
Q:slip_set_input_callback传入参数void (*c)(void))啥意思?
A: 一个指向一个返回值和参数均为空的函数的指针c,即C是一个函数指针。
函数指针最常见的两个用途是转换表(jump table)和作为参数传递给另一个函数。
注意:作为参数传递时,赋值(初始化)时一定要注意指向的函数类型要一致。那么看看input_callback 的定义
static void (* input_callback)(void) = NULL;
发现函数指针input_callback指向一个返回值和参数均为空的函数,可见是一致的,编译器不会有问题。赋值之后就变成了函数指针input_callback 指向slip_input_callback函数。
slip_input_callback函数原型:
static void
slip_input_callback(void) //输入回调函数
{
PRINTF("SR-SIN: %u '%c%c'\n", uip_len, uip_buf[0], uip_buf[1]);
cmd_input(uip_buf, uip_len); //处理CMD输入
uip_len = 0;
}
核心-CMD输入函数cmd_input(uip_buf, uip_len);//处理CMD输入语句原型
void
cmd_input(const uint8_t *data, int data_len)
{
int i;
for(i = 0; cmd_handlers[i] != NULL; i++) {
if(cmd_handlers[i](data, data_len)) {
/* Command has been handled */
return;
}
}
/* Unknown command */
cmd_send((uint8_t *)"EUnknown command", 16);
}
cmd_input函数使用数组cmd_handlers[]循环处理传入的消息,这个数组有什么特点呢?
cmd_handlers[]定义跳转到宏定义
CMD_HANDLERS(CMD_CONF_HANDLERS);
首先这个是宏,看看完整的宏定义
typedef int (* cmd_handler_t)(const uint8_t *data, int len);
#define CMD_CONF_HANDLERS slip_radio_cmd_handler,cmd_handler_cc2520
#define CMD_HANDLERS(...) \
const cmd_handler_t cmd_handlers[] = {__VA_ARGS__, NULL}
先分析第一行,这句话用
typedef 定义了一个什么呢?
首先知道typedef int * zzu 定义了一个指向int类型的指针的新类型zzu,
在分析typedef 的定义时可以用下面的方法:
先去掉typedef 和别名, 剩下的就是原变量的类型
int (* )(const uint8_t *data, int len);
那么typedef
int (* cmd_handler_t)(const uint8_t *data,int len)定义了一个
cmd_handler_t新类型,就像int类型可定义一个int变量一样,cmd_handler_t可定义一个指向int (* )(const uint8_t *data, int len)的函数指针。
比如
定义一个函数指针 cmd handler_t cmd_handler_zzu
定义一个函数指针数组 cmd_handler_t cmd_handlers[]
分析第二行
#define CMD_CONF_HANDLERS slip_radio_cmd_handler,cmd_handler_cc2520
CMD_CONF_HANDLERS宏定义成两个函数。怎么用你知道吗?
且看第三行可变参数宏定义
#define CMD_HANDLERS(...) \
const cmd_handler_t cmd_handlers[] = {__VA_ARGS__, NULL}
宏定义
CMD_HANDLERS,特点有一个
__VA_ARGS__类型的预定义符号。
__VA_ARGS__ 是一个可变参数的宏,C99编译器当中出现的新类型,常见的预定义符号还有:
__LINE__ : 当前(源代码文件)行号 [整数]
__FILE__ : 当前正在编译的文件的文件名 [字符串]
__DATE__ : 当前日期,以“月月 日日 年年年年”的形式给出 [字符串]
__TIME__ : 当前时间,以“HH:mm:ss”的格式给出 [字符串]
__STDC__ : 如果编译器符合ANSI C标准,该宏为1,否则为0
__STDC_HOSTED__ : 如果实现了所有C标准库,该宏为1,否则为0
__STDC_VERSION__ : 被定义为199901L(不同编译器可能不一样,比如我用的gcc里就没有这个预定义符号)
注:这些预定义符号的首尾为
两个下划线,如果是两个单词,中间以
一个下划线连接。
这些符号不能用#define重新定义,也不能用#undef取消。即使用#define重新定义了,它的值还是系统默认定义的值。
总结输入消息处理
回头看cmd_handlers[]的宏定义
CMD_HANDLERS(CMD_CONF_HANDLERS);
解析成
const cmd_handler_t cmd_handlers[] = {slip_radio_cmd_handler,cmd_handler_cc2520};
即完成了
cmd_handlers[]数组的初始化,只不过这是一个
函数指针数组,里边的元素都是函数。
完整的解析如下:
const int (* cmd_handlers[])(const uint8_t *data, int len) = {slip_radio_cmd_handler,cmd_handler_cc2520};
看上去是不是很不好理解啊!还是使用typedef定义一个函数指针新类型,在定义数组比较好理解
在定义复杂的类型名字,如函数指针或者指向数组的指针时,使用typedef 更为合适。
在CMD输入函数cmd_input中for循环调用数元素 cmd_handlers[i]-函数指针,处理传入的参数、 cmd_handlers[i]是一个 cmd_handler_t类型即指向(int (* )(const uint8_t *data, int len)函数的指针,初始化为{slip_radio_cmd_handler,cmd_handler_cc2520}。
信息流输入:
step1:
SLIP(串口)输入的数据传到contiki输入接口input_callback(函数指针),该指针初始化指向slip_input_callback,即我们的消息回调函数,
step2:
cmd_input中利用for循环调用cmd_handlers函数指针数组的元素(函数)(slip_radio_cmd_handler,cmd_handler_cc2520)处理流入的消息。
技巧:这里可以看到宏定义,typedef,函数指针的影子。真是高手啊。
下面分析这两个消息处理函数
slip_radio_cmd_handler
函数通过数据帧定义来做相应的处理。
SLIP数据格式的定义
起始码 | 消息 | 结束码 | |||
Data-0 | Data-1 | Data-2 | Data-X~ | Data-N | Data-N |
| 消息码 | 数据 |
消息帧定义 | |||
命令码 | 功能 | 16进制(HEX) | |
Byte0 | Byte1 | 前两个字节表示命令类型 |
|
! | S | !S 发送数据 | 21 53 |
? | M | ?M查询MAC地址,副作用:返回!M+MAC地址 | 3F 4D |
! | M | !M返回MAC地址 | 21 4D |
! | C | !C设置RF的无线信道 |
|
? | C | ?C查询无线RF信道channel,副作用:返回!C+channel |
|
注:这里是测试用的,在实际应用中可以自定义相关命令码。 |
slip_radio_cmd_handler函数中
当接收到!S消息后调用 NETSTACK_MAC.send(packet_sent, &packet_ids[packet_pos])发送数据。层层调用最终通过无线RF发送;
当接收到?M查询MAC地址消息后,获得MAC地址,调用cmd_send(uip_buf, uip_len)通过串口返回数据。
//调用CMD发送
cmd_send(uip_buf, uip_len);
//直接调用宏
CMD_OUTPUT(data, data_len);
//#define CMD_CONF_OUTPUT slip_radio_cmd_output
//#define CMD_OUTPUT CMD_CONF_OUTPUT slip_send_packet(data, data_len);//slip_radio_cmd_output
slip_send_packet(data, data_len)原型
void
slip_send_packet(const uint8_t *ptr, int len)
{
uint16_t i;
uint8_t c;
slip_arch_writeb(SLIP_END);
for(i = 0; i < len; ++i) {
c = *ptr++;
if(c == SLIP_END) {
slip_arch_writeb(SLIP_ESC);
c = SLIP_ESC_END;
} else if(c == SLIP_ESC) {
slip_arch_writeb(SLIP_ESC);
c = SLIP_ESC_ESC;
}
slip_arch_writeb(c);
}
slip_arch_writeb(SLIP_END);
}
这里可以更清楚地理解SLIP的数据格式,起始码和结束码,并且在数据内进行了字符转义。
cmd_handler_cc2520(const uint8_t *data, int len)函数处理关于无线的消息。
当接收到!C消息后调用 cc2520_set_channel(data[2])设置RF的无线信道。
当接收到?C查询无线RF信道channel,cc2520_get_channel()获得信道,在返回!C无线RF信道channel。
关于RF发送接收,没有在cmd_handler_cc2520处理,而是交给contiki OS分层模式去处理。这一点比较好。