当前位置: 首页 > news >正文

基于esp-idf的arm2d移植

什么是ARM2D

Arm在Github上发布了一个专门针对“全体” Cortex-M处理器的2D图形加速库——Arm-2D
我们可以简单的把这个2D图形加速库理解为是一个专门针对Cortex-M处理器的标准“显卡驱动”。虽然这里的“显卡驱动”只是一个夸张的说法——似乎没有哪个Cortex-M处理器“配得上”所谓的显卡,但其实也并没有差多远——因为根据最新的趋势,随着单片机资源的逐步丰富(较高级的工艺节点正在逐步降价),处理器不仅跑得越来越快、存储器越来越大,而且大量的厂商已经或者正在考虑给Cortex-M处理器配备专属的2D图形加速引擎

以上摘自公众号裸机思维的文章

首先,arm2d是一个2d引擎库,他是纯软件的东西。很多人可能会被它的arm2d名字给误导。分不清arm2d和DMA2D。实际上DMA2D是硬件,arm2d则是一个软件。arm2d的优秀性能,让我瞠目结舌。在裸机思维的文章中,你不难看到诸如M0+内核、25M主频的主控的平台上跑出各种逆天的效果, 这也是它吸引我的原因。虽然arm开发的初衷是服务于自家的硬件,但是不意味着它不能够移植到别的平台。

以下是我摸索并熟悉arm2d的移植过程

移植前的准备

首先,我们是基于esp-idf 5.0的sdk做的移植。那么,第一需要的肯定是安装环境。这里参考官方手册
就不多赘述。

接下来就应该准备一份驱屏的基础代码了。我们准备了一块esp32s3的开发板,其中屏幕使用了st7789的240X240的spi屏幕。

屏幕驱动

我喜欢以esp-iot-solution中的bus和screen为基础写屏幕驱动。bus中提供了诸如:spi i2c 8080 rgb的通讯层封装,而screen则基于bus的封装提供了st7789 ili9341等lcd芯片封装。
这使得驱屏变得异常简单

cp -r esp-idf/example/get-started/sample_project ./
cd sample_project 

新建components文件夹,再复制刚才提到的bus和screen组件放到components文件夹下。接下来开始着手写屏幕驱动代码:

/*** @file arm_math.h* @author cangyu (sky.kirto@qq.com)* @brief * @version 0.1* @date 2024-06-06* * @copyright Copyright (c) 2024, CorAL. All rights reserved.* */#include <stdio.h>
#include <stdlib.h>#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_log.h"#include "screen_driver.h"
#include "esp_log.h"
/* ==================== [Defines] =========================================== */#define BOARD_IO_SPI2_MISO          -1
#define BOARD_IO_SPI2_MOSI          11
#define BOARD_IO_SPI2_SCK           12
#define BOARD_LCD_SPI_CS_PIN        10
#define BOARD_LCD_SPI_DC_PIN        9
#define BOARD_LCD_SPI_RESET_PIN     -1
#define BOARD_LCD_SPI_BL_PIN        46
#define BOARD_LCD_SPI_CLOCK_FREQ    40000000/* ==================== [Typedefs] ========================================== *//* ==================== [Static Prototypes] ================================= */static void screen_clear(scr_driver_t *lcd, int color);/* ==================== [Static Variables] ================================== */static const char *TAG = "screen example";
static scr_driver_t g_lcd;/* ==================== [Macros] ============================================ *//* ==================== [Global Functions] ================================== */void app_main(void)
{spi_config_t bus_conf = {.miso_io_num = BOARD_IO_SPI2_MISO,.mosi_io_num = BOARD_IO_SPI2_MOSI,.sclk_io_num = BOARD_IO_SPI2_SCK,.max_transfer_sz = 1024*10};spi_bus_handle_t spi2_bus_handle = spi_bus_create(SPI2_HOST, &bus_conf);scr_interface_spi_config_t spi_lcd_cfg = {.spi_bus = spi2_bus_handle,.pin_num_cs = BOARD_LCD_SPI_CS_PIN,.pin_num_dc = BOARD_LCD_SPI_DC_PIN,.clk_freq = BOARD_LCD_SPI_CLOCK_FREQ,.swap_data = true,};scr_interface_driver_t *iface_drv;scr_interface_create(SCREEN_IFACE_SPI, &spi_lcd_cfg, &iface_drv);scr_find_driver(SCREEN_CONTROLLER_ST7789, &g_lcd);scr_controller_config_t lcd_cfg = {.interface_drv = iface_drv,.pin_num_rst = BOARD_LCD_SPI_RESET_PIN,.pin_num_bckl = BOARD_LCD_SPI_BL_PIN,.rst_active_level = 0,.bckl_active_level = 1,.offset_hor = 0,.offset_ver = 0,.width = 240,.height = 240,.rotate = SCR_DIR_LRTB,};g_lcd.init(&lcd_cfg);scr_info_t lcd_info;g_lcd.get_info(&lcd_info);ESP_LOGI(TAG, "Screen name:%s | width:%d | height:%d", lcd_info.name,lcd_info.width, lcd_info.height);screen_clear(&g_lcd, COLOR_GREEN);}/* ==================== [Static Functions] ================================== */static void screen_clear(scr_driver_t *lcd, int color)
{scr_info_t lcd_info;lcd->get_info(&lcd_info);uint16_t *buffer = malloc(lcd_info.width * sizeof(uint16_t));for (size_t i = 0; i < lcd_info.width; i++) {buffer[i] = color;}for (int y = 0; y < lcd_info.height; y++) {lcd->draw_bitmap(0, y, lcd_info.width, 1, buffer);}free(buffer);
}

