体验国产RISC-V单片机
WangGaojie Lv4

RISC-V单片机并不是很新鲜的东西,但因为其市场占有率相比于ARM内核的芯片较小,所以此类芯片并不流行。在此之前,我并没有接触过RISC-V的芯片,趁着春节假期,购得一块国产单片机CH32V307,本着学习的态度,看看该芯片相比于ARM内核有何优缺点。

芯片上手

RISC-V芯片的指令集、指令架构同ARM芯片有着非常大的差异,不过前期还不需要接触到这个层面,从最基本的helloworld程序开始可以最快上手这个芯片,芯片厂家提供了HAL层的代码和集成开发环境,还提供了大量的example可供参考,所有让这个芯片运行起来是很容易的。

这个芯片让人最感兴趣的有三个方面,RISC-V内核、内建10MPHY的以太网控制器、内建480MPHY的高速USB控制器。尤其是后两点,几乎所有的STM32的芯片都不支持,这对构建网络相关、USB相关的应用非常有利。

RISC-V内核

针对RISC-V内核的探索,主要是移植FreeRTOS操作系统为主。FreeRTOS官方提供了RISC-V的port层适配,其基于mtimer实现,但是该芯片的内部实现上有些差异(RISC-V生态不统一),其砍掉了mtimer,增加了一个类似ARM单片机的systick,所以芯片厂家自己实现了一个port适配层。

