大家好我是惊觉。是的,失踪人口回来了。最近参加了rt-thread的国产MCU移植活动,移植rt-thread到华大的HC32L196。rtt论坛中已有许多介绍移植到各种平台的文章,详细讲述移植步骤,在rtt论坛搜索“国产MCU移植”即可阅读。本文不介绍具体移植步骤,而是如往常一样,分享移植的原理与方法。

移植原理

移植一款软件,无非是获取源码,修改其中与硬件相关的代码以适配目标硬件。移植rt-thread也是如此,首要任务是要明确要修改哪部分内容。带着这个问题,我们来分析rt-thread的源码结构。

rt-thread源码结构

rt-thread源码根目录结构如下:

目录 说明
bsp 板级支持包。存放各种硬件平台的驱动代码,初始化代码,工程文件。
components 组件。如finsh控制台,抽象层驱动,文件系统,网络系统。
examples 示例程序
include 内核以及libc的头文件
libcpu 与CPU架构相关的接口,为操作系统调度提供支持。
src 内核代码,如线程、定时器、线程间通信(互斥锁,信号量)。

移植所涉及的目录有两个:bsplibcpu,相应的移植分为BSP移植CPU架构移植。其他的目录与具体的CPU无关,无须改动。

CPU架构移植

在嵌入式领域有多种不同 CPU 架构,例如 Cortex-M、ARM920T、MIPS32、RISC-V 等等。为了使 RT-Thread 能够在不同 CPU 架构的芯片上运行,RT-Thread 提供了一个 libcpu 抽象层来适配不同的 CPU 架构。向下提供了一套统一的 CPU 架构移植接口,这部分接口包含了全局中断开关函数、线程上下文切换函数、时钟节拍的配置和中断函数、Cache 等等内容。下表是 CPU 架构移植需要实现的接口和变量。

函数和变量 描述
rt_base_t rt_hw_interrupt_disable(void); 关闭全局中断
void rt_hw_interrupt_enable(rt_base_t level); 打开全局中断
rt_uint8_t *rt_hw_stack_init(void *tentry, void *parameter, rt_uint8_t *stack_addr, void *texit); 线程栈的初始化,内核在线程创建和线程初始化里面会调用这个函数
void rt_hw_context_switch_to(rt_uint32 to); 没有来源线程的上下文切换,在调度器启动第一个线程的时候调用,以及在 signal 里面会调用
void rt_hw_context_switch(rt_uint32 from, rt_uint32 to); 从 from 线程切换到 to 线程,用于线程和线程之间的切换
void rt_hw_context_switch_interrupt(rt_uint32 from, rt_uint32 to); 从 from 线程切换到 to 线程,用于中断里面进行切换的时候使用
rt_uint32_t rt_thread_switch_interrupt_flag; 表示需要在中断里进行切换的标志
rt_uint32_t rt_interrupt_from_thread, rt_interrupt_to_thread; 在线程进行上下文切换时候,用来保存 from 和 to 线程

是不是看起来挺复杂的,其实rtt已经支持了非常多的CPU架构。下图的libcpu目录中已支持多种CPU架构。让我们看看对arm系列的支持情况,从低端的cortex-m0到高端的cortex-m7,甚至还有cortex-a和cortex-r系列的。大家熟知的stm32f103为cortex-m3内核,stm32f407为cortex-m4内核。如果要移植到的目录芯片内核出现在此目录之中,那就无需关注libcpu,只要在配置文件中指定正确的内核即可。

我移植的HC32L196使用cortex-m0+内核,可使用cortex-m0的代码,因此无须进行CPU构架移植。

bsp结构

由于不需要进行CPU架构移植,所以本次移植相对简单,唯一的工作就是在rt-thread的bsp目录中创建自己硬件的bsp。

rt-thread当前支持了100多个bsp,可能大家用的最多的是stm32。不过我并不建议大家在移植时参考stm32,因为它是最复杂的一个bsp。