接下来就是编译烧录的事情了

idf.py set-target esp32s3 # 切换芯片
idf.py build 		# 编译代码
idf.py flash 		# 烧录
idf.py monitor      # 显示串口log

这是运行的效果

驱屏成果
如此,我们便得到了一个干净的驱屏的工程。

ARM2D的组件加入

我们在components文件夹下面创建一个arm2d的组件文件夹。再在arm2d文件夹里clone arm2d的仓库

cd  components 		# 进入组件文件夹
mkdir arm2d 		# 创建arm2d组件件夹
cd arm2d			# 进入arm2d组件文件夹
git clone https://github.com/ARM-software/Arm-2D.git # clone arm2d仓库

arm2d的仓库里面很多文件夹,很多文件。我们首要的就是要弄清楚哪些是我们需要的。我们需要的文件主要分布在Library中和Helper中。其中Library是核心部分,而Helper则是后续添加的有帮助的部分。我们在arm2d里面创建一个CMakeLists.txt用于添加编译

touch CMakeLists.txt

CMakeLists.txt内容如下:

idf_component_register(SRC_DIRS "Arm-2D/Library/Source" "Arm-2D/Helper/Source" INCLUDE_DIRS "Arm-2D/Library/Include" "Arm-2D/Helper/Include")

接下来就是退出到工程根目录开始启动上述的编译。
不出所料发生了报错, 找不到arm_2d_cfg.h。那我们在arm2d的文件夹下面添加它
通过搜索这个文件名,我们发现在components/arm2d/Arm-2D/Library/Include/template路径下是有一个同名的config文件。我们把内容复制粘贴过来。大致看一遍配置,值得注意的是,GLCD_CFG_SCEEN_WIDTHGLCD_CFG_SCEEN_HEIGHT 是屏幕的宽和高,别忘记改成我们的屏幕大小:240*240
修改后,别忘记CMakeLists.txt也要修改:

idf_component_register(SRC_DIRS "Arm-2D/Library/Source" "Arm-2D/Helper/Source" INCLUDE_DIRS "." "Arm-2D/Library/Include" "Arm-2D/Helper/Include")

再次进行编译,果然没那么简单。这里告诉我们缺少arm_2d_user_arch_port.h文件。哎,之前的tamplate文件夹里好像有。直接复制过来。再次进行编译。

