一步步学习操作系统(2)——在STM32上实现一个可动态加载kernel的"my-boot"
如果要做嵌入式Linux,我们首先要在板子上烧写的往往不是kernel,而是u-boot,这时需要烧写工具帮忙。当u-boot烧写成功后,我们就可以用u-boot附带的网络功能来烧写kernel了。每当板子上电时,u-boot一般会被加载到内存的前半段,如果我们的kernel之前就已经被烧写到开发板了,那么u-boot会加载kernel到内存的后半段并跳转到kernel的起始地址处执行(或者直接跳转到kernel的起始地址处执行,如果kernel可以直接在flash上执行的话。)
如上图所示,绿色部分为u-boot,红色部分为kernel。
把loader(指u-boot)和kernel分离究竟有什么好处呢?
举个极端的例子:没有grub的话,我们就没办法做windows和linux双系统了。这就是最大的好处。
然而对于嵌入式,我倒是说不出什么上得了台面的理由,根据个人喜好,我倒是有3点理由:
1、不用再求助烧写工具了;
2、方便使用GNU交叉编译工具;
3、摆脱Windows+linux虚拟机的工作平台。
现在,我的笔记本就可以轻松一下了,只需单开fedora/ubuntu就能工作啦!
以下是源码和工程的下载链接:
注:仅可使用在stm32f10x系列
接下来,我们将分为三部分叙述:
1、系统概述;
2、kernel;
3、“my-boot”;
4、先烧写"my-boot“,然后用"my-boot”加载kernel——操作示例;
1、系统概述
接下来我们将建立两个工程,一个是用来编译kernel,一个用来编译loader(姑且命名为“my-boot”)。首先,我们先把“my-boot”和kernel都编译好,并通过烧写工具把“my-boot”烧写进stm32的flash中。然后,我们就可以重启stm32,并使之运行“my-boot”。“my-boot”等待接收烧写kernel的起始命令,当我们通过串口向“my-boot”发送了烧写起始命令后,“my-boot”将把串口设置为DMA模式,并等待我们发送kernel的bin文件。接着,我们再通过串口传送kernel的bin文件。传送结束后,kernel也就被写入stm32的RAM中了,同时“my-boot”把串口切换回通常的窗口通信模式。此时,芯片的控制权依旧被掌控在“my-boot”手中,不过,如果我们再向串口发送一条启动kernel的指令,那么stm32将跳转到kernel代码处执行。至此,我们的目标达成。
2、kernel
我们的kernel很简单,只有一个源文件,其功能就是不停的闪led。程序参考了博客http://www.cnblogs.com/sky1991/archive/2012/10/13/2722640.html的“例子一”,并加以修改与简化,代码给出如下:
;RCC寄存器地址映像
RCC_BASE EQU 0x40021000
RCC_CR EQU (RCC_BASE + 0x00)
RCC_CFGR EQU (RCC_BASE + 0x04)
RCC_CIR EQU (RCC_BASE + 0x08)
RCC_APB2RSTR EQU (RCC_BASE + 0x0C)
RCC_APB1RSTR EQU (RCC_BASE + 0x10)
RCC_AHBENR EQU (RCC_BASE + 0x14)
RCC_APB2ENR EQU (RCC_BASE + 0x18)
RCC_APB1ENR EQU (RCC_BASE + 0x1C)
RCC_BDCR EQU (RCC_BASE + 0x20)
RCC_CSR EQU (RCC_BASE + 0x24)
;GPIO寄存器地址映像
GPIOA_BASE EQU 0x40010800
GPIOA_CRL EQU (GPIOA_BASE + 0x00)
GPIOA_CRH EQU (GPIOA_BASE + 0x04)
GPIOA_IDR EQU (GPIOA_BASE + 0x08)
GPIOA_ODR EQU (GPIOA_BASE + 0x0C)
GPIOA_BSRR EQU (GPIOA_BASE + 0x10)
GPIOA_BRR EQU (GPIOA_BASE + 0x14)
GPIOA_LCKR EQU (GPIOA_BASE + 0x18) SETENA0 EQU 0xE000E100
SETENA1 EQU 0xE000E104 ;;FLASH缓冲寄存器地址映像
FLASH_ACR EQU 0x40022000 ;-----------------
MSP_TOP EQU 0x20005000 ;主堆栈起始值
PSP_TOP EQU 0x20004E00 ;进程堆栈起始值 DelayTime EQU ; to choose a better number to fit your cpu
CLRPEND0 EQU 0xE000E280 ;常数定义---------
Bit0 EQU 0x00000001
Bit1 EQU 0x00000002
Bit2 EQU 0x00000004
Bit3 EQU 0x00000008
Bit4 EQU 0x00000010
Bit5 EQU 0x00000020
Bit6 EQU 0x00000040
Bit7 EQU 0x00000080
Bit8 EQU 0x00000100
Bit9 EQU 0x00000200
Bit10 EQU 0x00000400
Bit11 EQU 0x00000800
Bit12 EQU 0x00001000
Bit13 EQU 0x00002000
Bit14 EQU 0x00004000
Bit15 EQU 0x00008000
Bit16 EQU 0x00010000
Bit17 EQU 0x00020000
Bit18 EQU 0x00040000
Bit19 EQU 0x00080000
Bit20 EQU 0x00100000
Bit21 EQU 0x00200000
Bit22 EQU 0x00400000
Bit23 EQU 0x00800000
Bit24 EQU 0x01000000
Bit25 EQU 0x02000000
Bit26 EQU 0x04000000
Bit27 EQU 0x08000000
Bit28 EQU 0x10000000
Bit29 EQU 0x20000000
Bit30 EQU 0x40000000
Bit31 EQU 0x80000000 ;向量表*********************************************************************************
AREA RESET, DATA, READONLY DCD MSP_TOP ;初始化主堆栈
DCD Start ;复位向量
DCD NMI_Handler ;NMI Handler
DCD HardFault_Handler ;Hard Fault Handler
;***************************************************************************************
AREA |.text|, CODE, READONLY
;主程序开始
ENTRY ;指示程序从这里开始执行
Start
CPSID I ;关中断
ldr r0, =MSP_TOP
msr msp, r0 ;重设MSP
mov r0, #0
msr control, r0 ;切换MSP,并进入特权级 mov r0, #
mov r1, #
mov r2, #
mov r3, #
mov lr, # ldr r0, =CLRPEND0
ldr r1, [r0]
orr r1, #0xFFFFFFFF
str r1, [r0] ;时钟系统设置
;启动外部8M晶振 ldr r0,=RCC_CR
ldr r1,[r0]
orr r1,#Bit16
str r1,[r0]
ClkOk
ldr r1,[r0]
ands r1,#Bit17
beq ClkOk
ldr r1,[r0]
orr r1,#Bit17
str r1,[r0]
;FLASH缓冲器
ldr r0,=FLASH_ACR
mov r1,#0x00000032
str r1,[r0]
;设置PLL锁相环倍率为7,HSE输入不分频
ldr r0,=RCC_CFGR
ldr r1,[r0]
orr r1,#Bit18 | Bit19 | Bit20 | Bit16 | Bit14
orr r1,#Bit10
str r1,[r0]
;启动PLL锁相环
ldr r0,=RCC_CR
ldr r1,[r0]
orr r1,#Bit24
str r1,[r0]
PllOk
ldr r1,[r0]
ands r1,#Bit25
beq PllOk
;选择PLL时钟作为系统时钟
ldr r0,=RCC_CFGR
ldr r1,[r0]
orr r1,#Bit18 | Bit19 | Bit20 | Bit16 | Bit14
orr r1,#Bit10
orr r1,#Bit1
str r1,[r0]
;其它RCC相关设置
ldr r0,=RCC_APB2ENR
mov r1,#Bit2
str r1,[r0]
;IO端口设置
ldr r0,=GPIOA_CRH
ldr r1,[r0]
orr r1,#Bit0 | Bit1 ;PA.8输出模式,最大速度50MHz
and r1,#~Bit2 & ~Bit3 ;PA.8通用推挽输出模式
str r1,[r0] mov r5, # ; led flag ;CPSIE I
;主循环=================================================================================
main
bl Delay
bl LedFlas
b main
;子程序**********************************************************************************
LedFlas
push {r0-r3}
cmp r5,#
beq ONLED mov r5, #
;PA.8输出1
ldr r0,=GPIOA_BRR
ldr r1,[r0]
orr r1,#Bit8
str r1,[r0]
b LedEx
ONLED
mov r5, #
;PA.8输出0
ldr r0,=GPIOA_BSRR
ldr r1,[r0]
orr r1,#Bit8
str r1,[r0]
LedEx
pop {r0-r3}
bx lr Delay
push {r0-r3} ldr r0, =DelayTime
Loop CBZ r0, LoopExit
sub r0, #
b Loop
LoopExit
pop {r0-r3}
bx lr
;异常程序*******************************************************************************
NMI_Handler
;xxxxxxxxxxxxxxxxxx
bx lr
;-----------------------------
HardFault_Handler
;xxxxxxxxxxxxxxxxxx
bx lr
;***************************************************************************************
ALIGN ;通过用零或空指令NOP填充,来使当前位置与一个指定的边界对齐
;-----------------------------
END
(1)主循环程序:
main
bl Delay // 延时
bl LedFlas // 翻转led
b main // 跳转会main开头(即“延时”)
(2)延时程序:
Delay
push {r0-r3}
ldr r0, =DelayTime // r0 = DelayTime;
Loop
CBZ r0, LoopExit // if(r0 != 0) {
sub r0, #1 // r0 -= 1;
b Loop // goto Loop; }
LoopExit
pop {r0-r3}
bx lr
该延时程序是“C51式”的延时,就是纯粹的让CPU空跑n个周期,这里是“DelayTime=13000000“。“13000000”是随便设的一个数,只是为了让眼睛和耐性都能接受,时钟频率变化之后,这个数字可以自行的、随性的去进行调整。
(3)LED翻转程序:
LedFlas
push {r0-r3}
cmp r5,#1 // if(r5 == 1)
beq ONLED // goto ONLED;
mov r5, #1 // r5 = 1;
;PA.8输出1
ldr r0,=GPIOA_BRR
ldr r1,[r0]
orr r1,#Bit8
str r1,[r0]
b LedEx
ONLED
mov r5, #0 // r5 = 0;
;PA.8输出0
ldr r0,=GPIOA_BSRR
ldr r1,[r0]
orr r1,#Bit8
str r1,[r0]
LedEx
pop {r0-r3}
bx lr
该LED翻转程序以“r5”寄存器为标志,“r5”为0或1时,分别使PA.8输出不同的电平(此处PA.8对应开发板上一个红色LED)。
注:
一般MDK会生成hex文件,但不生成bin文件,所以我们还要给MDK加一些设置:
先找到fromelf.exe文件(一般在你的MDK安装目录里的bin目录里),然后如下图输入,
如:
C:\Keil_v5\ARM\ARMCC\bin\fromelf.exe --bin --output kernel.bin kernel.axf
重新编译之后,于是我们就得到kernel的bin文件了,即kernel.bin,留着备用。
此处的kernel是可以独立运行的,所以不妨将该程序通过烧写工具烧写进开发板验证一下。
Note:
如果要用arm-gcc的kernel,首先,你的Linux必须得有arm-gcc编译工具。可使用目录中提供的脚本build.sh直接编译。此处我用的是“arm-none-eabi-as”等,如果是arm-linux-eabi-as等,需要简单修改脚本中的“PREFIX”变量。
3、“my-boot”
我们知道,在kernel和loader之间,真正的主角是kernel,loader只是一个辅助工具罢了。然而,作为loader的"my-boot"在这里却比kernel复杂许多。
“my-boot”以一步步学习操作系统(1)中的代码为基础,并将之整理了一下,把各个源文件分类到了不同的目录。
如图,除了obj目录是存放编译时所用的中间文件和hex文件外,其余4个目录都存放源码。
(1)arch目录:其中的源码均是和CPU架构相关,如中断代码、串口初始化、启动代码等;
(2)include目录:所有的头文件都在这里;
(3)kernel目录:包含主函数、任务调度、延时相关的源码;
(4)lib目录:stm32f10x库函数源码及“printf”重定向至串口的辅助代码(printf_to_serial)。
主程序一共建立3个任务:Task1, TaskBH, TaskDMA_Print。
int main(void)
{
memset(SRAM_Buffer, , PAGE_SIZE);
OSInit(); OSTaskCreate(Task1, (void*), (OS_STK*)&Task1Stk[TASK_STACK_SIZE-]);
OSTaskCreate(TaskBH, (void*), (OS_STK*)&TaskBHStk[TASK_STACK_SIZE-]);
OSTaskCreate(TaskDMA_Print, (void*), (OS_STK*)&TaskDMA_PrintStk[TASK_STACK_SIZE-]); OSStart();
}
Task1:和kernel的功能一样,也是不断的闪led(最好是不同于kernel所使用的led),用来指示程序依旧正常运行,其功能很单纯;
TaskBH:接受串口发送过来的相关命令,并向串口打印信息以提示命令发送成功。特别是当收到“startos”指令后,会置位变量“GotoKernelFlag”,以致后续代码将跳转到kernel运行,该任务是三个任务中最复杂的一个;
TaskDMA_Print:打印RAM中的kernel代码。
其实以上三个任务的负担并不重,身上担子最重的时串口中断程序:
串口通信遵循一个自定义的协议,协议内容如下:
将以下串口中断程序与TaskBH结合着看,串口接受三种命令:
第一种:BURN命令:
如:
"BURN 0x08004000"
协议信息16进制表示为
57 41 4e 15 00 75 42 55 52 4e 20 30 78 30 38 30 30 34 30 30 30
该命令就是在通知开发板:“我要发送kernel了呦,赶紧准备接驾。”
这时,串口中断程序会启动串口的DMA模式,并开启DMA中断。
关于命令中的地址“0x08004000”,该值是设计为以后烧写flash做准备的,但现在我们只将kernel写入SRAM,所以现在还没有特别的作用,任意值都可以。
这个命令发送之后就要小心了,紧跟着必须向串口发送kernel的bin文件。发送结束后,DMA中断会被触发,并且会调用“LED1TURN()”去翻转另一个LED(不同于Task1的LED),用以指示kernel已经被写入RAM。
Note:看了以下代码后,其实对于"BURN”这个命令来说,校验和是形同虚设的,为了图方便就偷了个懒……
volatile void IRQ_Usart1(void)
{ RecvBuffer[Index] = serial_1; // Magic handling
// Byte order: 0 1 2
if(!MagicGotten) {
if( == Index && 'W' == RecvBuffer[Index]) {
Index++;
}else if( == Index && 'A' == RecvBuffer[Index]) {
Index++;
}else if( == Index && 'N' == RecvBuffer[Index]) {
Index++;
MagicGotten = TRUE;
}else {
Index = ;
}
return;
} // Size handling
// byte order: 3 4
if(!SizeGotten) {
Index++;
if( == Index) {
SizeGotten = TRUE;
MsgSize = RecvBuffer[] + (RecvBuffer[] << );
}
if(SizeGotten && MsgSize > BUFSIZ) {
MagicGotten = FALSE;
SizeGotten = FALSE;
Index = ;
}
return;
} // Checksum handling
// byte order: 5
if(!ChecksumGotten) {
Index++;
if( == Index) {
ChecksumGotten = TRUE;
}else {
MagicGotten = FALSE;
SizeGotten = FALSE;
Index = ;
}
return;
} // Data handling:
// byte order: 6...
Index++;
if(Index >= MsgSize) {
MagicGotten = FALSE;
SizeGotten = FALSE;
ChecksumGotten = FALSE;
Index = ;
MsgGotten = TRUE;
if( == strncmp((char *)RecvBuffer + , "BURN", )) {
USART_Cmd(USART1, DISABLE);
USART_ITConfig(USART1, USART_IT_RXNE, DISABLE);
USART_DMACmd(USART1,USART_DMAReq_Rx,ENABLE);
DMA1_Channel5->CNDTR = PAGE_SIZE;//re-load
DMA_Cmd(DMA1_Channel5, ENABLE);//re-open DMA
USART_Cmd(USART1, ENABLE);
LED1TURN();
}
}
}
第二种:“startos”
协议信息16进制表示为
57 41 4e 0d 00 f8 73 74 61 72 74 6f 73
开发板接收到该命令后,TaskBH会将变量“GotoKernelFlag”设为1。之后,当SysTick中断程序(如下)再次执行时,将会调用“ModifyPC()”(这里的“PC”不是指“Personal Computer”,而是指PC指令寄存器哦)。这个函数很难懂。如果能理解这个函数,那么loader加载kernel的原理也就等于理解了80%了。我们不妨来试着啃一啃这块硬骨头!
volatile void IRQ_SysTick(void)
{
OS_ENTER_CRITICAL();
if(GotoKernelFlag) ModifyPC();
if((--TaskTimeSlice) == ){
TaskTimeSlice = TASK_TIME_SLICE;
OSTaskSchedule();
}
TimeMS++; OS_EXIT_CRITICAL();
}
“ModifyPC()”是嵌入C语言式的汇编代码。其作用就是:
修改 PSP中存储的、“当前被SysTick中断的任务”的 PC指针,使之等于kernel代码的起始地址。当该任务再一次被调度时,由于PC被换成了kernel代码的起始地址,所以就进入了kernel。
于是,两个问题出现了:
(1)kernel的起始地址是什么?
(2)被SysTick中断的任务的PC又在哪?
或许有人会认为:“kernel在DMA传送时,被放进‘SRAM_Buffer’这个缓冲区了,那么kernel的起始地址不就是‘SRAM_Buffer’吗?”(一开始我也是这么想的……)
可惜,真正的“起始地址”要比SRAM_Buffer在靠后一点点。
不妨在MDK5下,在kernel工程里打开Debug,接着再用二进制编辑器打开kernel.bin,这样就能看出蹊跷了。
stm32烧写程序时,是将代码烧至起始地址为0x08000000的flash中,并在开机运行时也是直接从flash启动。
看到没有,我们开机时的第一条命令是“CPSID I”,对应的指令地址为0x08000010,机器码为“B672”。
再用二进制文件打开kernel.bin后,发现果然是“B672”(二进制文件为“小端法”表示,所以是“72 B6”)。
所以,我们的kernel代码的起始地址,确切来说是第一条命令“CPSID I”的地址为“SRAM_Buffer + 0x10”。
Note:
“既然代码烧写进地址为0x08000000起始的地方,那么第一条指令为什么确实0x08000010呢?”
意味0x08000010 - 0x08000000 = 0x10 = 16 = 4*4,也即代码开头的“4个DCD”,每个DCD4字节。
第二个问题,“被SysTick中断的任务”的PC到底在哪儿呢?
首先我们要知道,任务使用的是PSP(可参考“PendSV_Handler”的汇编代码)。确认了这点之后,我们就可以继续往下讲了。
根据《Cortex-M3权威指南》--“chap09中断的具体行为”--“入栈”,当SysTick中断发生时,PSP会将发生如下图的变化。
也就是说,当SysTick中断发生时,CPU会自动将被中断任务的R0-R3,R12,LR,PC,xPSR这8个寄存器装载进PSP的后续存储空间,并且PSP最后将指向被中断任务R0寄存器的存储地址。
那么被中断任务的PC寄存器的存储地址就找到啦:PSP+24!如果该任务再次被调度执行,其第一条指令就是地址“PSP+24”存储的内容,如果我“偷偷的”把这个存储内容换成kernel代码的起始地址(确切来说,是第一条指令所在的地址),那么当该任务再次被调度时,原来的任务摇身一变,就成了kernel。
那么,ModifyPC()函数的代码就比较容易理解了。
PCModifyPC伪代码可写为:
ModifyPC()
PSP.PC = SRAM_Buffer+0x10
__asm void ModifyPC(void) {
IMPORT SRAM_Buffer
MRS R0, PSP
LDR R1, =SRAM_Buffer
ADD R1, #0x10
STR R1, [R0, #]
BX LR
align
}
第三种:任意字符串
如:“ls”
协议信息16进制表示为
57 41 4e 08 00 31 6c 73
该命令将对TaskDMA_Print的行为产生影响(代码如下)。
不难看出,只有当“ReadDMAFlag不为0时,该任务才会打印缓冲区SRAM_Buffer的内容。而在TaskBH中,上述命令会使变量“ReadDMAFlag”在0,1之间翻转,所以该命令也就起到控制打印“SRAM_Buffer”内容的作用。
void TaskDMA_Print(void *p_arg)
{
int i = ;
while() {
delayMs();
if(!ReadDMAFlag) continue;
printf("########DMA##########START\r\n");
for(i = ; i < PAGE_SIZE; i++) {
printf("%x ", SRAM_Buffer[i]);
}
printf("########DMA##########END\r\n"); }
}
“my-boot"中几点注意事项:
(1)宏定义PAGE_SIZE
该宏在hardware.h中定义如下:
#define PAGE_SIZE 284
“284”?这个数字怎么这么莫名其妙?其实它表示的是kernel的大小(如下图),同时它也决定了缓冲区SRAM_Buffer的大小。
如果我编译了一个新的kernel,大小不再是284字节了怎么办?
实在对不住!“my-boot”中的这个宏也要改成相应的数字。当然,这确实是个不合理的地方,但现在为使代码尽可能简洁,所以就未做完善这方面的工作了,暂且辛苦一下。
(2)预设宏定义:USE_STDPERIPH_DRIVER
为了使用stm32的函数库,且避免编译出错,故定义该宏。具体内容可查询“stm32f10x.h”第8296行附近的代码。
(3)库函数文件:
如果stm32的型号不是stm32f10x系列的,需要自备相应的函数库。
4、先烧写"my-boot“,然后用"my-boot”加载kernel——操作示例
(1)将“my-boot”烧进stm32开发板
(2)向stm32开发板发送烧写命令:
BURN 0x08004000
16进制表示为
57 41 4e 15 00 75 42 55 52 4e 20 30 78 30 38 30 30 34 30 30 30
命令发送之后,串口工具会打印信息“Addr: 8004000”。而且还有一个变化,那就是另一个LED灯亮了/灭了(如果存在第二个led的话)。
注意,是16进制发送。
(3)发送kernel.bin
这时我们会发现,刚刚亮了/灭了的LED现在又灭了/亮了(如果存在第二个led的话)。
(4)打印刚刚烧进SRAM中的kernel命令(可选):
ls
16进制表示为
57 41 4e 08 00 31 6c 73
该命令发送一次,就会打印一次“###ls###”,并且跟后会打印SRAM中的内容。如果该命令只发送一次,那么SRAM中的打印将每隔2秒打印一次,直到再一次发送该命令为止。
所以图中有2个“###ls###”,第二个就是终止打印的。
(5)启动kernel:
startos
16进制表示为
57 41 4e 0d 00 f8 73 74 61 72 74 6f 73
这时你将会看到开发板在运行kernel的程序啦!
一步步学习操作系统(2)——在STM32上实现一个可动态加载kernel的"my-boot"的更多相关文章
- 一步步学习NHibernate(5)——多对一,一对多,懒加载(2)
请注明转载地址:http://www.cnblogs.com/arhat 通过上一章的学习,我们建立了Student和Clazz之间的关联属性,并从Student(many)的一方查看了Clazz的信 ...
- 一步步学习NHibernate(4)——多对一,一对多,懒加载(1)
请注明转载地址:http://www.cnblogs.com/arhat 通过上一章的学习,我们学会如何使用NHibernate对数据的简单查询,删除,更新和插入,那么如果说仅仅是这样的话,那么NHi ...
- Android学习笔记_31_通过后台代码生成View对象以及动态加载XML布局文件到LinearLayout
一.布局文件part.xml: <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android&qu ...
- XV6操作系统代码阅读心得(一):启动加载、中断与系统调用
XV6操作系统是MIT 6.828课程中使用的教学操作系统,是在现代硬件上对Unix V6系统的重写.XV6总共只有一万多行,非常适合初学者用于学习和实践操作系统相关知识. MIT 6.828的课程网 ...
- Flutter学习笔记(25)--ListView实现上拉刷新下拉加载
如需转载,请注明出处:Flutter学习笔记(25)--ListView实现上拉刷新下拉加载 前面我们有写过ListView的使用:Flutter学习笔记(12)--列表组件,当列表的数据非常多时,需 ...
- Android动态加载学习笔记(一)
前言 上周五DPAndroid小分队就第二阶段分享内容进行了讨论,结果形成了三个主题:性能优化.动态加载.内核远离.我选择的是第二项——动态加载.在目前的Android开发中,这一部分知识还是比较流行 ...
- python获取动态网站上面的动态加载的数据(初级)
我们在处理一些网站数据的时候,有时候我们需要的数据很多都是动态加载的,而不都是静态的,以下以一个实例来介绍简单的获取动态数据,首先申明本人小白,还在学习python中,这个方法还是比较笨拙的,但是对于 ...
- NGUI学习笔记(四):动态加载UI和NGUI事件
动态加载UI 我们进入一个场景后,如果将这个场景所有可能用到的UI都直接放在场景中做好,由于要在进入场景时就部署好所有的UI对象,那么当UI对象较多时会碰到的问题是:1.初始化场景会产生非常明显的卡顿 ...
- 从高德 SDK 学习 Android 动态加载资源
前不久跑去折腾高德 SDK 中的 HUD 功能,相信用过该功能的用户都知道 HUD 界面上的导航转向图标是动态变化的.从高德官方导航 API 文档中 AMapNaviGuide 类的描述可知,导航转向 ...
随机推荐
- Postgresql快速写入/读取大量数据(.net)
环境及测试 使用.net驱动npgsql连接post数据库.配置:win10 x64, i5-4590, 16G DDR3, SSD 850EVO. postgresql 9.6.3,数据库与数据都安 ...
- Codeforces Round #424 (Div. 2, rated, based on VK Cup Finals)A B题
当时晚上打CF时候比较晚,加上是集训期间的室友都没有晚上刷题的习惯,感觉这场CF很不在状态.A题写复杂WA了一发后去厕所洗了个脸冷静了下,换个简单写法,可是用cin加了ios::sync_with_s ...
- 两个java项目,跨域访问时,浏览器不能正确解析数据问题
@Controller@RequestMapping(value = "api/item/cat")public class ApiItemCatController { @Aut ...
- [Oracle]高水位标记(HWM)
(一)高水位标记(High Water Mark,HWM)的概念 所谓高水位标记,是指一个已经分配的段中,已经使用的空间与未使用的空间的分界线.在表的使用过程中,随着数据的不断增多(insert),H ...
- LInux基础命令分类
1. 命令的概念 命令的执行过程 系统第一次执行外部命令时Hash缓存表为空,系统会先从PTAH路径下寻找命令,找到后会将路径加入到Hasa缓存中,当再次执行此命令时会直接从Hash的路径下执行,如果 ...
- Office365开发系列——开发一个全功能的Word Add-In
2016年10月我参加了在北京举行的DevDays Asia 2016 - Office 365应用开发”48小时黑客马拉松“,我开发的一个Word Add-In Demo——WordTemplate ...
- Centos 6.5开启rsync同步
一.测试环境 操作系统:Centos6.5 Server1:172.18.11.100 源服务器 Server2:172.18.11.110 目标服务器 二.操作步骤: 1.先 ...
- USB的前世今生
在人类的历史长河中,很少有一种技术或者传输标准能像USB那样跟我们的生活息息相关,甚至到了没有不行的地步.USB对于今天的人们来说,就好像是空气,是水,是我们每天必需但是又熟视无睹的东西,没有多少人知 ...
- PHP闭包和高阶函数
<?php function func($a, $b) { $line = function ($x) use ($a, $b) { return $a*$x + $b; }; return $ ...
- .NET CORE——Console中使用依赖注入
我们都知道,在 ASP.NET CORE 中通过依赖注入的方式来使用服务十分的简单,而在 Console 中,其实也只是稍微绕了个小弯子而已.不管是内置 DI 组件或者第三方的 DI 组件(如Auto ...