12/25/2018

Arduino移植第三方硬件 - 以CCORE为例

Arduino是一个非常容易上手的开发环境,那怎么能用Arduino开发自己喜欢的芯片呢?

准备

Arduino需要什么?

(第一步 - 基础)

  • 一个可以编译C与C++的编译器
  • 一个可以在芯片运行时重置芯片并下载程序的方法/软件

(第二步 - 软件)

  • 实现Arduino的库函数,比如pinMode(), digitalWrite(), Serial类
  • 实现对程序入口和中断函数的标记和跳转(当使用bootloader时)

在所有步骤开始之前,必须确认第一部分所说的编译器和下载器是存在的,否则就不可能完成移植(比如不可能把Arduino移植到51单片机上)

另外就是熟读官方的教程 https://github.com/arduino/Arduino/wiki/Arduino-IDE-1.5-3rd-party-Hardware-specification

编译器

编译器的设置对应于platform.txt文件。只要把对应的编译器填到相应的设置里就好

不同文件编译命令的设置

# AVR compile patterns
# --------------------

## Compile c files
recipe.c.o.pattern="{compiler.path}{compiler.c.cmd}" {compiler.c.flags} {compiler.c.extra_flags} {build.extra_flags} {includes} "{source_file}" -o "{object_file}"

## Compile c++ files
recipe.cpp.o.pattern="{compiler.path}{compiler.cpp.cmd}" {compiler.cpp.flags} {compiler.cpp.extra_flags} {build.extra_flags} {includes} "{source_file}" -o "{object_file}"

## Compile S files
recipe.S.o.pattern="{compiler.path}{compiler.S.cmd}" {compiler.S.flags} {compiler.S.extra_flags} {build.extra_flags} {includes} "{source_file}" -o "{object_file}"

命令中使用的path/cmd/flag等的设置如下:

# Default "compiler.path" is correct, change only if you want to override the initial value
complier.toolchain.path={runtime.platform.path}/toolchain/ccore-elf
compiler.path={complier.toolchain.path}/bin/
compiler.c.cmd=mcore-elf-gcc
compiler.c.flags= "-I{runtime.platform.path}/cores/ccore/" "-I{runtime.platform.path}/cores/ccore/port" "-I{runtime.platform.path}/cores/ccore/ccore" -O3 -fno-common -ffunction-sections -mlittle-endian -Wall -c
compiler.c.elf.flags=-EL --gc-sections "-L{complier.toolchain.path}/lib" "-L{complier.toolchain.path}/mcore-elf/lib/c0_div" "-L{complier.toolchain.path}/lib/gcc/mcore-elf/4.6.0/c0_div" -e __start -N -t -warn-common "-T{complier.toolchain.path}/linkmap"
compiler.c.elf.cmd=mcore-elf-ld
compiler.S.cmd=mcore-elf-as
compiler.S.flags=-g -gstabs -EL 
compiler.cpp.cmd=mcore-elf-g++
compiler.cpp.flags=-mlittle-endian -O3 -ffunction-sections -Wno-write-strings -Wall -c -fmessage-length=0 "-I{runtime.platform.path}/cores/ccore/" "-I{runtime.platform.path}/cores/ccore/port" "-I{runtime.platform.path}/cores/ccore/ccore"
compiler.ar.cmd=mcore-elf-ar
compiler.ar.flags=rcs
compiler.objcopy.cmd=mcore-elf-objcopy
compiler.objcopy.eep.flags=-O ihex -j .eeprom --set-section-flags=.eeprom=alloc,load --no-change-warnings --change-section-lma .eeprom=0
compiler.elf2hex.flags=-O ihex -R .eeprom
compiler.elf2hex.cmd=mcore-elf-objcopy
compiler.ldflags=
compiler.size.cmd=mcore-elf-size

静态库打包

recipe.ar.pattern="{compiler.path}{compiler.ar.cmd}" {compiler.ar.flags} {compiler.ar.extra_flags} "{archive_file_path}" "{object_file}"

生成elf