然后发现缺少arm_math.h文件。这个文件比较棘手,是arm的dsp库。arm2d为了加速图形计算,使用了很多arm的dsp库来加速。我们的esp32s3不是arm架构的根本没法使用。这下只能自己写一个arm_math.h文件,将arm2d内部依赖arm-dsp库的内容提取出来, 并简单的替代。这个过程比较费时费力,需要从报错中找到源头,然后从arm2d中理解。使用math.h进行替换。这里我直接放出我最终的arm_math.h:

/*** @file arm_math.h* @author cangyu (sky.kirto@qq.com)* @brief * @version 0.1* @date 2024-06-06* * @copyright Copyright (c) 2024, CorAL. All rights reserved.* */#ifndef __ARM_MATH_H__
#define __ARM_MATH_H__/* ==================== [Includes] ========================================== */
#include <math.h>#ifdef __cplusplus
extern "C" {
#endif/* ==================== [Defines] =========================================== *//* ==================== [Typedefs] ========================================== */typedef int16_t q15_t;
typedef int32_t q31_t;
typedef int64_t q63_t;/* ==================== [Global Prototypes] ================================= */__STATIC_FORCEINLINE q31_t clip_q63_to_q31(q63_t x)
{return ((q31_t) (x >> 32) != ((q31_t) x >> 31)) ?((0x7FFFFFFF ^ ((q31_t) (x >> 63)))) : (q31_t) x;
}__STATIC_FORCEINLINE float arm_sin_f32(float x)
{return sin(x);
}__STATIC_FORCEINLINE float arm_cos_f32(float x)
{return cos(x);
}__STATIC_FORCEINLINE q31_t arm_sin_q31(q31_t x)
{return (q31_t)sin((float)x);
}__STATIC_FORCEINLINE q31_t arm_cos_q31(q31_t x)
{return (q31_t)cosl((float)x);
}__STATIC_FORCEINLINE uint32_t usat(int32_t val, uint8_t sat) {uint32_t max = (1U << sat) - 1; // 最大值为 2^sat - 1if (val < 0) {return 0;} else if (val > max) {return max;} else {return (uint32_t)val;}
}__STATIC_FORCEINLINE int32_t saturate_to_int32(int64_t value) {if (value > INT32_MAX) {return INT32_MAX;} else if (value < INT32_MIN) {return INT32_MIN;} else {return (int32_t)value;}
}__STATIC_FORCEINLINE int32_t qadd_impl(int32_t x, int32_t y) {int64_t result = (int64_t)x + y; // 将x和y相加return saturate_to_int32(result); // 对结果进行饱和处理
}/* ==================== [Macros] ============================================ */// 计算一个32位整数从最高有效位
#define __CLZ(x) __builtin_clz(x)// 确保一个数值在给定的位宽内   
#define __USAT(val, sat) usat(val, sat)// 它将两个32位有符号整数相加,并在结果超出32位有符号整数范围时进行饱和处理
#define __QADD(x, y) qadd_impl(x, y)#ifdef __cplusplus
} /* extern "C" */
#endif#endif // __ARM_MATH_H__

再度编译发现虽然编译过了,但是很多地方有warning,看着十分难受。这里去请教了arm2d的作者,傻孩子大佬。大致了解了原因后按照他的说法在CMakeLists.txt中加入了两行编译器命令

idf_component_register(SRC_DIRS "Arm-2D/Library/Source" "Arm-2D/Helper/Source" INCLUDE_DIRS "." "Arm-2D/Library/Include" "Arm-2D/Helper/Include")target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-implicit-fallthrough -Wno-unused-variable)
target_compile_options(${COMPONENT_LIB} PRIVATE -fms-extensions)

这样的话编译就没有警告了,nice!

对接arm2d