早期rt-thread中关于stm32的bsp比较简单,各种型号如stm32f10x, stm32f40x都是独立的bsp。新手入门相对简单。不过弊病也很明显:随着支持的stm32系列的增加,bsp的子目录也就急剧增加,维护成本很高。

可能得益于stm32的HAL库,可以相对较低的投入将它们合为一个bsp。它们共用一份驱动代码,其在HAL_Drivers中。

可能以后国产MCU的bsp也会发展成这样,不过对于移植新手,最好是先易后难。我移植的HC32L196是华大单片机,以已经被rtt支持的hc32f4a0为模板进行移植。同时参考了swm320,以及stm32\stm32l053-st-nucleo。

大多数bsp目录结构:

目录 说明
applications 用户代码。纯净的bsp中只需要一个main.c文件,里面定义main函数。
board 板级驱动代码(最主要的是board.c),链接脚本(gcc, keil, iar)。
drivers 设备驱动代码,比如gpio和uart驱动。
figures 电路板照片。
Libraries 芯片厂商驱动库。
.config, rtconfig.h, Kconfig Kconfig配置系统相关文件
rtconfig.py, SConscript, SConstruct scons构建系统相关文件
template.uvprojx, template.uvoptx keil模板工程
project.uvprojx, project.uvoptx keil工程
template.eww, template.ewp iar模板工程
project.eww, project.ewp iar工程

可分为如下几类:

  1. 代码文件:applications, board, drivers, Libraries中的.h和.c
  2. Kconfig配置系统相关文件
  3. scons构建系统相关文件
  4. 工程模板

代码结构

先来看看我移植后的keil工程,其打开的几个目录就是涉及移植的代码目录。

applications目录最为简单。drivers目录是移植的重点,不过它不是移植的首要任务。下面几节介绍移植前最迫切需要搞清楚的内容。

Kconfig

rtt支持通过menuconfig命令来配置内核、组件及软件包。执行menuconfig命令时,其从Kconfig文件中解析菜单结构,由用户勾选、配置各个选项,最终将配置结果写入.config和rtconfig.h。

bsp中通常有两个Kconfig文件。一个位于根目录,另一个位于board。

根目录中的Kconfig仅仅是导入了别的目录的Kconfig,所有bsp的基本都一样,无须修改。

mainmenu "RT-Thread Project Configuration"config BSP_DIRstringoption env="BSP_ROOT"default "."config RTT_DIRstringoption env="RTT_ROOT"default "../.."config PKGS_DIRstringoption env="PKGS_ROOT"default "packages"source "$RTT_DIR/Kconfig"
source "$PKGS_DIR/Kconfig"
source "board/Kconfig"

board/Kconfig

menu "Hardware Drivers Config"config MCU_HC32L196bool select ARCH_ARM_CORTEX_M0select RT_USING_COMPONENTS_INITselect RT_USING_USER_MAINdefault ymenu "Onboard Peripheral Drivers"endmenumenu "On-chip Peripheral Drivers"config BSP_USING_GPIObool "Enable GPIO"select RT_USING_PINdefault ymenuconfig BSP_USING_UARTbool "Enable UART"default yselect RT_USING_SERIALif BSP_USING_UARTconfig BSP_USING_UART0bool "Enable UART0"default yconfig BSP_USING_UART1bool "Enable UART1"default nendifendmenumenu "Board extended module Drivers"endmenuendmenu

其自动选择了几个必选的配置,比如RT_USING_USER_MAIN。另,定义了可配置的驱动选项,比如GPIO配置和串口配置。

上述文件对应的串口配置菜单如下:

rtt官方文档中有对Kconfig进行详细讲解:https://www.rt-thread.org/document/site/#/development-tools/kconfig/kconfig

scons和工程模板文件

rt-thread使用scons作为构建系统,其用于编译源码,生成固件。不过呢,大家用的最多的,可能是用它生成keil工程,就是在使用menuconfig配置内核、组件和驱动之后,使用如下命令生成keil工程:

scons --target=mdk5