recipe.c.combine.pattern="{compiler.path}{compiler.c.elf.cmd}" {compiler.c.elf.flags} {compiler.c.elf.extra_flags} -o "{build.path}/{build.project_name}.elf" {object_files} "{build.path}/{archive_file}" "{build.path}/core/ccore/vector_table.c.o" "{build.path}/core/ccore/vector_rce.c.o" "-L{build.path}" --start-group -lgcc -lc -lg -lBootSwitch -lm -lcmb -lsim -lnosys -lrsa -lsm2 -lalg -lget_serial_sn_lib -lsd --end-group

最后需要设置输出二进制文件的方式。默认支持hex 或eep方式输出二进制文件,我们设置hex

recipe.objcopy.hex.pattern="{compiler.path}{compiler.objcopy.cmd}" -O binary "{build.path}/{build.project_name}.elf" "{build.path}/{build.project_name}.bin"

下载器

如果想要降低开发难度的话,使用现有的jtag/swd编程器是最好的。这样的话不用自己编写bootloader,也不用处理程序引导和中断跳转的问题。

但是Arduino的精神不就是让所有人都能用得上么,jtag下载器一点都不Arduino。

所以我们需要串口下载的方式,加编写bootloader,整个系统流程大概如下:

      Bootloader(0x0, _start位置)
          |
          V
      初始化串口(如果使用串口下载)
      初始化USB CDC (如果使用USB虚拟串口)
      初始化systick (如果需要精确倒计时)
          |
          V         (有信息)
      等待串口信息  ---------- >  接收/校验/烧录数据
 (无信息)  |                            |
          V                            V
    正常启动,程序跳转         读取程序的中断向量表,写入系统中断向量表

另一方面,众所周知,只要Arduino通电,不管是不是在Bootloader中,都可以在IDE里点上传完成烧录。

这就需要解决芯片正常运行时通过串口控制Reset,并进入bootloader。

解决方法其实很简单,串口中除了Vcc GND Tx Rx 外,还有DTS CTS信号,其中DTS信号,在串口不传输数据时为高电平,传输数据时为低电平,因此只要在传输数据前(下降沿)Reset芯片即可。最简单的办法就是串联一个电容到Reset接口。在下降沿电压会迅速降低引发芯片关闭,接着由于Reset接口的充电,电容的电平会缓慢升高,解除Reset状态,使芯片重启。电容的容量是需要实际测试的。过小的电容会使得充电速度太快,Reset信号太短以至于芯片不会重启。过大的电容充电速度太慢,降低下载时的启动速度。

Arduino中使用的是100nF的电容,但是在CCORE中,100nF无法使芯片重启,因此换成1μF的,成功解决。

库函数

这个其实没什么好说的,把常用的库函数自己写一遍就好了,但是需要注意的是使用GPIO时可能出现端口复用的问题。在调用pinMode配置时应该判断端口的类型,并设置为GPIO模式。