arm2d的对接十分曲折,由于arm2d的源代码拥有若干个宏堆砌而成。很难读懂,我也是参考了components/arm2d/Arm-2D/examples/[template][pc][vscode]/platform路径下的arm_2d_disp_adapter_0.h和arm_2d_disp_adapter_0.c拉过来放到arm2d文件夹下。
接了这两个文件的代码后,由于引入了.c和一些esp32的代码,其中arm_2d_disp_adapter_0.c的代码还借用了components/arm2d/Arm-2D/examples/common里面的代码。那么CMakeLists.txt自然也要修改如下:

idf_component_register(SRC_DIRS "." "Arm-2D/Library/Source" "Arm-2D/Helper/Source" "Arm-2D/examples/common/controls" "Arm-2D/examples/common/asset"INCLUDE_DIRS "." "Arm-2D/Library/Include" "Arm-2D/Helper/Include" "Arm-2D/examples/common/controls" "Arm-2D/examples/common/asset")target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-implicit-fallthrough -Wno-unused-variable)
target_compile_options(${COMPONENT_LIB} PRIVATE -fms-extensions)

然而编译后,发现arm_2d_disp_adapter_0.c 和 arm_2d_disp_adapter_0.h里面的代码都是灰色,原来是缺少RTE_Acceleration_Arm_2D_Helper_Disp_Adapter0宏,搜索arm2d文件发现在 components/arm2d/Arm-2D/examples/[template][pc][vscode]/platform/RTE_Components.h 中有定义,那么拉取到arm2d的文件夹内后,还需要
修改CMakeLists.txt添加一个编译宏 _RTE_ 就可以了

idf_component_register(SRC_DIRS "." "Library/Source" "Helper/Source" "common/controls" "common/asset" INCLUDE_DIRS "." "Library/Include" "Helper/Include" "common/controls" "common/asset" )target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-implicit-fallthrough -Wno-unused-variable)
target_compile_options(${COMPONENT_LIB} PRIVATE -fms-extensions)
target_compile_definitions(${COMPONENT_LIB} PRIVATE "_RTE_" )

这部分的代码真的很难理解,作者十分擅长用宏。在这样的基础之下,写下的代码如同自带一层混淆,让人难以读懂和移植。
然后我们编译后发现缺少 :

void Disp0_DrawBitmap(uint32_t x, uint32_t y, uint32_t width, uint32_t height, const uint8_t *bitmap)

int64_t arm_2d_helper_get_system_timestamp(void)

uint32_t arm_2d_helper_get_reference_clock_frequency(void)

这部分是对接esp32底层,我们留在main里面做

主函数调用arm2d

到了主函数了,首先我们要引入头文件

// arm2d的内容
#include "arm_2d.h"
#include "arm_2d_disp_adapter_0.h"// 对接需要的内容
#include "esp_timer.h"

然后我们要对接Disp0_DrawBitmap函数,这部分是给arm2d底层刷新屏幕使用

void Disp0_DrawBitmap(uint32_t x, uint32_t y, uint32_t width, uint32_t height, const uint8_t *bitmap)
{g_lcd.draw_bitmap(x, y, width, height, (uint16_t*)bitmap);
}

对接arm_2d_helper_get_system_timestamp函数,这部分给arm2d提供时间戳:

int64_t arm_2d_helper_get_system_timestamp(void)
{return esp_timer_get_time();
}

对接arm_2d_helper_get_reference_clock_frequency函数,这部分是时间戳频率:

uint32_t arm_2d_helper_get_reference_clock_frequency(void)
{return 1000000;
}

然后,主函数下面加入代码:

 arm_irq_safe {arm_2d_init();}disp_adapter0_init(Disp0_DrawBitmap);while (1){disp_adapter0_task();vTaskDelay(1);}

开始编译,结果最后的链接阶段报错:

A fatal error occurred: Segment loaded at 0x3c030390 lands in same 64KB flash mapping as segment loaded at 0x3c030020. Can't generate binary. Suggest changing linker script or ELF to merge sections.
ninja: build stopped: subcommand failed.