参考该适配层代码完成了操作系统的移植。在移植过程中主要遇到了以下问题:

  1. 需要在链接脚本中分配独立的操作系统栈
    该栈仅用于线程切换的中断使用,FreeRTOS官方的port层方案中没有这方面内容。RISC-V芯片没有独立的中断栈空间,所以芯片厂商为RTOS上下文切换中断特别实现了中断栈切换机制,相对应的栈空间需要提前在链接脚本中分配。这是与ARM单片机区别较大的地方,ARM单片机可自定义线程栈和中断栈,并由芯片自动完成切换。
    这里需要注意链接脚本的写法,操作系统的栈空间和系统开始运行的栈空间其实可以为同一个地址空间,因为操作系统开始运行后,最开始的主栈空间将不再使用。所以这两部分空间是一致的。
    在xPortStartFirstTask实现中,将链接脚本分配的空间地址减小了512字节,这主要是避免和主栈空间发送冲突,但实际上这里应该是不需要的。将中断栈地址将写入到mscratch寄存器中,这是一个临时存储数据的寄存器,这里将专用于保存栈地址,通过寄存器交换指令实现sp寄存器和mscratch的交换,达到栈空间的切换目的,这样用户空间和内核调度中断的栈将相互独立。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    .stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size :
    {
    PROVIDE( _heap_end = . );
    . = ALIGN(4);
    PROVIDE(_susrstack = . );
    . = . + __stack_size;
    PROVIDE( _eusrstack = .);
    __freertos_irq_stack_top = .;
    } >RAM
  2. 芯片运行模式
    该芯片具有两种运行模式,用户模式和机器模式,原则上来说芯片上电启动后芯片运行在机器模式,但是厂家提供的启动代码将芯片调整到了用户模式,在裸机开发时这可能更有利于系统安全。但是在RTOS系统中,系统为了访问CSR寄存器实现对处理器的控制,系统必须允许在特权级。所以这里需要调整启动汇编代码,根据芯片相关手册,调整mstatus寄存器使得芯片始终运行在机器模式。
    这部分内容与ARM单片机也有像类似的地方。ARM单片机也分为特权模式和非特权模式,目前市面上大部分操作系统都运行在特权级,包括用户级的线程。仅少部分操作系统对用户线程实现了权限管理。

  3. Call API from ISR
    为了系统的安全性和健壮性,FreeRTOS要求在中断中使用FromISR结尾的系统API。但是芯片官方的port实现中缺少相关的检测支持,这使得软件开发过程中可能出现一些潜在的风险无法暴露。主要是在vPortEnterCritical()接口的实现中,当第一次进入临界区,需要检测是否在中断中,因为该API仅能够在非中断上下文使用,中断上下文有其它的实现接口。芯片官方和FreeRTOS默认的port层代码如下:

    1
    2
    3
    4
    5
    void vPortEnterCritical( void )
    {
    portDISABLE_INTERRUPTS();
    uxCriticalNesting++;
    }

    可见缺少相关故障断言支持,这里可调整为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    void vPortEnterCritical( void )
    {
    portDISABLE_INTERRUPTS();
    uxCriticalNesting++;
    if( uxCriticalNesting == 1 )
    {
    configASSERT( ( (PFIC->GISR) & 0x1ff ) == 0 );
    }
    }

    通过GISR寄存器实现中断上下文的识别和检测。

  4. 低功耗模式
    无论是FreeRTOS官方代码还是芯片产家的port层代码中都缺少低功耗支持相关的内容,所以这部分需要自己实现,因为芯片是支持低功耗模式的。主要是需要充分理解systick运行机制以及芯片的低功耗进入及唤醒机制并配合操作系统完成低功耗运算的实现。
    使用WFI指令可以让处理器进入低功耗模式,当发生中断时可自动唤醒芯片,但是在FreeRTOS系统中配合systick实现的低功耗接口vPortSuppressTicksAndSleep,在WFI指令前需要关闭中断,所以导致系统无法正常唤醒。这里需要特殊处理一下,通过SCTLR寄存器将WFI指令切换为event模式,即通过事件唤醒系统。再将SEVONPEND标志置位,这样即使关闭全局中断,当系统的任意中断发生时仍然能够立即唤醒芯片。
    在具体实现vPortSuppressTicksAndSleep上,需要注意systick的计数方向,FreeRTOS已经将其改为向上计数,所以这里同一般STM32单片机的计数方向是相反的。先根据系统需要休眠的时间估算systick的计数值,重设systick的CMP寄存器,通过WFI让芯片休眠,芯片唤醒后检查是systick唤醒还是其它中断唤醒,如果是其它中断唤醒则需要根据systick的计数值计算刚刚休眠的时长。将systick的CMP恢复到休眠前的状态,通知RTOS内核刚刚休眠的时长,这样即完成了低功耗port的实现,在具体的实现上还需要注意systick在不同时间段的累计补偿和停机补偿。
    使用低功耗模式会不可避免的使得RTOS时间同实际时间发生一定的偏移,但这个偏移在长时间的累计才会比较明显,一般影响不大。但是如果将RTOS的内核时间作为日历时间使用则会导致一定的误差,这是开启低功耗模式时需要注意的问题。
    在我的开发板上测试,在运行相同软件应用的情况下,未开启低功耗模式运行时的电源为5V/29mA,开启低功耗模式后的电源为5V/19mA,考虑到开发板上存在一些固定功率开销,低功耗模式带来的优势还是非常明显的。

注:开启低功耗模式运行后,网卡驱动的发送部分硬件DMA存在一定的异常问题还未解决。

太网控制器

芯片内建10M的PHY,这意味这该芯片不需要单独的网卡芯片即能够实现网络通信,使用单片芯片方案即能够完成联网应用开发。芯片厂家提供了一个网络协议栈WCHNET,该协议栈能够满足基本的网络应用开发需求,但是面对复杂应用,使用LwIP协议栈可能更加合适。

LwIP依赖前面的操作系统,此外协议栈没有其它的移植要求。为了让协议栈正常运行起来,最关键的是实现网卡驱动。这里参考官方示例基本能够完成驱动开发,官方虽然没有针对LwIP进行适配,但是驱动层的MAC帧都是通用的,所以驱动的移植难度也不大。以太网HAL层的代码结构同STM32单片机类似,用法上没有陌生感。

驱动移植完成后进行了简单的网络测试,使用iperf工具测试,收发带宽可达9.3M,基本上达到10M接口的传输速度上限。

USB控制器

芯片官方提供了较丰富的测试代码,有HS、FS端口相关的代码,还有主机、从机的示例。应用类别也有CDC、网络等。运行了官方示例代码,可以确认USBHS能够很好的工作,但是在移植Tinyusb是遇到了一些问题。

