• 以优化 MaixPy 的启动速度为例,说说 K210 的双核使用及原子操作。


    本篇文章,也是在总结自己使用 K210 芯片的过程中留下的一些痕迹,如果觉得有帮助,不清楚的地方都可以直接留言告诉我。

    文章大纲

    这次我编写了一个大纲说明,方便查阅的朋友得知本篇主题

    • 优化 MaixPy 的 SD 卡挂载和启动问题。
    • 示范一下双核的使用。
    • 双核中需要的原子操作。

    MaixPy 进入系统很慢,慢在哪里了?

    现在 MaixPy 项目也进入到了稳定阶段,它迟早会成为一个优秀的 MCU 项目参考,至少我们可以从中学会很多观念,运用很多复杂代码。

    但还有很多优化空间,如一直存在的启动速度过慢的问题,我们理一下 MaixPy 的启动过程。

    • 调用 maixpy_main 经过标准的 K210 BSP 启动过程,设置相应的芯片初始化,再配置 PLL RTC FLASH CORE 等硬件资源。

    MaixPy/components/micropython/port/src/maixpy_main.c#L646-L703

    • 再来进入到 mp_task 导入 MicroPython 的环境代码,选择执行的核心,并设置 Gc HeapSize 区域,它就类似于一个死循环,维持 MicroPython 的执行,在进入 REPL 之前,要完成 内存、外设、系统 的初始化。

    MaixPy/components/micropython/port/src/maixpy_main.c#L488-L578

    • 才到执行内置的 _boot.py 和 main.py 等内置代码。

    经过了调试确认 SD 卡的 spi 驱动初始化占用了至少 3s 的循环时间,主要影响的地方在我上次修改 SD 的读取超时导致的时间过长,但这个问题不是因为靠改变等待时间长度来解决的。

    此前 MaixPy 的 SD 卡启动存在问题,会在运行一段时间后出现不稳定情况,从而在 Python 层面丢失 SD 卡的内容,后来量测数据确认为 K210 与 SPI 之间出现了死锁,就主机和从机都在等对方给应答,理论上只要保证 SD 卡每次的读写失败都会退回上一层 SPI 初始化就可以从根本上解决这个问题,但这并不在本文中继续讨论。

    这是在这之前的代码,我们可以看到如下逻辑:

    • sdcard_is_present 是为了确认 spi 与 sdcard 能够通信成功。
    • init_sdcard_fs 是为了进一步初始化 SD 插入 MicroPython 环境中。

    这里介绍一下关于 MicroPython 的 VFS 注入 SD 卡操作,实时上可以用这样的代码去完成 os 模块的盘符加载的:

    import esp
    
    class FlashBdev:
    
        SEC_SIZE = 4096
        RESERVED_SECS = 1
        START_SEC = esp.flash_user_start() // SEC_SIZE + RESERVED_SECS
        NUM_BLK = 0x6b - RESERVED_SECS
    
        def __init__(self, blocks=NUM_BLK):
            self.blocks = blocks
    
        def readblocks(self, n, buf):
            #print("readblocks(%s, %x(%d))" % (n, id(buf), len(buf)))
            esp.flash_read((n + self.START_SEC) * self.SEC_SIZE, buf)
    
        def writeblocks(self, n, buf):
            #print("writeblocks(%s, %x(%d))" % (n, id(buf), len(buf)))
            #assert len(buf) <= self.SEC_SIZE, len(buf)
            esp.flash_erase(n + self.START_SEC)
            esp.flash_write((n + self.START_SEC) * self.SEC_SIZE, buf)
    
        def ioctl(self, op, arg):
            #print("ioctl(%d, %r)" % (op, arg))
            if op == 4:  # BP_IOCTL_SEC_COUNT
                return self.blocks
            if op == 5:  # BP_IOCTL_SEC_SIZE
                return self.SEC_SIZE
    
    bdev = FlashBdev((size - 20480) // FlashBdev.SEC_SIZE - FlashBdev.START_SEC)
    
    

    我们只需要能够构建这个 vfs 对象并提供 readblocks 和 writeblocks 给到 os 去执行该对象所映射的接口就可以添加到 MicroPython 中的 vfs 中,所以理解了这一层,我们就知道 Python 是如何连接到具体的 SD 卡读写操作的,关于这个的代码,我也留个标记 MaixPy/components/micropython/port/src/standard_lib/machine/machine_sdcard.c#L84-L102

    那么我们回到主题上,排除了上层代码的问题,将目光定位到 SD 卡驱动上,我们不难发现 spi 的驱动方式只存在于单线程流程,也就是 K210 先使用 SPI 对目标的通信和等待后再做后续操作。

    从整体上来看问题,单独拿出来,这个再正常不过了,从大局来看,如果没有 sd 的卡的芯片岂不是也要等待?

    因为从该逻辑只考虑软件,而不考虑硬件。如果从硬件上考虑问题,则需要硬件上做一个引脚识别或协议判断,从而不超时等待来确认 SD 卡是否存在,但目前来看,只能软件默默的等待应答。

    而不使用 RTOS 的情况下,也就没有办法让出当前的时间片给其他线程的话,我们还能怎么做?

    可以多靠硬件的资源来解决问题,一方面可以透过定时器中断,另一方面则可以使用双核,而不需要总是想要通过软件的逻辑来解决问题,有时候结合硬件可以轻松解决问题。

    所以我最后选择了使用多核,主要的修改记录如下。

    MaixPy/commit/450f20b9956d88107109499a7eabfaf24021f4ed

    最终的优化结果为复位芯片后 250ms 进入 repl 接口,这就保证了 MaixPy IDE 连接可以在 2s 内完成,在这之前需要等待 10 秒才能完成连接,而使用双核就是为了不想在底层代码引入 RTOS 的代码依赖。

    双核要如何使用?

    K210 的双核使用相关的 API 示范在 bsp 的 kendryte-standalone-sdk/lib/bsp/entry_user.ckendryte-standalone-sdk/lib/bsp/include/entry.h

    我们看一下的双核接口调用的方法,且不说初始化,我们可以看如下代码:

    
    typedef int (*dual_func_t)(int);
    corelock_t lock;
    volatile dual_func_t dual_func = 0;
    void *arg_list[16];
    
    void core1_task(void *arg)
    {
        while (1)
        {
            if (dual_func)
            { //corelock_lock(&lock);
                (*dual_func)(1);
                dual_func = 0;
                //corelock_unlock(&lock);
            }
    
            //usleep(1);
        }
    }
    int core1_function(void *ctx)
    {
        // vTaskStartScheduler();
        core1_task(NULL);
        return 0;
    }
    
    

    使用的方法很简单,只需要这样一行。

    dual_func = sd_preload; // int sd_preload(int core)
    

    当然,这样看起来很草率,如果封装一下会更好看。

    不过这样修改运行后 MaixPy 就变成白屏了,这是为什么呢?

    使用双核出现了问题?

    通常这是因为双核的使用上在使用同一个资源的时候,出现了冲突,如 MaixPy 启动后就会使用 lcd.display 进行 lcd_draw_picture 绘图操作,那么它做了什么呢?

    这是由于 lcd 在 K210 上的绘图前需要对缓冲区进行翻转的,因此可以在这做一层简单的两路重复操作的优化。

        g_pixs_draw_pic_half_size = g_pixs_draw_pic_size/2;
        g_pixs_draw_pic_half_size = (g_pixs_draw_pic_half_size%2) ? (g_pixs_draw_pic_half_size+1) : g_pixs_draw_pic_half_size;
        g_pixs_draw_pic = p+g_pixs_draw_pic_half_size;
    
        dual_func = swap_pixs_half; // 注册函数
    
        for(i=0; i< g_pixs_draw_pic_half_size; i+=2)
        {
            #if LCD_SWAP_COLOR_BYTES
                g_lcd_display_buff[i] = SWAP_16(*(p+1));
                g_lcd_display_buff[i+1] = SWAP_16(*(p));
            #else
                g_lcd_display_buff[i] = *(p+1);
                g_lcd_display_buff[i+1] = *p;
            #endif
            p+=2;
        }
    
        while(dual_func){} // 等待注册的函数执行完成
    

    这也就是为什么启动后会白屏,因为它上电后双核挂载 SD 卡的期间进行双核加速的绘图就锁死了,那么怎么办呢?有两个方法分别是 信号量 和 临界区 的方式去让资源互斥,虽然最终我选择了信号量的方式,但也会提及临界区的原子操作进行说明。

    加入一个 maixpy_sdcard_loading 的变量,可装饰为 volatile 变量(但我没有这样使用它),设置 volatile 保证了永远都是读取变量的实时值,也就避开变量缓存的情况,此时绘图函数只需要保证在 SD 卡挂载释放双核操作才可以使用双核加速,问题得到解决。

            if (maixpy_sdcard_loading) {
                for(i=0; i< g_pixs_draw_pic_size; i+=2)
                {
                    #if LCD_SWAP_COLOR_BYTES
                        g_lcd_display_buff[i] = SWAP_16(*(p+1));
                        g_lcd_display_buff[i+1] = SWAP_16(*(p));
                    #else
                        g_lcd_display_buff[i] = *(p+1);
                        g_lcd_display_buff[i+1] = *p;
                    #endif
                    p+=2;
                }
            } else {
    

    但这样做是不优雅,且有些乱来的操作,如果不是 LCD 调用双核呢?其他的模块难道也要等它?

    所以想要真正解决这个问题,最好就是封装在资源的访问操作之间,进行临界区的加锁形成资源互斥的形式,也就是所谓的线程安全函数,最好保证函数均可分时复用退出,否则和单核无异,不然就是新一代 MTK 8核围观传说 XD 。

    原子操作要如何使用?

    双核中需要的原子操作,有类似于临界区的 lock 和 unlock 的实现,具体的实现可以继续往深处查看,但本质不变,我们可以从 BSP 中获取这个定义。

    我们在这个头文件 kendryte-standalone-sdk/lib/bsp/include/atomic.h 中获取它的使用方法如下。

    
    #include "atomic.h"
    
    spinlock_t lock = SPINLOCK_INIT;
    
    int set_call_back() {
          spinlock_lock(&lock);
    
          // your operator
    
          spinlock_unlock(&lock);
    }
    
    

    实际上这是很基础的东西,在 BSP 这里提供了自旋锁 spinlock 的实现,并加入了 CAS 的 try_lock 的方式,形成如下原子操作的雏形。

    • 试图获取锁,如果失败则退出,确保了双核在并行执行时调取互斥资源的时候不会冲突。
          spinlock_lock(&lock);
          // your operator
          spinlock_unlock(&lock);
    
    • 试图设置变量到目标值,如果变量持续不满足则失败,类似 CAS 操作。
        int var = 0;
        atomic_add(&(var), 1);
    

    实际上使用起来并不困难,取决于自己的场景需求,如果想要在 MicroPython 层面上开放该函数接口,恐怕还需要考虑,尤其是不熟悉的开发者调用,系统随时可能崩溃。

    后记

    在没有 RTOS 的时候,也并非没有多线程的方法,如使用一些定时器中断、外部触发中断等接口,或者像双核这类硬件提供的资源,也是可以达到目的。

    不过代码并不会因为使用多线程而提高性能,想要用好多线程,应该要在思维上一定要保持状态机多路并行的方式,用异步并行的方式来思考问题,如在硬件中存在多和物理世界中不断循环的执行单元,而你要做的就是等待这个单元的状态发生改变再进行下一段操作,所以代码里的设计思维可以如下:

    
    def loop():
    
          if state si 0:
                pass
          if state is 1:
                pass
    

    保持住状态机的思维方式,让代码都可以分时执行,那么在接入 RTOS 的时候代码架构上也不会发生任何改变,只需要注意分配到各个函数在整个芯片中执行周期的比例即可。

    这次的内容很基础,所以就到这里吧。

    junhuanchen 2020年10月5日

    这次为了不引入 RTOS 环境,借助双核操作跳过了 SD 卡的阻塞运行,使得 MaixPy 进入 MPY 的 REPL 快了从而执行了对 SD 卡的访问,导致了 SD 卡的读写操作冲突,由于硬件挂载设备的通信需要时间,所以一旦出现 SPI SDCard 不稳定,那么上层就得不到 SD 卡的路径进行访问。
    在发现 SPI SDCard 工作不稳定的情况下,通过打开日志的 printf 后可以克服,这就说明是时序问题,做嵌入式软件需要对硬件的特性十分敏感,硬件电路不一定会输出我们想要的结果,所以我们必须能够量测到这样的现象,但实在量测不到怎么办?我们可以通过逻辑上的思考来想象代码的执行过程,这个内容想在讲解 ESP8285 实现软串口的芯片内部接收实现的时候,如何才能有效的结合硬件特性,让代码在不确定的环境中符合预期的工作。

  • 相关阅读:
    ant build 报 warning modified in the future
    JQUERY选择器大全(转载)
    MAVEN实践经验
    Jquery ajax参数设置(转)
    解决WIN7下pl/sql连接弹出空白提示框问题
    getContextPath、getServletPath、getRequestURI的区别
    HTTP协议
    构建接口层快速稳定的质量保证体系
    接口测试流程
    接口测试的意义
  • 原文地址:https://www.cnblogs.com/juwan/p/13767971.html
Copyright © 2020-2023  润新知