通过翻译软件我们知道,这里的段错误,好像是冲突了。
我们通过指令xtensa-esp32-elf-objdump -h build/lcd_tjpgd.elf查找了所有的段:

$ xtensa-esp32-elf-objdump -h build/lcd_tjpgd.elf build/lcd_tjpgd.elf:     file format elf32-xtensa-leSections:
Idx Name          Size      VMA       LMA       File off  Algn0 .rtc.text     00000010  600fe000  600fe000  00054000  2**0ALLOC1 .rtc.force_fast 00000000  600fe010  600fe010  0005374f  2**0CONTENTS2 .rtc_noinit   00000000  50000000  50000000  0005374f  2**0CONTENTS3 .rtc.force_slow 00000000  50000000  50000000  0005374f  2**0CONTENTS4 .rtc_reserved 00000018  600fffe8  600fffe8  00053fe8  2**3ALLOC5 .iram0.vectors 00000403  40374000  40374000  0001d000  2**2CONTENTS, ALLOC, LOAD, READONLY, CODE6 .iram0.text   0000edbb  40374404  40374404  0001d404  2**2CONTENTS, ALLOC, LOAD, READONLY, CODE7 .dram0.dummy  0000b200  3fc88000  3fc88000  0000f000  2**0ALLOC8 .dram0.data   0000255c  3fc93200  3fc93200  0001a200  2**4CONTENTS, ALLOC, LOAD, DATA9 .noinit       00000000  3fc9575c  3fc9575c  0005374f  2**0CONTENTS10 .dram0.bss    00004040  3fc95760  3fc95760  0001c75c  2**3ALLOC11 .flash.text   0002672f  42000020  42000020  0002d020  2**2CONTENTS, ALLOC, LOAD, READONLY, CODE12 .flash_rodata_dummy 00030000  3c000020  3c000020  00001020  2**0ALLOC13 .flash.appdesc 00000100  3c030020  3c030020  00001020  2**4CONTENTS, ALLOC, LOAD, READONLY, DATA14 arm2d.tile.c_tileWhiteDotMask 00000010  3c030120  3c030120  00001120  2**2CONTENTS, ALLOC, LOAD, READONLY, DATA15 arm2d.tile.c_tileWhiteDotRGB565 00000010  3c030130  3c030130  00001130  2**2CONTENTS, ALLOC, LOAD, READONLY, DATA16 arm2d.asset.c_bmpWhiteDotRGB565 00000188  3c030140  3c030140  00001140  2**2CONTENTS, ALLOC, LOAD, READONLY, DATA17 arm2d.asset.c_bmpWhiteDotAlpha 000000c4  3c0302c8  3c0302c8  000012c8  2**2CONTENTS, ALLOC, LOAD, READONLY, DATA18 .flash.rodata 0000d01c  3c030390  3c030390  00001390  2**4CONTENTS, ALLOC, LOAD, DATA19 .flash.rodata_noload 00000000  3c03d3ac  3c03d3ac  0005374f  2**0CONTENTS20 .ext_ram.dummy 0003ffe0  3c000020  3c000020  00001020  2**0ALLOC21 .ext_ram.bss  00000000  3c040000  3c040000  0005374f  2**0CONTENTS22 .iram0.text_end 00000041  403831bf  403831bf  0002c1bf  2**0ALLOC23 .iram0.data   00000000  40383200  40383200  0005374f  2**0CONTENTS24 .iram0.bss    00000000  40383200  40383200  0005374f  2**0CONTENTS25 .dram0.heap_start 00000000  3fc997a0  3fc997a0  0005374f  2**0CONTENTS26 .xt.prop      0002c2f8  00000000  00000000  0005374f  2**0CONTENTS, READONLY27 .xt.lit       000013a0  00000000  00000000  0007fa47  2**0CONTENTS, READONLY28 .xtensa.info  00000038  00000000  00000000  00080de7  2**0CONTENTS, READONLY29 .comment      0000004b  00000000  00000000  00080e1f  2**0CONTENTS, READONLY30 .debug_frame  00013cd8  00000000  00000000  00080e6c  2**2CONTENTS, READONLY, DEBUGGING, OCTETS31 .debug_info   001d8e2e  00000000  00000000  00094b44  2**0CONTENTS, READONLY, DEBUGGING, OCTETS32 .debug_abbrev 00028366  00000000  00000000  0026d972  2**0CONTENTS, READONLY, DEBUGGING, OCTETS33 .debug_loc    000d92c3  00000000  00000000  00295cd8  2**0CONTENTS, READONLY, DEBUGGING, OCTETS34 .debug_aranges 00007910  00000000  00000000  0036efa0  2**3CONTENTS, READONLY, DEBUGGING, OCTETS35 .debug_ranges 0000f150  00000000  00000000  003768b0  2**3CONTENTS, READONLY, DEBUGGING, OCTETS36 .debug_line   00176594  00000000  00000000  00385a00  2**0CONTENTS, READONLY, DEBUGGING, OCTETS37 .debug_str    000474ad  00000000  00000000  004fbf94  2**0CONTENTS, READONLY, DEBUGGING, OCTETS38 .debug_loclists 0000f07c  00000000  00000000  00543441  2**0CONTENTS, READONLY, DEBUGGING, OCTETS39 .debug_rnglists 00000418  00000000  00000000  005524bd  2**0CONTENTS, READONLY, DEBUGGING, OCTETS40 .debug_line_str 00001955  00000000  00000000  005528d5  2**0CONTENTS, READONLY, DEBUGGING, OCTETS

