vivi源代码最为详细分析(二)

2023-05-16

现在进入bootloader之vivi分析的第二阶段,这部分使用C语言实现,部分代码采取内嵌汇编的方式。这里需要用到GNU GCC内嵌汇编的知识,这部分基础还没有具备,需要学习。

 
    下面先按照流程进行分析。需要注意的是,此部分内容并非完全按照原版的vivi源代码,而是加入了自己的理解。另外,对非常简单、google出一片而且有分析正确的部分,在这里就简化了,不做详细分析,只是对网上没有分析到位而又影响理解的部分进行深入分析。我想,这部分内容应该是对《s3c2410完全开发》中vivi源代码分析部分的补充和完善。
 
stage 2:【init/main.c】
 
    第二阶段的入口就是init/main.c, 按照源代码的组织流程,根据模块化划分的原则,应该分为8个功能模块,源代码注释以step区分,非常清晰。现在首先解决一个问题,就是关于main的形参。vivi源代码中对main的原型使用了: int main(int argc, char *argv[])的标准形式,在第一阶段的【arch/s3c2410/head.S】中,利用 APCS设定了相应的入口参数,如下:
 

@ get read to call C functions
        ldr sp, DW_STACK_START @ setup stack pointer
        mov fp, #0 @ no previous frame, so fp=0
        mov a2, #0 @ set argv to NULL

        bl main @ call main 

 
    这里的sp、fp、a2都是APCS中名字,与之对应的寄存器分别为R13、R11、R2。这里理解的重点在于 fp(frame pointer,帧指针),也就是栈帧指针。这是比较复杂的一个地方,对栈需要有深入的分析。搜集了一些资料,看了APCS标准,后续会把关于fp和APCS的部分单独拿出,总结成文。不管怎样, 通过其入口地址的设置也可以看出,main是不需要入口地址的。那么,为了理解上的方便,不妨把main原型改为int main(void),这样,相应的入口地址就不需要设置了。更改后的head.S对应部分如下:
 

@ jump to ram
        @ a technology about trampoline
        ldr pc, =on_the_ram

on_the_ram:
        bl main
        @ if main ever returns, reboot
        mov pc, #FLASH_BASE

 
    清晰而且符合规则,前提是init/main.c中main原型修改为int main(void)。当然,对main还动了一些手术,现在把main的主流程部分放在这里,后面会对为什么如此改动详细说明。
 
 

int main(void)
{
        int ret;

        
/*
         * Step 1:
         * Print Vivi version information
         */

        putstr("/r/n");
        putstr(vivi_banner);
        reset_handler();
        
/*
         * Step 2:
         * initialize board environment
         */

        ret = board_init();
        if (ret) {
                putstr("Failed a board_init() procedure/r/n");
                error();
        }

        
/*
         * Step 3:
         * MMU management
         * When it's done, vivi is running on the ram and MMU is enabled.
         */

        mem_map_init();
        mmu_init();
        putstr("Succeed memory mapping./r/n");

        
/*
         * Step 4:
         * initialize the heap area
         */

        ret = heap_init();
        if (ret) {
                putstr("Failed initailizing heap region/r/n");
                error();
        }

        
/*
         * Step 5:
         * initialize the MTD device
         */

        ret = mtd_dev_init();

        
/*
         * Step 6:
         * initialize the private data
         */

        init_priv_data();

        
/*
         * Step 7:
         * initialize the humanmachine environment
         */

        misc();
        init_builtin_cmds();

        
/*
         * Step 8:
         * boot kernel or step into vivi
         */

        boot_or_vivi();

        return 0;
}

 
(1)step 1:打印版本信息
 
    这一部分其实是作为调试和增强人机交互行而用的,如果不用,对vivi的主要功能也不会产生影响。本来是最为简单的一个部分,但是实际上却是我理解上问题最多的一个部分, 对这块动的手术也最多。事实上,这个部分的reset_handler存在bug。具体分析一下。
 
    源代码step 1部分如下:

putstr("/r/n");
putstr(vivi_banner);

reset_handler();

 
    打印的vivi_banner在【init/version.c】中,如下:
 

#include "version.h"
#include "compile.h"

