10

大目熊 · 2022年04月10日

【GD32F310开发板试用】Contiki-NG在GD32F310的移植

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();

  /* Setup a periodic timer that expires after 10 seconds. */
  etimer_set(&timer, CLOCK_SECOND * 100);

  while(1) {
    printf("Hello, world\n");

    /* Wait for the periodic timer to expire and then restart the timer. */
    PROCESS_WAIT_EVENT_UNTIL(etimer_expired(&timer));
    etimer_reset(&timer);
  }

  PROCESS_END();
}

看了以上代码,如果你不了解Contiki-NG,是否会觉得这是一个实时系统?笔者稍微解释一下这段代码的意思:

  1. PROCESS和AUTOSTART_PROCESSES这两个宏定义了一个helloworld任务;
  2. PROCESS_THREAD宏函数的执行内容即是任务的执行内容;
  3. 在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移植是在https://github.com/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)
{
    /* setup systick timer for 1000Hz interrupts */
    if(SysTick_Config(SystemCoreClock / 1000U)) {
        /* capture error */
        while(1) {
        }
    }
    /* configure the systick handler priority */
    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种:

  1. 在启动文件中分配栈空间的时候加标号,并导出符号;
  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例程进行验证,效果如下: contiki运行效果.png

最后,我们再来看下系统的内存和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)

==============================================================================

对本项目有兴趣的伙伴可以通过百度网盘下载相关源码:
链接:https://pan.baidu.com/s/1-e85...
提取码:1234~~~~

推荐阅读
关注数
10694
内容数
187
中国高性能通用微控制器领域的领跑者兆易创新GD系列芯片技术专栏。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息