定位了问题出在arm2d里面,好像是arm2d的某个操作导致了内存段覆盖。搜索关键词arm2d.tile,发现很多地方使用了ARM_SECTION(“arm2d.tile.c_tileUTF8UserFontA1Mask”)。知道这里很简单,找到根源然后将它注释,这个宏就不会起作用了。
大约是在components/arm2d/Arm-2D/Library/Include/arm_2d_utils.h文件的620行我找到了这个宏,并在
arm_2d_cfg.h中加入宏定义来替换掉内部的宏:

// 屏蔽内部的段操作
#define ARM_SECTION(__X)

至此,我们编译终于成功。虽然这时候还有一些warning没有消除(arm2d被调用的时候产生的warning)。但是我也无力追求完美了。直接编译,烧录。
结果没有出现想要的动画效果,这里我们通过在arm_2d_cfg.h中打开log分析发现,是因为没有启动arm_2d_disp_adapter_0.h中的默认界面。我们通过arm_2d_disp_adapter_0.h的下面宏:

// <q>Disable the default scene
// <i> Remove the default scene for this display adapter. We highly recommend you to disable the default scene when creating real applications.
#ifndef __DISP0_CFG_DISABLE_DEFAULT_SCENE__
#   define __DISP0_CFG_DISABLE_DEFAULT_SCENE__                     0
#endif

这里默认__DISP0_CFG_DISABLE_DEFAULT_SCENE__ 是1,我们设置成0,打开它。再进行编译烧录(别忘记arm_2d_cfg.h中关闭log)。结果如下:

运行效果

总结

其实我的结果并不是很好。按照傻孩子大佬的话说,还是有很大优化空间。下一步优化应该就是在spi异步传输的方向上。乐鑫的spi分为queue传输和poll传输,其中bus库采用的是poll传输。然而这种方式相当于同步操作,应该用queue去异步等待,这样能在传输的同时计算像素。能够消除LCD-Latency的时间。我这次记录摸索过程相当于是抛砖引玉,希望大家能够优化出更好的版本

关于移植

移植的一个最大的准则就是”不要动别人的源码“。按照傻孩子大佬的说法就是【用扩展替代修改】。我遇到问题,虽然会深入源码,但是会根据源码的情况在配置文件或者是自己写的文件里面进行补充。如果别人的代码让你无法这样操作,那就是提issue的时候。

后记(碎碎念)