其原理,以生成keil5工程为例,是scons根据rtconfig.h文件中的配置,在template.uvprojx上添加宏定义、头文件路径配置、文件链接,从而生成project.uvprojx。下图左侧为模板工程,右侧为生成的rtt工程。

再多说一句,rtt是如何能够读写keil工程文件呢?.uvprojx其实是xml文件,rtt通过模板工程创建新工程,就是在读写xml,有兴趣的话,可以阅读rt-thread源码根目录下的tools/keil.py。

rtconfig.h是在Kconfig系统中生成,只要修改好Kconfig相关文件后,无须操心rtconfig.h。要修改的是模板工程。不过也很简单,从其他bsp复制模板工程,修改设备类型,RAM和ROM配置就可以了。其他的配置,如下载接口等,可根据需要修改。

稍复杂些的任务是修改下面三种文件:

  • SConstruct
  • rtconfig.py
  • SConscript

这三个文件都是python脚本,只不过它们里面调用了许多scons系统提供的函数。所以,如果熟悉python的话,修改起来会很轻松。

SConstruct

SConstruct是scons的入口脚本,其通过rtconfig.py以导入各种编译配置,之后调用PrepareBuilding以获取编译对象(要编译哪些文件)。PrepareBuilding会调用各SConscript脚本以获取编译对象。这文件一般不用修改,除非参考的bsp有瑕疵。

rtconfig.py

rtconfig.py中定义了各种与编译相关的选项和参数。

头部定义CPU架构与型号,还记得文首提到的架构移植吗?对于rtt已支持的CPU架构,只需要在这里指明即可,scons系统会根据这里的配置选择相应的架构代码以进行编译链接。

ARCH='arm'
CPU='cortex-m0'

其他主要的是编译参数,比如armcc编译系列如下。

elif PLATFORM == 'armcc':# toolchainsCC = 'armcc'CXX = 'armcc'AS = 'armasm'AR = 'armar'LINK = 'armlink'TARGET_EXT = 'axf'DEVICE = ' --cpu Cortex-M0 'CFLAGS = '-c ' + DEVICE + ' --apcs=interwork --c99'AFLAGS = DEVICE + ' --apcs=interwork 'LFLAGS = DEVICE + ' --scatter "board\linker_scripts\link.sct" --info sizes --info totals --info unused --info veneers --list rt-thread.map --strict'CFLAGS += ' -I' + EXEC_PATH + '/ARM/ARMCC/include'LFLAGS += ' --libpath=' + EXEC_PATH + '/ARM/ARMCC/lib'CFLAGS += ' -D__MICROLIB 'AFLAGS += ' --pd "__MICROLIB SETA 1" 'LFLAGS += ' --library_type=microlib 'EXEC_PATH += '/ARM/ARMCC/bin/'if BUILD == 'debug':CFLAGS += ' -g -O0'AFLAGS += ' -g'else:CFLAGS += ' -O2'CXXFLAGS = CFLAGS POST_ACTION = 'fromelf --bin $TARGET --output rtthread.bin \nfromelf -z $TARGET'

这块与生成keil工程无关,而是在命令行下编译源码并生成固件。可能大家平时不会这么编译,都是用Keil。其实这种方法意义重大,其是持续集成的基础。

适配起来很也简单,直接把相同CPU的配置复制过来即可。我移植HC32L196,虽然主要参考HC32F4A0,然而HC32F4A0的构架是cortex-m4,显然不适合。所以在适配rtconfig.py时,我从stm32\stm32l053-st-nucleo获取cortex-m0的配置。

SConscript

SConscript存在于各源码目录下,用于决定编译哪些文件。这些要编译的文件也会在创建keil工程中时被包含进去。

bsp根目录下的SConscript用于扫描出子目录中的SConscript并调用之,一般不用修改。

子目录下的SConscript大致分为两种:

  1. 将指定文件包含到编译目标之中,或者使用Glob(’*.c’)包含所有的C文件。
  2. 根据rtconfig.h中的配置来包含被选中的文件。

