Contiki-NG概述
Contiki-NG是物联网中资源受限设备的操作系统。 Contiki-NG包含符合 RFC 的低功耗 IPv6 通信堆栈,可实现 Internet 连接。
据官方描述,Contiki-NG代码占用量约为 100 kB,内存使用量可配置为低至 10 kB。个人认为这应该是系统基本无裁剪的情况下的资源占用。
Contiki-NG除了自带低功耗IPv6协议栈(6LoWPAN)之外,其本身也是一个优秀的事件驱动系统。对于事件驱动系统,除了Contiki-NG之外,笔者只接触过TI的Zigbee协议栈。Zigbee协议栈基于OSAL系统,整个系统分为很多层次(应用层、应用支持子层、网络层、MAC层、物理层等),每个层次分别轮询各个层次的事件,底层的事件比上层的优先级高,当有事件到来时,则触发事件处理函数的运行。这样的处理机制是不是很像平时单片机裸机情况下跑的轮询程序?在while(1)循环中判断某个事件标志位是否置1,置1则运行相对应的事件处理。
Contiki-NG也是同样的事件驱动机制,但它将事件驱动机制抽象成了线程处理模型,这也是笔者对此感兴趣的一个比较大的原因。具体是什么意思呢?我们来看一段Contiki-NG的例程,如下:
PROCESS(hello_world_process, "Hello world process");
AUTOSTART_PROCESSES(&hello_world_process);
PROCESS_THREAD(hello_world_process, ev, data)
{
static struct etimer timer;
PROCESS_BEGIN();
etimer_set(&timer, CLOCK_SECOND * 100);
while(1) {
printf("Hello, world\n");
PROCESS_WAIT_EVENT_UNTIL(etimer_expired(&timer));
etimer_reset(&timer);
}
PROCESS_END();
}
看了以上代码,如果你不了解Contiki-NG,是否会觉得这是一个实时系统?笔者稍微解释一下这段代码的意思:
- PROCESS和AUTOSTART_PROCESSES这两个宏定义了一个helloworld任务;
- PROCESS_THREAD宏函数的执行内容即是任务的执行内容;
- 在Contiki-NG中,每个PROCESS的开始都需要有PROCESS_BEGIN,结束位置要有对应的PROCESS_END,并且Contiki-NG是不可抢占的系统,所以每个PROCESS在执行事件处理后必须主动交出CPU的使用权,也就是代码中PROCESS_WAIT_EVENT_UNTIL的作用。
事实上,Contiki-NG在初始化完毕之后,就进入主循环while(1)中,在循环中不断地检测是否有事件发生,并把事件分发到具体的PROCESS,假设有事件触发了上面的hello_world_process会怎样呢?CPU就会开始运行hello_world_process中的while(1),那问题来了,CPU如何退出hello_world_process的执行,然后把使用权交给事件调度器呢?下一次又有事件发送给hello_world_process时,hello_world_process又是怎么实现继续之前的执行呢(而不是重新开始执行)?
答案就在PROCESS_BEGIN、PROCESS_END、PROCESS_WAIT_EVENT_UNTIL这些宏的实现里面,本篇就不对这些原理进行叙述。
Contiki-NG移植
移植说明
本文的Contiki-NG移植是在下载的源码中添加GD32F310平台。一个系统的适配不是一蹴而就的,需要对gpio、usart、timer、watchdog等等一一进行适配,甚至后期可能还需要做一些代码的优化。本文在发布的时候呢,只适配了跑“Helloworld”例程所需的基本组件和驱动,另外,GD32F310本身只有8k的ram,没有集成RF,因此暂时将系统的射频和网络协议栈部分裁剪掉。
Contiki-NG工程默认是用Makefile管理工程的,初期为了避免Makefile引入的其他问题,先在Keil中移植GD32F310平台。
移植关键步骤
建立Keil工程
建立工程文件夹,将对应的C文件分类存放,笔者建立的目录如下:
- app:存放应用逻辑程序
- cmsis:存放arm内核提供给芯片厂商的接口文件及具体实现
- cpu:Contiki-NG存放芯片相关源码的目录
- os:Contiki-NG存放系统源码的目录
- platform:Contiki-NG存放平台相关源码的目录
- stdlib:GD32F310的标准库
- output:编译输出文件目录
前期的目的是为了跑通整个系统,一些不必要的驱动可以先不需要加入工程,甚至可以注释掉一些外设的运行,比如看门狗、按键等。不必要的驱动代表去掉也不会影响系统功能性运行。怎么确定是不必要的驱动呢?这可以从系统认知、官网的文档说明、Makefile等去确定。
适配基础组件
在Contiki-NG中,PROCESS的轮询调度器在主循环中运行,不需要定时器的参与。但是很多PROCESS都会用到一个event timer,也就是etimer。一般情况下,一个PROCESS不是在等待事件到来,就是在执行事件。没有事件,就不会有PROCESS的运行。如果PROCESS在没有事件的情况下也需要周期性地执行,该如何做呢?我们可以定义一个etimer,etimer可以通过我们设置的参数定时发出事件,达到定时触发PROCESS运行的效果。
因此,我们首先适配etimer,etimer的底层实现是一个定时器,并且还有很多其他的timer实现与etimer是同一个底层实现。根据其他平台的适配情况,我们选择所以systick作为etimer的底层实现。相关代码如下:
void
SysTick_Handler(void)
{
count++;
if(etimer_pending()) {
etimer_request_poll();
}
if(--second_countdown == 0) {
current_seconds++;
second_countdown = CLOCK_SECOND;
}
}
void
clock_init(void)
{
if(SysTick_Config(SystemCoreClock / 1000U)) {
while(1) {
}
}
NVIC_SetPriority(SysTick_IRQn, 0x00U);
}
无论哪个程序,log的输出对于调试都是很有帮助的。因此,我们第二个适配的组件就是log输出。相关代码如下:
int
dbg_putchar(int c)
{
#if DBG_CONF_SLIP_MUX
static char debug_frame = 0;
if(!debug_frame) {
write_byte(SLIP_END);
write_byte('\r');
debug_frame = 1;
}
#endif
write_byte(c);
if(c == '\n') {
#if DBG_CONF_SLIP_MUX
write_byte(SLIP_END);
debug_frame = 0;
#endif
flush();
}
return c;
}
unsigned int
dbg_send_bytes(const unsigned char *s, unsigned int len)
{
unsigned int i = 0;
while(s && *s != 0) {
if(i >= len) {
break;
}
putchar(*s++);
i++;
}
return i;
}
在串口的适配上,其实有个坑花了较多时间。在Contiki-NG的原本的printf实现上,是直接在源文件中定义了一个printf的具体实现来实现重定向,笔者发现这种方法在Keil中无法实现,Keil会使用C库中的printf实现,并移除重定向的printf实现。(而在gcc平台,是可以直接重定向printf的。)Keil重定向printf有几种实现方式,因此最后直接使用了GD32F310 Demo中的重定向实现。
提供栈底和栈顶地址
Contiki-NG在初始化会有检测栈的操作(目前没了解这一步去掉是否有影响),因此,需要提供栈地址变量_stack和_stack_origin。
在Keil中,提供栈地址变量的方法,笔者认为有2种:
- 在启动文件中分配栈空间的时候加标号,并导出符号;
- 在分散加载文件中将栈的运行地址单独放置,这样Keil就可以导出相应的$$符号变量来代表栈起始和结束位置。
笔者使用第一种方法,相关代码如下:
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
_stack
Stack_Mem SPACE Stack_Size
__initial_sp
_stack_origin
EXPORT _stack
EXPORT _stack_origin
通过编译生成的map文件可查到以下信息:
_stack 0x20000960 Data 0 startup_gd32f3x0.o(STACK)
__initial_sp 0x20000d60 Data 0 startup_gd32f3x0.o(STACK)
_stack_origin 0x20000d60 Data 0 startup_gd32f3x0.o(STACK)
_stack和_stack_origin的地址都是在0x20000000(RAM空间地址),且差值刚好是0x400,因此应该是获取到了正确的地址(有兴趣还可以通过读取内存来确认)。
其它
由于没用到网络协议栈,因此需添加部分宏定义取消协议栈的运行,如下:
ROUTING_CONF_NULLROUTING=1, NETSTACK_CONF_WITH_NULLNET=1, MAC_CONF_WITH_NULLMAC=1
Contiki-NG运行效果
为了验证系统是否已经运行,笔者选择Helloworld例程进行验证,效果如下:
最后,我们再来看下系统的内存和flash占用情况,如下:
==============================================================================
Total RO Size (Code + RO Data) 9236 ( 9.02kB)
Total RW Size (RW Data + ZI Data) 3424 ( 3.34kB)
Total ROM Size (Code + RO Data + RW Data) 9388 ( 9.17kB)
==============================================================================
原作者:大目熊