const char *vivi_banner =
                       "VIVI version " VIVI_RELEASE " (" VIVI_COMPILE_BY "@"
                       VIVI_COMPILE_HOST ") (" VIVI_COMPILER ") " UTS_VERSION "/r/n";

 
   vivi_banner就是字符串,中间有五个未知的宏,这是非常明显的。显然就是头文件中给出的。 但是find,没有version.h。compile.h,所以现在就要知道version.h,compile.h是怎样生成的,在compile.h中一次定义了VIVI_COMPILE_BY,VIVI_COMPILE_HOST,VIVI_COMPILER ,UTS_VERSION 这四个宏,这在前面Makefile分析时也详细讲解过了,我们可以在顶层的Makefile中找到在编译时是如何自动生成version.h以及compile.h这两个头文件的:
include/version.h:
 @echo /#define VIVI_RELEASE /"$(VIVIRELEASE)/" > .ver
 @echo /#define VIVI_VERSION_CODE `expr $(VERSION) //* 65536 + $(PATCHLEVEL) //* 256 + $(SUBLEVEL)` >> .ver
 @echo '#define VIVI_VERSION(a,b,c) (((a) << 16) + ((b) << 8) + (c))' >>.ver
  @mv -f .ver $@
可以看到在version.h中定义了VIVI_RELEASE,VIVI_VERSION_CODE以及VIVI_VERSION(a,b,c)三个宏,同样在顶层Makefile中也定义了compile.h的生成规则,而且还把version.h合并入compile.h里面了。可以just for fun,增加一些个性化的打印信息,比如,我的version.c修改如下:
 

#include "compile.h"

const char *vivi_banner =
                        "/r/n/t^_^ Well done, boy! Go on -->/r/n"
                        "VIVI version " VIVI_RELEASE " (" VIVI_COMPILE_BY "@"
                        VIVI_COMPILE_HOST ") (" VIVI_COMPILER ") " UTS_VERSION "/r/n";

 
    下面进入疑惑的reset_handler功能部分。作者的本意是 利用reset_handler()实现软复位硬复位的处理。
 

【lib/reset_handle.c】
void
reset_handler(void)
{
    int pressed;

    pressed = is_pressed_pw_btn();

    if (pressed == PWBT_PRESS_LEVEL) {
        DPRINTK("HARD RESET/r/n");
        hard_reset_handle();
    } else {
        DPRINTK("SOFT RESET/r/n");
        soft_reset_handle();
    }
}

 
    首先看一下 is_pressed_pw_btn,按照函数字面意思,应该是 判断电源复位键是否按下如果按下,则证明是硬复位如果没有检测到键按下,那么就是软复位。具体代码如下:

static int
is_pressed_pw_btn(void)
{
    return read_bt_status();
}

--> read_bt_status

static int
read_bt_status(void)
{
    ulong status;

    
//status = ((GPLR & (1 << GPIO_PWBT)) >> GPIO_PWBT);

    status = ((PWBT_REG & (1 << PWBT_GPIO_NUM)) >> PWBT_GPIO_NUM);
    
    if (status)
        return HIGH;
    else
        return LOW;
}

 
    可是, PWBT_REG是没有定义的,PWBT_GPIO_NUM也没有定义,也就是说,这个函数实际上是不可能编译通过的。从表面上分析,如同网上大部分讨论一样,我可以知道作者是什么意图,但是这段代码真正有效吗?从上面分析看,答案显然是这个reset_handle.c根本就是无效的。那么为什么vivi移植成功都没有注意这个问题,还只是按照表面意思分析代码呢?这个可以看reset_handle.h(在include文件夹中)的头文件。
 

#ifdef CONFIG_RESET_HANDLING
void reset_handler(void);
#else
#define reset_handler()    (void)(0)
#endif

 
    很显然,在配置的时候,CONFIG_RESET_HANDLING是没有定义的,那么reset_handler()为空,也就是说这部分根本就是空代码,并没有实际执行功能。如果还不放心,那就做测试,如果把CONFIG_RESET_HANDLING选中(具体是把General setup部分的support reset handler选中),那么就会出现错误:
 