arm2d的理念是以mask为中心,所有的东西全都是贴图加上arm2d自带的蒙版(mask)完成的效果。这个理念实际上不是gui的理念,例如lvgl是以控件为中心。arm2d则是更加底层, 从使用者的角度实际上会比较麻烦,但是这种效果能够在性能有限的设备上发挥很大的效果。实际上arm2d的源码一度让我崩溃,以宏构建的内容, 很多情况下无从知晓如何使用。代码的抽象程度已经完全是另一种语言。这次的移植意义也不大,因为esp32性能足够,有更加有好的lvgl,没必要折腾arm2d。主要是想学习学习,也想挑战一下自己。arm2d的代码在我看来我不能评价他是不好的,毕竟恐怖的效率,惊人的效果还是深深折服。但是我个人还是不会学习他的做法,我希望能写出更加清晰易懂的代码。

相关文章:

  • 计算机编码以及URL转码
  • C语言王国——深入自定义类型(联合体、枚举)
  • 一款Wordpress网站导航主题,带昼夜切换功能
  • .NET C# 使用GDAL读取FileGDB要素类
  • Spring Boot集成tablesaw插件快速入门
  • 2024广东省职业技能大赛云计算赛项实战——Ceph集群部署
  • 微信小程序-scroll-view实现上拉加载和下拉刷新
  • 细说MCU定时器中断的实现方法
  • Java18新特性(极简)
  • 在Linux/Ubuntu/Debian中使用SSH连接远程服务器VPS
  • curl命令使用
  • 活出属于自己的人生
  • VScode基本使用
  • 使用 Netty 自定义解码器处理粘包和拆包问题详解
  • 【HiveSQL】join关联on和where的区别及效率对比
  • 【跃迁之路】【735天】程序员高效学习方法论探索系列(实验阶段492-2019.2.25)...
  • 2018天猫双11|这就是阿里云!不止有新技术,更有温暖的社会力量
  • C# 免费离线人脸识别 2.0 Demo
  • JavaScript 基础知识 - 入门篇(一)
  • React-flux杂记
  • React-Native - 收藏集 - 掘金
  • Spark in action on Kubernetes - Playground搭建与架构浅析
  • spring boot 整合mybatis 无法输出sql的问题
  • Spring Boot快速入门(一):Hello Spring Boot
  • Zepto.js源码学习之二
  • 阿里云应用高可用服务公测发布
  • 不用申请服务号就可以开发微信支付/支付宝/QQ钱包支付!附:直接可用的代码+demo...
  • 精彩代码 vue.js
  • 聊聊springcloud的EurekaClientAutoConfiguration
  • 码农张的Bug人生 - 见面之礼
  • 七牛云假注销小指南
  • 推荐一款sublime text 3 支持JSX和es201x 代码格式化的插件
  • 在 Chrome DevTools 中调试 JavaScript 入门
  • 怎么将电脑中的声音录制成WAV格式
  • 栈实现走出迷宫(C++)
  • nb
  • No resource identifier found for attribute,RxJava之zip操作符
  • ​iOS实时查看App运行日志
  • ​secrets --- 生成管理密码的安全随机数​
  • ​TypeScript都不会用,也敢说会前端?
  • ### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTr
  • ###C语言程序设计-----C语言学习(6)#
  • #mysql 8.0 踩坑日记
  • #vue3 实现前端下载excel文件模板功能
  • (C++哈希表01)
  • (function(){})()的分步解析
  • (MonoGame从入门到放弃-1) MonoGame环境搭建
  • (MTK)java文件添加简单接口并配置相应的SELinux avc 权限笔记2
  • (二)【Jmeter】专栏实战项目靶场drupal部署
  • (二)JAVA使用POI操作excel
  • (转)chrome浏览器收藏夹(书签)的导出与导入
  • . ./ bash dash source 这五种执行shell脚本方式 区别
  • .bat批处理(九):替换带有等号=的字符串的子串
  • .DFS.
  • .java 9 找不到符号_java找不到符号