application中的为第1种,drivers为第2种。修改时依葫芦画瓢即可。

更多细节,可参阅:https://www.rt-thread.org/document/site/#/development-tools/scons/scons

board

终于进入代码讲解环节。board目录中通常会有一个board.c。

void rt_hw_board_clock_init(void)
{}void  SysTick_Configuration(void)
{}void SysTick_Handler(void)
{/* enter interrupt */rt_interrupt_enter();rt_tick_increase();/* leave interrupt */rt_interrupt_leave();
}void rt_hw_board_init()
{/* Configure the System clock */rt_hw_board_clock_init();/* Configure the SysTick */SysTick_Configuration();#ifdef RT_USING_HEAPrt_system_heap_init((void *)HEAP_BEGIN, (void *)HEAP_END);
#endif#ifdef RT_USING_COMPONENTS_INITrt_components_board_init();
#endif#ifdef RT_USING_CONSOLErt_console_set_device(RT_CONSOLE_DEVICE_NAME);
#endif}

其做了如下事情:

  • 初始化时钟
  • 配置SysTick定时器
  • 初始化rtt堆内存模块
  • 初始化板级驱动,如gpio和uart
  • 设计控制台串口

所有bsp的board.c都差不多,上面代码中rt_hw_board_clock_init和SysTick_Configuration空着,这就是移植时需要修改的代码。其他部分,一般不用修改。

另,配置堆内存时用到的宏定义在board.h之中,需要根据硬件做修改。

#define SRAM_BASE 0x20000000
#define SRAM_SIZE 0x8000
#define SRAM_END (SRAM_BASE + SRAM_SIZE)

board\linker_scripts目录中存放链接脚本,需要修改其中有关RAM和ROM的配置。

  • link.sct:keil链接脚本
  • link.lds:gcc链接脚本
  • 没做iar支持,因为我不用iar,也没装iar:)

Libraries

Libraries存放芯片厂商提供的驱动代码。我移植的HC32L196基本结构如下:

  • HC32L196_StdPeriph_Driver:分inc和src,存放芯片驱动,如hc32l196_adc.h和hc32l196_adc.c。
  • CMSIS\Include:存放CMSIS相关头文件,如core_cm0.h
  • CMSIS\Device\HDSC\HC32L196\Include:杂类驱动头文件。
  • CMSIS\Device\HDSC\HC32L196\Source:杂类驱动源文件,比如system_hc32l19x.c,其内包含汇编启动文件会调用的SystemInit函数。
  • CMSIS\Device\HDSC\HC32L196\Source\ARM:keil汇编启动文件startup_hc32l19x.s
  • CMSIS\Device\HDSC\HC32L196\Source\GCC:gcc汇编启动文件startup_hc32l19x.s
  • CMSIS\Device\HDSC\HC32L196\Source\IAR:iar汇编启动文件startup_hc32l19x.s
  • SConscript:包含本目录中的代码文件。在包含汇编启动文件时,根据rtconfig.CROSS_TOOL来包含相应编译平台的文件。

上述这些文件,除了SConscript,都来自芯片厂商的SDK,只不过其文件分布可能与上述不同。视具体情况做调整即可。

汇编启动文件

关于Libraries中的汇编启动文件,需要补充说明一点。对于keil版本,一般无须修改。对于gcc版本,需要把跳转main函数的语句修改为跳转entry函数。

stm32的启动文件,其调用的是main函数。

需要改为:

bl entry

之所以有如此差异,是因为armcc(keil编译器)与gcc的机制不同。

armcc

armcc的汇编启动文件相对简单,职责如下:

  • 定义堆空间和栈空间,初始化栈指针
  • 定义中断向量表
  • 定义入口函数Reset_Handler,其先调用SystemInit,之后调用__main