void pinMode(uint8_t pin, uint8_t mode){
    switch(pin&0xf0){
    case PIN_EPORT_MASK:
        pin=pin&7;
        if (mode&1)    EPORT_EPDDR |= (1 << pin);
        else         EPORT_EPDDR &= ~(1 << pin);
        break;
  case xxxx.....
.......

还有串口的问题,需要实现HardwareSerial类,并使用extern HardwareSerial Serial来定义Serial

void HardwareSerial::begin(unsigned long br){
  SCI_REG *sci = (SCI_REG *)0x70040000;
  int baud_rate;
  sci->BRDF = (((ipsclk * 8 / br) + 1) / 2) & 0x003f;
  baud_rate = (ipsclk * 4 / br) >> 6;
  sci->BDHL.BDH = (UINT8)((baud_rate >> 8) & 0x00ff);
  sci->BDHL.BDL = (UINT8)(baud_rate & 0x00ff);
  sci->CR1 = 0x00;
  sci->CR2 = 0x00;
}
......

Bootloader实现

这是最重要最复杂的一部分。主要完成的目的有两个:下载和运行。

在编写之前应该对芯片二进制文件的map有大概的了解,一般来说,0x0位置存储指向程序入口的指针(_start),之后的一部分位置则存储中断的指针。

需要注意的是,为了保证芯片上电时能先进入bootloader,因此芯片上0x0位置应该指向bootloader而不是实际的程序,应该由bootloader跳转程序入口

但中断需要快速的响应,不能由bootloader代理中断,因此在烧录程序时应把程序的中断向量部分写入实际芯片的中断向量部分。

下载/验证/烧录部分

Bootloader启动后,开启倒计时并监听串口信息。如果接到烧录的信息,则先接收数据,接收完成后,计算hash,并和上位机发来的hash进行对比,如果正确,读取程序的中断向量,写入系统中断向量中。

首先清除flash:(注意只能按块清除)

    for(i = ds; i <= ds+dl; i += 512){
        ret_code = eflash_page_erase(i);
        if(ret_code == EFLASH_PROG_ERASE_FAIL){
            PMSG("Flash erase error!\n\r");
            DMSG("f2");
            return 2;
        }
    }

接收/烧录数据

    //receive data
    for(i=0;i<dl/4;i++){
        u32 data=0;u8 a;
        a=sci_receive_byte();
        data|=a;
        a=sci_receive_byte();
        data|=a<<8;
        a=sci_receive_byte();
        data|=a<<16;
        a=sci_receive_byte();
        data|=a<<24;
        bin[i]=data;
    }
    //program at once
    ret_code = eflash_bulk_program(ds,dl/4,bin);
    if(ret_code == EFLASH_PROG_ERASE_FAIL){
        PMSG("Flash write error!\n\r");
        DMSG("f3");
        return 3;
    }

计算hash,并和上位机发来的对比

    //calculate SHA256
    u8* sha256=(u8*)0x00830000;
    hash_init(MODE_SHA256);
    sha_update(ds,dl);
    hash_dofinal(MODE_SHA256,sha256);
    u8 shaans[65];
    for(i=0;i<32;i++){
        u8 hi=sha256[i]>>4;
        u8 lo=sha256[i]&0xf;
        if(hi<10)shaans[i*2]=hi+'0';
        else shaans[i*2]=hi+'A'-10;
        if(lo<10)shaans[i*2+1]=lo+'0';
        else shaans[i*2+1]=lo+'A'-10;
    }
  ...对比...

运行部分

因为程序存储位置的起始点存放了实际程序入口(_start)的地址,因此读取这个位置并跳转

在汇编中简单的一句ljmp,在C语言里就得用到函数指针,尺有所短寸有所长,哈哈哈

最后记得写一个while(1)防止程序跑飞

    u32 start_point=eflash_word_read(0x00420000);
    void (*app_start)(void);
    app_start = (void (*) (void))(UINT32*)start_point;
    app_start();
    while(1);

Arduino 引导部分

众所周知,arduino里没有main函数,而是初始化用的setup()和一直循环的loop()。但实际上肯定是有__startmain()的,这部分也需要我们来编写。

有可能遇到的问题是某些芯片的库(比如万恶的CCORE)只支持C语言,而Arduino需要cpp的main文件,如果一个个extern "C"有点繁琐,可以考虑写一个c文件初始化系统,并在cpp里调用。

一个典型的main.cpp如下:


#include <Arduino.h>

int main(void)
{

    init();

    initVariant();

    setup();

    for (;;) {
        loop();
    }

    return 0;
}

其中setup()和loop()是用户使用时编写的,我们不需要写这个函数,对系统的初始化要全部写在init()和initVariant()中

另外还需要解决的是链接器并没有定义main函数,必须自己把__start跳转到main上:

asm(".text\n"
    ".export __start\n"
    ".align 4\n"
    ".global __start\n"
    ".type __start,@function\n"
    "__start:\n"
    "jsri main\n");

题外话

编译器是怎么实现把程序入口地址、中断向量地址写入二进制文件的开头的呢?很简单,编写一个常量数组,里面元素为实际的指针即可

void *const VecTable[]={
    (void *) __start,
    (void *) ISR01,
    (void *) ISR02,
    (void *) ISR03,
    (void *) ISR04,
    (void *) ISR05,
    (void *) ISR06,
    (void *) ISR07,
    (void *) ISR08,
    (void *) ISR09,
    (void *) ISR0a,
  ......

接下来只需要在map里把VecTable放在二进制文件开头就行了