本文剖析Contiki最简单的实例hello_world,深入源码分析,详解了本实例用到的各个宏,进而给出一份完整展开的代码。最后把本实例用到的宏总结成API,并给出了创建一个进程的模型。
1. Hello World概览
hello_world.c
用于向串口打印"Hello World",源代码如下,
//filename:hello_world.c
#include "contiki.h"
#include "debug-uart.h" /* For usart_puts()*/
#include <stdio.h> /* For printf() */
PROCESS(hello_world_process, "Hello world"); /*参照二*/
AUTOSTART_PROCESSES(&hello_world_process); /*参照三*/
/*Define the process code*/
PROCESS_THREAD(hello_world_process, ev, data) /*参见四*/
{
PROCESS_BEGIN(); /*参照5.1*/
usart_puts("Hello, world!\n"); /*向串口打印字符串"Hello,world"*/
PROCESS_END(); /*参考5.2*/
}
接下来逐句分析。
2. PROCESS宏
PROCESS宏完成两个功能:
(1) 声明一个函数,该函数是进程的执行体,即进程的thread函数指针所指的函数
(2) 定义一个进程
源码展开如下:
//PROCESS(hello_world_process, "Hello world");
#define PROCESS(name, strname) PROCESS_THREAD(name, ev, data); \
struct process name = { NULL, strname, process_thread_##name }
对应参数展开为:
#define PROCESS((hello_world_process, "Hello world")
PROCESS_THREAD(hello_world_process, ev, data); \ /*分析见2.1*/
struct process hello_world_process = { NULL, "Hello world", process_thread_hello_world_process }; /*分析见2.2*/
2.1 PROCESS_THREAD宏
PROCESS_THREAD宏用于定义进程的执行主体,宏展开如下
#define PROCESS_THREAD(name, ev, data) \
static PT_THREAD(process_thread_##name(struct pt *process_pt, process_event_t ev, process_data_t data))
对应参数展开为:
//PROCESS_THREAD(hello_world_process, ev, data);
static PT_THREAD(process_thread_hello_world_process(struct pt *process_pt, process_event_t ev, process_data_t data)); /*分析见2.1.1*/
(1)PT_THREAD宏
PT_THREAD宏用于声明一个protothread,即进程的执行主体,宏展开如下:
#define PT_THREAD(name_args) char name_args
展开之后即为:
//static PT_THREAD(process_thread_hello_world_process(struct pt *process_pt, process_event_t ev, process_data_t data));
static char process_thread_hello_world_process(struct pt *process_pt, process_event_t ev, process_data_t data);
这下就很清楚了,声明一个静态的函数process_thread_hello_world_process
,返回值是char类型。
另,struct pt *process_pt
可以直接理解成lc
,用于保存当前被中断的地方(保存程序断点),以便下次恢复执行。
2.2 定义一个进程
PROCESS宏展开的第二句,定义一个进程hello_world_process,源码如下:
struct process hello_world_process = { NULL, "Hello world", process_thread_hello_world_process };
结构体process定义如下:
struct process
{
struct process *next;
const char *name; /*此处略作简化,源代码包含了预编译#if。即可以通过配置,使得进程名称可有可无*/
PT_THREAD((* thread)(struct pt *, process_event_t, process_data_t));
struct pt pt;
unsigned char state, needspoll;
};
可见进程hello_world_process 的lc
、state
、needspoll
都默认置为0。关于process结构体请参见博文《Contiki主要数据结构之进程》。
3. AUTOSTART_PROCESSES宏
AUTOSTART_PROCESSES
宏实际上是定义一个指针数组,存放Contiki系统运行时需自动启动的进程,宏展开如下:
//AUTOSTART_PROCESSES(&hello_world_process);
#define AUTOSTART_PROCESSES(...) \ struct process * const autostart_processes[] = {__VA_ARGS__, NULL}
这里用到C99 支持可变参数宏的特性,如:#define debug(…) printf(__VA_ARGS__)
,缺省号代表一个可以变化的参数表,宏展开时,实际的参数就传递给 printf()
了。例:debug("Y = %d\n", y);
被替换成printf("Y = %d\n", y);
。那么,AUTOSTART_PROCESSES(&hello_world_process);
实际上被替换成:
struct process * const autostart_processes[] = {&hello_world_process, NULL};
这样就知道如何让多个进程自启动了,直接在宏AUTOSTART_PROCESSES()
加入需自启动的进程地址,比如让hello_process和world_process这两个进程自启动,如下:
AUTOSTART_PROCESSES(&hello_process,&world_process);
最后一个进程指针设成NULL,则是一种编程技巧,设置一个哨兵(提高算法效率的一个手段),以提高遍历整个数组的效率。
4. PROCESS_THREAD宏
PROCESS(hello_world_process, "Hello world");
展开成两句,其中有一句是也是PROCESS_THREAD(hello_world_process, ev, data) ;
。这里要注意到分号,是一个函数声明。而这PROCESS_THREAD(hello_world_process, ev, data)
没有分号,而是紧跟着{}
,是上述声明函数的实现。关于PROCESS_THREAD宏的分析,最后展开如下,展开过程参见上述2.1。
static char process_thread_hello_world_process(struct pt *process_pt, process_event_t ev, process_data_t data);
温馨提示:在阅读Contiki源码,手动展开宏时,要特别注意分号。
5. PROCESS_BEGIN宏和PROCESS_END宏
原则上,所有代码都得放在PROCESS_BEGIN
宏和PROCESS_END
宏之间(如果程序全部使用静态局部变量,这样做总是对的。倘若使用局部变量,情况就比较复杂了,当然,不建议这样做),看完下面宏展开,就知道为什么了。
5.1 PROCESS_BEGIN宏
PROCESS_BEGIN宏一步步展开如下:
#define PROCESS_BEGIN() PT_BEGIN(process_pt)
process_pt
是struct pt*
类型,在函数头传递过来的参数(见四),直接理解成lc,用于保存当前被中断的地方,以便下次恢复执行。继续展开:
#define PT_BEGIN(pt) { char PT_YIELD_FLAG = 1; LC_RESUME((pt)->lc)
#define LC_RESUME(s) switch(s) { case 0:
把参数替换,结果如下:
{
char PT_YIELD_FLAG = 1; /*将PT_YIELD_FLAG置1,类似于关中断???*/
switch(process_pt->lc) /*程序根据lc的值进行跳转,lc用于保存程序断点*/
{
case 0: /*第一次执行从这里开始,可以放一些初始化的东东*/
;
很奇怪是吧,PROCESS_BEGIN
宏展开都不是完整的语句,别急,看完下面的PROCESS_END
就知道Contiki这些天才们是怎么设计的。
5.2 PROCESS_END宏
PROCESS_END
宏一步步展开如下:
#define PROCESS_END() PT_END(process_pt)
#define PT_END(pt) LC_END((pt)->lc); PT_YIELD_FLAG = 0; \ PT_INIT(pt); return PT_ENDED; }
#define LC_END(s) }
#define PT_INIT(pt) LC_INIT((pt)->lc)
#define LC_INIT(s) s = 0;
#define PT_ENDED 3
整理下,实际上如下代码:
}
PT_YIELD_FLAG = 0;
(process_pt)->pt = 0;
return 3;
}
好了,现在回过头来看,PROCESS_BEGIN
宏和PROCESS_END
宏是如此的般配,天生一对呀,祝福他们:-)
6. 总结
6.1 宏全部展开
根据上述的分析,该实例全部展开的代码如下,再浏览下,是不是有种神清气爽的感觉:-)
#include "contiki.h"
#include "debug-uart.h" /* For usart_puts()*/
#include <stdio.h> /* For printf() */
static char process_thread_hello_world_process(struct pt *process_pt, process_event_t ev, process_data_t data);
struct process hello_world_process = { ((void *)0), "Hello world process", process_thread_hello_world_process};
struct process * const autostart_processes[] = {&hello_world_process, ((void *)0)};
char process_thread_hello_world_process(struct pt *process_pt, process_event_t ev, process_data_t data)
{
{
char PT_YIELD_FLAG = 1;
switch((process_pt)->lc)
{
case 0:
;
usart_puts("Hello, world!\n");
};
}
PT_YIELD_FLAG = 0;
(process_pt)->lc = 0;;
return 3;
}
6.2 宏总结
对本实例用到的宏总结如上,以后就直接把宏当API用了。
PROCESS(name, strname)
,声明进程name
的主体函数process_thread_##name(进程的thread函数指针所指的函数)
,并定义一个进程name
AUTOSTART_PROCESSES(...)
,定义一个进程指针数组autostart_processes
PROCESS_THREAD(name, ev, data)
,进程name的定义或声明,取决于宏后面是;
还是{}
PROCESS_BEGIN()
,进程的主体函数从这里开始PROCESS_END()
,进程的主体函数从这里结束
6.3 编程模型
这实例虽说很简单,但却给出了定义一个进程的模型(还以Hello world为例),实际编程过程中,只需要将usart_puts("Hello, world!\n");
换成自己需要实现的代码。
//假设进程名称为Hello world
#include "contiki.h"
#include <stdio.h> /* For printf() */
PROCESS(hello_world_process, "Hello world"); //PROCESS(name, strname)
AUTOSTART_PROCESSES(&hello_world_process); //AUTOSTART_PROCESS(...)
/*Define the process code*/
PROCESS_THREAD(hello_world_process, ev, data) //PROCESS_THREAD(name, ev, data)
{
PROCESS_BEGIN();
/***这里填入你的代码***/
PROCESS_END();
}
注:声明变量最好不要放在PROCESS_BEGIN
之前,因为进程再次被调度,总是从头开始执行,直到PROCESS_BEGIN
宏中的switch
判断才跳转到断点case __LINE__
。也就是说,进程被调度总是会执行PROCESS_BEGIN
之前的代码。