初始化全局变量等工作放在了__main之中,__main完成初始化操作后会调用main函数。不过呢,armcc提供了一种函数补丁机制。如果定义了$Sub$$main函数的话,在main函数调用之前,会先调用$Sub$$main。rt-thread就是通过定义$Sub$$main函数,在其中进行操作系统的初始化,之后调用applications中的main函数以执行用户代码。

gcc

gcc汇编启动文件职责如下:

  • 定义中断向量表
  • 定义入口函数Reset_Handler,其负责初始化全局变量(data和bss),调用SystemInit,调用main函数

由于gcc没有armcc那样的函数补丁机制,所以要运行rt-thread的话,需要将调用main函数改为调用rt-thread入口函数,即entry。

rt-thread根据编译平台定义了不同的入口函数,armcc对应$Sub$$main,gcc对应entry

#ifdef __ARMCC_VERSION
extern int $Super$$main(void);
/* re-define main function */
int $Sub$$main(void)
{rtthread_startup();return 0;
}
#elif defined(__ICCARM__)
extern int main(void);
/* __low_level_init will auto called by IAR cstartup */
extern void __iar_data_init3(void);
int __low_level_init(void)
{// call IAR table copy function.__iar_data_init3();rtthread_startup();return 0;
}
#elif defined(__GNUC__)
/* Add -eentry to arm-none-eabi-gcc argument */
int entry(void)
{rtthread_startup();return 0;
}
#endif

移植到HC32L196

如前所说,我不打算详细讲解每一步操作,仅提一些要点。

移植步骤

可分为两步:

  • 创建可以运行的bsp,这是最关键的一步。
  • 填充rtt设备驱动,如gpio和uart,这是相对费时的一步。

之所以分为两大步,是因为先完成关键的一步,运行成功,将给予移植者一个很大的激励,提高信心。如果第一步失败了,也好及时查找问题,而不是等经历了漫长的设备驱动移植后,在测试时发现rt-thread系统都还无法跑起来。

创建可以运行的bsp

所谓可以运行,是指可以让rt-thread操作系统在芯片上跑起来,并不需要跑finsh控制台,甚至不需要点亮LED灯,不需要任何外设驱动,能运行如下代码就行。

main.c

int main(void)
{for (uint32_t i = 0; ; i++){rt_thread_delay(RT_TICK_PER_SECOND);};
}

当然啦,没有任何外设驱动的话,只能在调试模式下运行才能观察效果。只要rt_thread_delay的功能正常,就说明rt-thread调度系统正常工作了。

在了解了移植原理后,创建可以运行的bsp应该能较快完成,具体步骤如下:

  1. 复制一个bsp,将名称改为自己的平台。
  2. 使用芯片原厂提供的SDK替换Libraries目录中的内容。对其中的汇编启动文件和链接脚本要稍加关注,尤其是gcc汇编。
  3. 修改board目录源码,主要是board.c,完成初始化时钟和SysTick的工作。
  4. 删除drivers中的文件,或者保留几个驱动文件的框架,删除硬件相关代码。
  5. 修改模板工程。
  6. 修改Kconfig相关文件。
  7. 修改Scons相关文件。
  8. 使用menuconfig更新rtconfig.h文件。
  9. 使用scons生成rt-thread工程。
    10.编译烧录调度。

RT-Thread Studio

创建可以运行的bsp之后,之后就是开发驱动程序了。此时其实是可以使用RT-Thread Studio开发的,其有一个非常好用的功能:导入Keil或者IAR项目到工作空间中。

我在之后的驱动开发环节一直使用RT-Thread Studio编写代码。

gpio映射表

struct rt_pin_ops
{void (*pin_mode)(struct rt_device *device, rt_base_t pin, rt_base_t mode);void (*pin_write)(struct rt_device *device, rt_base_t pin, rt_base_t value);int (*pin_read)(struct rt_device *device, rt_base_t pin);rt_err_t (*pin_attach_irq)(struct rt_device *device, rt_int32_t pin,rt_uint32_t mode, void (*hdr)(void *args), void *args);rt_err_t (*pin_detach_irq)(struct rt_device *device, rt_int32_t pin);rt_err_t (*pin_irq_enable)(struct rt_device *device, rt_base_t pin, rt_uint32_t enabled);rt_base_t (*pin_get)(const char *name);
};

