> 文档中心 > 【GD32F310开发板试用】Contiki-NG在GD32F310的移植

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


首发极术社区。如对兆易创新GD32F310 MCU感兴趣,欢迎添加微信 aijishu2020 加入GD32技术讨论群。

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/contiki-ng.git 下载的源码中添加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的底层实现。相关代码如下:

voidSysTick_Handler(void){  count++;  if(etimer_pending()) {    etimer_request_poll();  }  if(--second_countdown == 0) {    current_seconds++;    second_countdown = CLOCK_SECOND;  }}voidclock_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输出。相关代码如下:

intdbg_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 intdbg_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_stackStack_MemSPACE   Stack_Size__initial_sp_stack_originEXPORT  _stackEXPORT  _stack_origin

通过编译生成的map文件可查到以下信息:

    _stack0x20000960   Data    0  startup_gd32f3x0.o(STACK)    __initial_sp 0x20000d60   Data    0  startup_gd32f3x0.o(STACK)    _stack_origin0x20000d60   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例程进行验证,效果如下: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2PRmQFcn-1649927840336)(/img/bVbumU)]

最后,我们再来看下系统的内存和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-e85NmI56b8Ama7a3IX6XA?pwd=1234
提取码:1234~~~~

15路电子城