reset_handle.c: In function `read_bt_status':
reset_handle.c:31: `PWBT_REG'
undeclared (first use in this function)
reset_handle.c:31: (Each undeclared identifier is reported only once
reset_handle.c:31: for each function it appears in.)
reset_handle.c:31: `PWBT_GPIO_NUM
' undeclared (first use in this function)
reset_handle.c:28: warning: `status'
might be used uninitialized in this function
reset_handle.c: In function `hard_reset_handle
':
reset_handle.c:52: `USER_RAM_BASE'
undeclared (first use in this function)
reset_handle.c:52: `USER_RAM_SIZE
' undeclared (first use in this function)
reset_handle.c: In function `reset_handler'
:
reset_handle.c:68: `PWBT_PRESS_LEVEL
' undeclared (first use in this function)
make[2]: *** [reset_handle.o] Error 1
make[2]: Leaving directory `/home/armlinux/embedded_Linux/s3c2410/bootloader/m-boot-1.0.0/lib'

make[1]: *** [first_rule] Error 2
make[1]: Leaving directory `/home/armlinux/embedded_Linux/s3c2410/bootloader/m-boot-1.0.0/lib

 
    可见这部分功能是多余的。可以选择把这部分设置完全去掉,方法如下:
 
    ·【init/main.c】去掉行reset_handler();,去掉#include <reset_handle.h>。
    · 删除【lib/reset_handle.c】,删除【include/reset_handle.h】
    ·【arch/config.in】,删除行bool 'support reset handler' CONFIG_RESET_HANDLING,这样就彻底把此项配置部分也删除了。如果还有原来的默认配置文件,可以把# CONFIG_RESET_HANDLING is not set删除。
 
    经过上面三步,就可以把reset handler功能去掉了。这些在你了解了vivi的配置机制后是很容易操作的,它们之间的关系并不复杂,就是一条链,顺着找就可以了。
 
    我现在第一步想做的是把vivi进行“瘦身”,只需要完成在EDUKIT-III上 从nand flash启动引导内核的功能就可以,从中也可以了解核心技术和主要流程。但是,在整个的软件架构上是保持不变的。如果我想增加功能,因为对这个软件架构熟悉了,所以很容易扩展,而且也容易自己重新做一个功能更好一些的bootloader。
 
(2)step 2:
 
    主要是初始化GPIO。这个在前面实验中做过了,基本的思路和方法就是在把握好整个系统硬件资源的前提下,根据datasheet把所有的初始值设定,在这里利用这个函数就可以完成初始化了。我们在这里稍微分析下board_init()函数(arch/s3c2410/smdk.c)
int board_init(void)
{
 init_time();   //arch/s3c2410/proc.c中
 set_gpios(); //arch/s3c2410/smdk.c
 return 0;
}
先来看看init_time():
void init_time(void)
{
 TCFG0 = (TCFG0_DZONE(0) | TCFG0_PRE1(15) | TCFG0_PRE0(0));
}
再来看看set_gpios()
void set_gpios(void)
{
 GPACON  = vGPACON;
 GPBCON  = vGPBCON;
 GPBUP   = vGPBUP;
 GPCCON  = vGPCCON;
 GPCUP   = vGPCUP;
 GPDCON  = vGPDCON;
 GPDUP   = vGPDUP;
 GPECON  = vGPECON;
 GPEUP   = vGPEUP;
 GPFCON  = vGPFCON;
 GPFUP   = vGPFUP;
 GPGCON  = vGPGCON;
 GPGUP   = vGPGUP;
 GPHCON  = vGPHCON;
 GPHUP   = vGPHUP;
 EXTINT0 = vEXTINT0;
 EXTINT1 = vEXTINT1;
 EXTINT2 = vEXTINT2;
}也就是设置各个GPIO口的控制寄存器,和上拉寄存器
 
(3)step 3:
 
    MMU初始化。这部分在MMU基础实验中完成了。关于GNU GCC内嵌汇编部分还不是太清晰,还有待于在后续工作中加强。
     mem_map_init(); //arch/s3c2410/mmu.c
    mmu_init();
 
(4)step 4:
    ret = heap_init(); //lib/heap.c
   
    heap——堆,内存动态分配函数mmalloc就是从heap中划出一块空闲内存的,mfree则将动态分配的某块内存释放回heap中。
heap_init函数在SDRAM中指定了一块1M大小的内存作为heap(起始地址HEAP_BASE  =  0x33e00000),并在heap的开头定义了一个数据结构blockhead——事实上,heap就是使用一系列的blockhead数据结构来描述和操作的。每个blockhead数据结构对应着一块heap内存, 假设一个blockhead数据结构的存放位置为A,则它对应的可分配内存地址为“A + sizeof(blockhead)”到“A +
sizeof(blockhead) + size - 1”。(因为A地址处存放了一个blockhead结构所以不能分配,从A+sizeof(blockhead)处开始,而blockhead结构体中的size定义了该结构控制的区域的大小,所以这块区域的尾地址也就是A+sizeof(blockhead)+size -1,后面的blockhead结构就要从这块的尾地址开始存放,这样依次存放,每个blockhead结构通过指针练成一个链表,这样利于分配)
    堆初始化。堆与栈的区别已经比较清晰了,在动手分析vivi的过程中,更为明确了。在这里,实际上就是 实现动态内存分配策略。具体实现部分在【lib/heap.c】。因为以前自己没有写过动态内存分配,所以要仔细分析这部分是如何实现的。这部分的工作主要有两个: 一是分析封装调试宏的技巧和printk的实现方法,这部分在这里还是挺重要的。二是heap基本的原理是什么?具体如何实现?我们暂且来分析下heap_init函数:
int heap_init(void)
{
 return mmalloc_init((unsigned char *)(HEAP_BASE), HEAP_SIZE); 
}
 我们在smdk2410.h中发现上面两个常量的定义:
#define HEAP_SIZE  SZ_1M //堆的分配大小为1M
#define HEAP_BASE  (VIVI_RAM_BASE - HEAP_SIZE)//我们可以看到一是高端顺序存放二是堆空间的分配挨着sdram中VIVI的分区
typedef struct blockhead_t {
 Int32 signature;  //固定为BLOCKHEAD_SIGNATURE
 Bool allocated;  //此区域是否已经分配出去:0-N,1-Y
 unsigned long size;  //此区域大小
 struct blockhead_t *next;  //链表指针
 struct blockhead_t *prev;  //链表指针
} blockhead;
vivi对heap的操作比较简单,vivi中有一个全局变量static blockhead *gHeapBase,它是heap的链表头指针,通过它可以遍历所有blockhead数据结构。假设需要动态申请一块sizeA大小的内存,则mmalloc函数从gHeapBase开始搜索
blockhead数据结构,如果发现某个blockhead满足:
a.  allocated = 0  //表示未分配
b.  size > sizeA,
则找到了合适的blockhead,于是进行如下操作:
a.allocated设为1
b.如果size – sizeA > sizeof(blockhead),则将剩下的内存组织成
一个新的blockhead,放入链表中
c.返回分配的内存的首地址
分析分析动态分配内存函数mmalloc函数:
void *mmalloc(unsigned long size)
{
 blockhead *blockptr = gHeapBase; //将heap的链表头地址赋值,从头开始查找
 blockhead *newblock;
 Bool compacted = FALSE;
 size = (size+7)&~7; /* unsigned long align the size */
 DPRINTK("malloc(): size = 0x%08lx/n", size);
 while (blockptr != NULL) {
  if (blockptr->allocated == FALSE) { //判断是否被分配出去
   if (blockptr->size >= size) { //判断这个结构控制的区域够不够分配的大小
    blockptr->allocated=TRUE; //将这个区域定义为已经分出去的标志
    if ((blockptr->size - size) > sizeof(blockhead)) { //如果该区域除开分配的内存大小,余下的比结构体大小还要大,就重新创建一个新的blockhead结构体链接到该结构体后面
     newblock = (blockhead *)((unsigned char *)(blockptr) + sizeof(blockhead) + size);
     newblock->signature = BLOCKHEAD_SIGNATURE;
     newblock->prev = blockptr;
     newblock->next = blockptr->next; //链表操作
     newblock->size = blockptr->size - size - sizeof(blockhead);//这时候新建的结构体可分配的大小就是余下的内存减去新建结构体大小
     newblock->allocated = FALSE;
     blockptr->next = newblock;
     blockptr->size = size;
    } else {
    }
    break;
   } else {
    if ((blockptr->next == NULL) && (compacted == FALSE)) {
     if (compact_heap()) {
      compacted=TRUE;
      blockptr = gHeapBase;
      continue;
     }
    }
   }
  }
  blockptr = blockptr->next;
 }
 DPRINTK("malloc(): returning blockptr = 0x%08lx/n", blockptr);
 if (blockptr == NULL)
  printk("Error: malloc(), out of storage. size = 0x%x/n", size);
 return (blockptr != NULL) ? ((unsigned char *)(blockptr)+sizeof(blockhead)) : NULL;
}
再来看看动态内存的释放mfree函数:
void mfree(void *block) {
 blockhead *blockptr;
 if (block == NULL) return;
 blockptr = (blockhead *)((unsigned char *)(block) - sizeof(blockhead));
 if (blockptr->signature != BLOCKHEAD_SIGNATURE) return;
 blockptr->allocated=FALSE;//主要就是这句,将该区域的分配标志置为没分出去,就表示回收啦
 return;
}
释放内存的操作更简单,直接将要释放的内存对应的blockhead数据结构的allocated设为0即可。
static blockhead *gHeapBase = NULL; //分配堆空间的起始地址结构
static inline int mmalloc_init(unsigned char *heap, unsigned long size)
{
 if (gHeapBase != NULL) return -1;
 DPRINTK("malloc_init(): initialize heap area at 0x%08lx, size = 0x%08lx/n", heap, size);//#define DPRINTK(args...) printk(##args)
 gHeapBase = (blockhead *)(heap); //在堆的分配起始地址处定义一个blockhead的结构体
 gHeapBase->allocated=FALSE;
 gHeapBase->signature=BLOCKHEAD_SIGNATURE;
 gHeapBase->next=NULL;
 gHeapBase->prev=NULL;
 gHeapBase->size = size - sizeof(blockhead);
 return 0;
}
分配heap区域后,内存划分情况如下:
0x34000000-0x33f00000这1MB的空间存放的是vivi的stage1和stage2,0x33f00000-0x33e00000这1MB的空间就是堆空间。
    下面首先进行第一个重点分析。关于 调试手段,在分析ARM的基本调试手段时也提到过,使用串口打印调试信息是一个非常有效且常用的手段,vivi中采取的也是这种方式。当然,如果你只是实现最为简单的打印字符串等,那么 初始化串口后,封装一个基本的输出函数就可以了。但是,这个基本函数的功能是非常有限的。我们在Linux用的printk则要强大好用的多。 vivi的思想就是把Linux kernel的printk拿过来,稍微裁减一下(因为vivi不需要打印级别,但是需要打印手段的多样化)。这样,自己的工作量并不大,但是调试手段则要完善得多了。在这里,关于printk的代码细节不作为重点,vivi也只是借用了Linux kernel的printk的实现,并做了简单的修改,把console映射到了串口0上。
 
    手头上暂时有linux-2.4.18的内核,暂时以这个为依据来探讨vivi中printk的实现。

    ·复制【lib/vsprintf.c】到vivi的lib目录下,更改名称为printk.c。 然后只保留vsnprintf,及其用到的number函数、skip_atoi函数。skip_atoi中用到了isdigit,所以把【include/linux/ctype.h】复制到vivi的include目录下。另外,还要用到do_div和strnlen两个函数。其中do_div是宏,在【include/asm-arm/div64.h】中实现,直接复制到vivi的include文件夹中。strnlen应该在string.c中实现,可以从【lib/string.c】复制然后添加到vivi的lib下的string.c文件中,最后把声明加到include下的vivi_string.h中。这样,printk需要的基础部分就具备了。
    ·复制【kernel/printk.c】,然后把printk的实现部分摘出来,去掉打印等级等功能,参考vivi的就可以封装起来了。
 
    可见,vivi中的printk只是把Linux kernel中的代码拿过来,做了及其少量的修改。我现在已经重现了这个过程,并且对整个vivi工程文件做出一些修改,编译下载,测试功能稳定。
 
    实现了printk,往往需要封装一个调试宏。在【lib/heap.c】中和其他一些文件中,调试宏都是这样的形式:

#ifdef DEBUG_HEAP
#define DPRINTK(args...)    printk(##args)
#else
#define DPRINTK(args...)
#endif

 
    分析这样是不妥的。原因就在于stdarg.h (注意,这里只需要定义头文件<stdarg.h>就可以把变长参数表的功能引入了。该头文件的实现因为不同的机器而不同,但是提供的接口是一致的。具体可以看《The C Programming Language》。但是一个细节就是你如果find,会找不到stdarg.h这个头文件,原因就是gcc直接把stdarg.h放到编译器里。)规定的 变长参数表必须至少包括一个有名参数,va_start会将最后一个有名参数作为起点。这里封装的printk缺少了有名参数,这里可以做测试。测试工程如下:
通过这个测试手段,发现如果选择方式 #define DPRINTK(fmt, args...)   printk(fmt, ##args),那么结果如下:
 

[armlinux@lqm printk_test]$ make
gcc -Wall -g -O2 -c -o printk.o printk.c
gcc -Wall -g -O2 -c -o test.o test.c
gcc -Wall -g -O2 printk.o test.o -o test
[armlinux@lqm printk_test]$ ls
Makefile printk.c printk.h printk.o test test.c test.o
[armlinux@lqm printk_test]$ ./test
test: i = 5, j = 10

 
    如果选择 #define DPRINTK(args...)        printk(##args),那么结果如下:
 

[armlinux@lqm printk_test]$ make
gcc -Wall -g -O2 -c -o printk.o printk.c
gcc -Wall -g -O2 -c -o test.o test.c
test.c:23:47: warning: pasting "(" and ""test: i = %d, j = %d/n"" does not give a valid preprocessing token
gcc -Wall -g -O2 printk.o test.o -o test
[armlinux@lqm printk_test]$ ls
Makefile printk.c printk.h printk.o test test.c test.o
[armlinux@lqm printk_test]$ ./test
test: i = 5, j = 10

 
    可见第二种方式是不合适的。于是修改如下:
 

#ifdef DEBUG_HEAP
#define DPRINTK(fmt, args...) printk(fmt, ##args)
#else
#define DPRINTK(fmt, args...)
#endif

 
    这样的调试宏就没有问题了。也算是宏的一个小技巧吧,在Linux内核中查看,可以看到不少的printk的宏封装都是这样的。
 
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

vivi源代码最为详细分析(二) 的相关文章

  • C#完整的通信代码(点对点,点对多,同步,异步,UDP,TCP)

    C code namespace UDPServer class Program static void Main string args int recv byte data 61 new byte 1024 构建TCP 服务器 得到本机
  • http digest认证(Java server)

    背景 xff1a 服务器接收客户端请求 xff0c 处理并验证 并返回服务器的验证结果 关于digest认证的相关概念及验证原理查看相关的说明 xff0c 此处只对处理进行贴码 CODE import com alibaba fastjso
  • 测试apache时出现[error] [client 192.168.6.1] File does not exist: /etc/httpd/htdocs

    问题 xff1a 测试apache时出现 error client 192 168 6 1 File does not exist etc httpd htdocs 解决方法 xff1a 1 创建文件夹htdocs xff08 etc ht
  • HAL库 STM32 串口通信函数

    HAL UART Receive IT串口 xff01 HAL UART Receive IT amp UART1 Handler u8 aRxBuffer RXBUFFERSIZE HAL UART Receive IT函数使用的时候 简
  • linux环境下安装QT超详细

    QT安装 1 首先下载QT安装包 QT官网 xff1a Index of archive qt 我这里使用的是qt opensource linux x64 5 14 0 run版本 2 打开终端 xff0c 输入命令 xff0c 赋予安装
  • 使用ssh连接虚拟机保姆级教程

    首先安装SSH 安装先检测是否已经安装SSH xff1a service ssh status 如果出现提示 xff1a ssh unrecognized service 说明没有安装openSSH xff0c 则需安装ssh SSH 服务
  • 小游戏2048设计思路超简单

    2048作为一个经典的小游戏 xff0c 对于C语言的逻辑练习是一个比较好的案例了 xff0c 看似很复杂 xff0c 但是如果掌握了设计思路 xff0c 那么就不会觉得难了 xff0c 而且会了这个之后对今后编程的也会有很大的帮助 先分析
  • ES-Elasticsearch查看所有索引及查看某索引下的信息

    1 查看所有索引 xff0c 地址栏直接访问下面的连接 http localhost 9200 cat indicesv amp pretty 2 查看某索引下存的信息 xff0c 查询的信息为索引结构信息 xff08 indexName为
  • 教你十分钟搭建博客,已在多台电脑测试,无坑

    1 前期工作 1 注册Github账号 官网地址 xff1a GitHub 2 下载安装git Git软件下载地址 xff1a Git Downloading Package git scm com 安装的话一直点next就可以了 3 绑定
  • MQTT使用TLS加密

    使用TLS加密在MQTT的使用中是比较常见的 xff0c TLS加密过程在网上有很多说明 xff0c 但是没几个应用教程的 xff0c MQTT软件中的EMQX软件是支持TLS加密的 xff0c 只不过要进行一些设置 安装EMQX软件 首先
  • linux下将QT移植至arm环境

    前言 讲下整个项目流程 xff0c 我们的目标是把qt编出来程序放在arm开发板上面跑 xff0c 首先下载QT源码和tslib源码 xff08 QT源码编译和QT程序运行需要tslib库的支持 xff09 xff0c 在虚拟机里使用交叉编
  • linux下移植onvif至arm环境

    前言 onvif是一种网络摄像头协议 xff0c linux网络摄像头这一块是需要移植onvif协议的 xff0c 整个移植过程是这样的 xff0c 首先onvif协议是依赖于gsoap的 xff0c 所以需要先将gsoap编译安装 xff
  • C语言使用二级指针实现字符串分割

    话不多说 xff0c 直接上代码 include lt stdio h gt int opt char p char ch char res char q 61 p if q 61 61 NULL q 61 61 39 0 39 retur
  • 已include包却提示未定义标识符

    已 include lt string gt xff0c include lt vector gt 却提示string xff0c vector未定义的标识符 因为没定义默认的命名空间 xff0c 改为std string xff0c st
  • jpg和png的区别小结

    png图像的大小是jpg图像大小的数倍 xff0c png为可移植网络图形格式 xff0c 也是一种位图文件存储格式 xff0c 可以进行无损压缩 xff0c jpg是最常见的图像格式 xff0c 图像占用的存储较小 xff0c 但是牺牲了
  • 色彩空间中的HSL、HSV、HSB有什么区别?

    作者 xff1a 大蔚陈 链接 xff1a https www zhihu com question 22077462 answer 29483467 来源 xff1a 知乎 著作权归作者所有 商业转载请联系作者获得授权 xff0c 非商业
  • opencv小程序练习——createBackgroundSubtractorMOG2()实现跟踪

    思想 xff1a 使用createBackgroundSubtractorMOG2 使用运动物体背景分割 使用findContours 查找轮廓并画出 实现一定感官上的跟踪功能 代码如下 xff1a include lt opencv2 o
  • opencv_tutorial_code学习——canny边缘检测后findContours

    tutorial code ShapeDescriptors findContours demo cpp 步骤 xff1a 1 灰度化 2 滤波 3 canny边缘检测 4 findContours canny output contour

随机推荐

  • idea开发工具右侧没有maven工具栏

    一 解决方式一 依次点击 View gt Tool Windows gt Maven xff0c 如下图 xff1a 二 解决方式二 点击如图所示IDEA界面最左下角的按钮 xff0c 如下图 xff1a 三 解决方式三 1 打开项目的 p
  • 语义分割与实例分割的区别

    目前的分割任务主要有两种 xff1a xff08 1 xff09 像素级别的语义分割 xff08 2 xff09 实例分割 这个有意思 xff0c 什么叫实例分割呢 xff1f 它与语义分割有什么区别与联系呢 xff1f 顾名思义 xff0
  • tiny-dnn配置运行

    tiny dnn是一个轻量级的CNN xff08 卷积神经网络 xff09 xff0c 不需要各种依赖和GPU xff0c 由三千多行C 43 43 代码完成 适配android平台的话 xff0c 感觉这个比较好做一点 下载地址 xff1
  • SSM笔记

    进入数据库 xff1a 34 D Program Files x86 mysql 5 7 20 winx64 bin mysql exe 34 u root p admin 展示数据库 xff1a show databases 创建数据库
  • 工程编译那点事:Makefile和cmake(一)

    makefile 前言Makefile介绍 Makefile规则make是如何工作的Makefile使用变量让make自动推导另类风格的Makefilemake中的函数文件搜索绝对顶级的大型工程Makefile 前言 一个项目 xff0c
  • 头文件相互包含问题

    先来看看以下这个例子 xff1a Keyboard h的代码为 span class token macro property span class token directive hash span span class token di
  • 解决roslaunch启动stage_ros节点仿真时输出rostopic hz速率较低的问题(始终为10Hz)

    问题描述 xff1a 在实验过程中偶然间发现无论是odom还是laser scan的输出频率都只有10Hz clock输出速率有50Hz 直接使用命令 rosrun stage ros stageros test world能够正常输出50
  • STM32八种IO口模式区别

    xff08 1 xff09 GPIO Mode AIN 模拟输入 xff08 2 xff09 GPIO Mode IN FLOATING 浮空输入 xff08 3 xff09 GPIO Mode IPD 下拉输入 xff08 4 xff09
  • linux下C++实现Http请求类(GET,POST,上传,下载)

    linux下C 43 43 实现Http请求类 GET xff0c POST xff0c 上传 xff0c 下载 Http协议简述 协议 xff1a 网络协议的简称 xff0c 网络协议是通信计算机双方必须共同遵从的一组约定 如怎么样建立连
  • 正点原子MiniFly V1.2学习笔记四---txQueue队列数据哪来

    笔记二的第四点中 xff0c 把解包出来的指令发送到 rxQueue队列里 xff0c 然后从txQueue队列取数据发送到串口 那么txQueue队列的数据从哪里来的 一 txQueue数据从哪里来 xff1f 二 什么地方会调用radi
  • redis链接工具

    redis链接工具 今天推荐一款redis链接工具 xff0c 其实世面上连接redis的工具很多 xff0c 但是好用的很少 例如 xff1a redis desktop manager这款工具也不错 xff0c 但是我个人不能使用 xf
  • Linux 网络编程——UDP编程

    一 概述 UDP 是 User Datagram Protocol 的简称 xff0c 中文名是用户数据报协议 xff0c 是一个简单的面向数据报的运输层协议 xff0c 在网络中用于处理数据包 xff0c 是一种无连接的协议 UDP 不提
  • 给指定的寄存器地址:0x0001eea7 ,赋值

    coretexM0平台上给指定的寄存器地址 xff1a 0x0001eea7 赋值100 怎么实现 xff1f xff08 volatile char xff09 0x0001eea7 61 100 xff1b 常见错误1 xff1a xf
  • 常见cmake命令总结

    常见cmake命令总结 cmake常见命令 cmake minimum required 指定CMake的最小版本要求 cmake minimum required VERSION 2 8 project 定义工程名称 project PR
  • 利用Qt Phonon框架制作音视频播放器

    Phonon严格来说其实非为Qt的library xff0c Phonon原本就是KDE 4的开放源代码多媒体API xff0c 後来与Qt合并与开发 xff0c 所以简单来说就是Qt使用Phonon这个多媒体框架来提供一般影音多媒体档案的
  • 主设备号和次设备号

    Linux的设备管理是和文件系统紧密结合的 xff0c 各种设备都以文件的形式存放在 dev目录下 xff0c 称为设备文件 应用程序可以打开 关闭和读写这些设备文件 xff0c 完成对设备的操作 xff0c 就像操作普通的数据文件一样 为
  • Makefile中的wildcard用法

    在Makefile规则中 xff0c 通配符会被自动展开 但在变量的定义和函数引用时 xff0c 通配符将失效 这种情况下如果需要通配符有效 xff0c 就需要使用函数 wildcard xff0c 它的用法是 xff1a wildcard
  • GPIO

    一 什么是GPIO xff1f 首先应该理解什么是GPIO GPIO xff0c 英文全称为General Purpose IO ports xff0c 也就是通用IO口 在嵌入式系统中常常有数量众多 xff0c 但是结构却比较简单的外部设
  • UART

    一 S3C2410内置的UART控制器 S3C2410内部具有3个独立的UART控制器 xff0c 每个控制器都可以工作在Interrupt xff08 中断 xff09 模式或DMA xff08 直接内存访问 xff09 模式 xff0c
  • vivi源代码最为详细分析(二)

    现在进入bootloader之vivi分析的第二阶段 xff0c 这部分使用C语言实现 xff0c 部分代码采取内嵌汇编的方式 这里需要用到GNU GCC内嵌汇编 的知识 xff0c 这部分基础还没有具备 xff0c 需要学习 下面先按照流