rt-thread gpio设备驱动接口使用引脚号(pin)来操作指定的引脚。早期的bsp会定义一个大数组来存储引脚列表,下图是swm320定义的列表。

这种方式比较繁琐。通常芯片的GPIO口有一定的规律,比如PA0-PA15,PB0-PB15,PC0-PC15,等等。这些GPIO对应的寄存器的地址是连续的,可以通过一个公式将寄存器地址转换为引脚序号,反之亦然。

因此出现了使用GET_PIN宏来计算指定GPIO引脚序号的方法,比如GET_PIN(A, 5)会计算出PA5引脚的序号。可能stm32 bsp最先使用这种方法,大家移植的时候可以参考一下。

支持gcc

支持编译

HC32L196的原厂SDK中并不支持gcc。不过笔者是eclipse系列IDE的忠实用户,既然原厂不支持,那我就自己支持吧。

首先,要创建汇编启动文件:Libraries\CMSIS\Device\HDSC\HC32L196\Source\GCC\startup_hc32l19x.s。

怎么创建呢,当然不需要从零开始啦。从其他cortex-m0的bsp中复制一个来修改。比如stm32的,

bsp\stm32\libraries\STM32L0xx_HAL\CMSIS\Device\ST\STM32L0xx\Source\Templates\gcc\startup_stm32l053xx.s

修改中断向量表和中断函数即可。

另外要关注下rtconfig.py和board\linker_scripts\link.lds,同样可以参考cortex-m0的bsp。

支持烧录

添加了对gcc的支持后,使用RT-Thread Studio创建开发板支持包,就可以真正使用RT-Thread Studio来开发项目了。不过在这之前,需要先编译出固件并烧录验证。

使用scons命令编译。

编译后的固件位于bsp根目录:

可使用J-Flash烧录:

成功运行:

大家在使用J-Flash创建工程时,可能发现找不到自己的硬件配置。如下图,HC32系列只有我移植的HC32L196,而没有HC32F4A0等,这是我自己添加进去的。怎么添加呢,可参考我之前的一篇文章:https://mp.weixin.qq.com/s/ZPre7XZIKwIoS-s_8x_VTQ 。

后记

本次移植过程相当漫长,不是因为移植任务本身艰难,而是我只能用碎片化的时间进行移植。有几天我九点半准备下班回家移植,老板觉得走的太早,硬是拖到十点,这些天就没有早于十一点到家的。大家说这样的老板是不是很可恶?表示赞同的,支持下笔者,点个赞呗:)。