TinyUSB是第三方USB协议栈,其支持CH32V307芯片,该协议栈能够非常方便的进行USB设备描述符的编写,这是我常用的一个USB协议栈库。该协议栈也对CH32V307的HS端口进行了适配,但是在实际运行过程中出现了一些问题。在关闭协议栈调试日志的情况下,内部会出现断言错误,打开日志后,该故障消失。输出日志会影响代码的运行速度,这应该就其内部的差异。导致该问题的原因没有进一步研究,应该是驱动层或协议层代码存在一些竟态问题。所以针对该芯片USB方面的内容就没有进行更加深入的测试。检查TinyUSB官方的issus列表,可以确认针对CH32V307的适配还存在一些问题,待修复后再测试该芯片的USB性能。

故障诊断

该芯片提供了一个独立的故障中断,类似于ARM单片机的HardFault,但是该故障中断囊括了芯片运行的大部分故障,包括指令异常、内存寻址错误、内存非对齐等。特别注意RISC-V单片机不支持非对齐内存访问,在编写C代码时需要注意规避这方面的问题。

当发生故障时,可以通过CSR寄存器列表中的mcause寄存器确定发生故障的原因。更进一步的,通过mepc寄存器还能够提供发生故障时的PC指针。这对于故障的诊断十分有利。

中断控制器

这是芯片相关的内容,并不是RISC-V通用的中断控制器。芯片可支持8级中断优先级、4组优先级分组。该芯片的特色是支持硬件压栈,即硬件上有独立的内存空间用于中断发生时通过硬件自动保持caller saved寄存器,这意味其相比于一般的单片机中断,它的响应过程可以减少许多额外的指令开销。硬件压栈最高支持3级嵌套,可以满足中断高实时性的要求。

此外,芯片还支持4路免表中断,即不通过中断向量表,将中断函数入口地址直接绑定到指定的中断上,这将进一步提高中断的响应速度。

需要注意的是,区别于ARM单片机中断有独立的栈,该芯片发生中断时并不会自动切换栈空间,这意味着中断栈空间将是线程栈空间的延续。在调试网卡中断时我没有意识到这一点,导致中断发生时将IDLE线程的栈空间溢出。如果不能手动将栈切换到独立空间,那么需要控制中断函数的栈空间消耗。

其它内容

芯片官方开发的HAL库在代码风格上接近STM32早期的标准库,甚至部分API名称都一致,这也是方便入手该芯片的优势。

在以上开发测试过程中编译器一直使用-Os优化,代码运行过程中没有出现由于编译优化导致的问题。

芯片的最高主频为144MHz,代码模板采用默认的96MHz。96MHz主频下,基于FreeRTOS系统运行Coremark基准测试,跑分可达250分(满频率运行应当为370分),性能同ARM的CM4内核水平相当。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2K performance run parameters for coremark.
CoreMark Size : 666
Total ticks : 15839
Total time (secs): 15.839000
Iterations/Sec : 252.541196
Iterations : 4000
Compiler version : GCC8.2.0
Compiler flags : -Ofast
Memory location : FreeRTOS-heap4
seedcrc : 0xe9f5
[0]crclist : 0xe714
[0]crcmatrix : 0x1fd7
[0]crcstate : 0x8e3a
[0]crcfinal : 0x65c5
Correct operation validated. See README.md for run and reporting rules.
CoreMark 1.0 : 252.541196 / GCC8.2.0 -Ofast / FreeRTOS-heap4

此外还测试了SPI、TIM等外设,性能尚可。该芯片还具有DVP、FSMC等外设,可进一步拓展该芯片的用途。

总的来说,第一次使用RISC-V芯片,该芯片给我留下了较好的影响。我认为该国产芯片的功能已经达到了目前主流单片机的行列,但其流行程度、相关的开发生态环境、社区活跃度都还需要进一步发展。将其应用到项目开发过程中需要考虑面临故障时缺少经验的问题,国产单片机的发展任重而道远。