【国产MCU移植】移植RT-Thread到国产芯片HC32L196相关推荐

  1. 正点原子delay函数移植到rt thread操作系统(HAL库)

    正点原子教程中涉及到的操作系统只涉及了UCOS的教程,其中例程的system文件夹中的delay.c函数只是适配了UCOS. 下面将delay.c函数移植到rt thread中,使用的bsp是rt t ...

  2. 【国产MCU移植】移植RT-Thread到国产芯片FM33LC026

    本文由RT-Thread论坛用户@jiao96 原创发布:https://club.rt-thread.org/ask/article/3020.html 摘要 因为项目需要,使用了复旦微FM33LC ...

  3. stm32移植到国产MCU雅特力AT32

    第一章 STM32移植AT32 概述 前言 雅特力科技于2016年在重庆成立的国产MCU品牌,全系列产品采用55nm先进工艺主打M4内核的高性能32位单片机,目前正式发布的有AT32F403.AT32 ...

  4. RT Thread Free Modbus移植问题整理

    RT Thread Free Modbus移植问题整理 问题描述: 在读写寄存器中,写数据正常,只能读1个寄存器的值,多个值会异常. 在移植过程中发现串口(或RS485)数据接收长度异常. 一.环境描 ...

  5. STM32的国产替代,盘点下我知道的国产MCU

    电子元件涨价和缺货是多少嵌入式工程师的痛,一年内上游厂家晶圆产能告急能有数十次之多.而MCU更是重灾区,且不说国内有超75%的市场都是被国外产品占据,就是本国内的代理和供应商也是漫天要价,而交期更是长 ...

  6. 文末赠书《GD32 MCU原理及固件库开发指南》5本 | 国产MCU中GD32系列有望成为未来32位MCU的主流

    学习优秀博文([guo产MCU移植]手把手教你使用RT-Thread制作GD32系列BSP)有感 一篇优秀的博文是什么样的?它有什么规律可循吗?优秀的guo产32位单片机处理器是否真的能成功替换掉st ...

  7. MCU不再“芯慌慌”,国产新品Air101帮你忙

    "全球缺芯,最缺是MCU!"这两年在疫情.政治因素等复杂背景交织下,MCU价格呈几十倍甚至几百倍上涨之势,如同翻着筋斗云:纵然舍得大价钱,漫漫交期也让人心慌慌.更别提开发切换之难, ...

  8. 客户回访|国产MCU测试解决方案 助力中国“芯”智造

    半导体技术持续更新迭代,MCU也在与时俱进,为了更好地迎接市场未来趋势,国产MCU厂商积极布局各系列MCU产品线,开始逐渐在特定细分领域实现突破.随着应用场景的进化升级,MCU 中包含越来越多的功能模 ...

  9. 灵动微MM32SPIN360C获选2020年度国产MCU评选

    由芯师爷主办.深福保集团冠名的"2020硬核中国芯"活动中,灵动微电子获选2020年度国产MCU评选. 企业介绍 灵动微电子是中国本土领先的通用32位MCU产品及解决方案供应商.公 ...

最新文章

  1. 张一鸣的微博世界-产品篇一
  2. java 线程 操作系统线程_线程基础:线程(1)——操作系统和线程原理
  3. 反馈速度小于 200ms!“弹窗”功能让你极速触达用户内心
  4. 【tomcat】servlet原理及其生命周期
  5. 视频显示边缘空白的真相
  6. python测验2_测验2: Python基础语法(上) (第4周)
  7. 版权所有LIKEWING_柳我借地存个图学习一下
  8. 你是“细”精你就赢了 游戏开发者怎样做好玩家细分
  9. mac降级safari_如何在Mac上的Safari中将网页另存为PDF
  10. java字符常量_字符常量 java
  11. 2022-2028全球与中国健康资讯交换(HIE)市场现状及未来发展趋势
  12. web网页端 微信 登录 内嵌 二维码 方法
  13. 如何在AD19的PCB库编辑界面修改尺寸单位
  14. 不怕加班狗有情绪,就怕加班狗有“武器”
  15. 67、INGeo:利用占用网格先验加速/减少迭代次数
  16. ArcGIS 平均最近邻分析、多距离空间聚类分析与密度空间制图
  17. SAP ABAP——SMARTFORMS(一)【SF概要及文本编辑器】
  18. rails应用无法读取kafka数据报错Kafka::Error: Failed to find group coordinator
  19. 【实验五 一维数组】7-2 sdut-C语言实验-整数位
  20. topwin耳塞使用方法_如何使Windows PC使用“单声道”音频(因此您可以戴一副耳塞)...

热门文章

  1. AAAI 2020:北大开源算法姿态辅助多摄像机协作以进行主动对象跟踪
  2. addEventlistener()方法,事件监听
  3. android开发 java.lang.IllegalStateException at android.media.MediaPlayer._prepare(Native Method)
  4. 30岁后再转行程序员,能行吗?
  5. R Studio 安装stringi 报错download of package ‘stringi’ failed
  6. 我的iphone6退货之路
  7. 共阴数码管段码-共阳数码管段码
  8. seate底层原理_Seate
  9. awk 字符串转时间戳
  10. 使用pandas处理excel,并使用Openpyxl修改单元格格式