基于STM32系列芯片的 IAP实现的探索

  • 什么是IAP?
  • 如何实现IAP?
    • 第一步:学习官方源代码
    • 第二步:了解STM32芯片基本硬件参数
    • 第三步、搞清除STM32内置Flash
    • 第四步、 STM32程序运行机制
  • IAP代码实现:
    • 1、实现flash写入,删除,修改。
    • 2、IAP 通信协议设计
    • 3、APP代码部分修改
  • 总结:

什么是IAP?

IAP是In Application Programming的首字母缩写,IAP是用户自己的程序在运行过程中对User Flash的部分区域进行烧写,目的是为了在产品发布后可以方便地通过预留的通信口对产品中的固件程序进行更新升级。

哇塞! 这个功能一般在产品正式发布的时候都会实现,我们的手机,MP3 等数码产品都会预留接口实现固件的升级。

如何实现IAP?

关于IAP的相关技术内容确实很多,但是总结起来无非就是通过上位机,它可以是手机或者电脑上的程序,通过无线或者有线的通讯方式,将APP(应用程序)的二进制文件,写入到指定地址的User Flash的区块中。

IAP 将User Flash 区域分成两部分,Bootloader程序引导区和APP 应用程序区域。

上电或者重启时,程序会首先进入Boodloader区,通过特定的标志位,判断程序停留在当前的Bootloader程序还是跳转到APP区。

IAP需要解决的两大问题:

1、Flash操作,包括Flash区域的划分(确定Blootloader程序大小,APP程序大小,设定从哪个地址开始为APP程序地址)、指定Flash区域的操作,包括写入或者擦除。

2、数据传输,选择串口或者网络等方式烧写。

第一步:学习官方源代码

STM32官网上有IAP的实例源代码,用兴趣的朋友可以下载来学习借鉴。单纯的看源代码会使人发晕。但是它提供了相对完整的IAP解决方案。

接下来的步骤是为了让我们更好地掌握IAP技术

第二步:了解STM32芯片基本硬件参数

STM32官网上有关于STM32 系列芯片的选型,其中也提到各类型号的命名规则:

我们这里关心的是闪存存储器的容量。这里要注意Bootloader+APP 的程序大小不要超过闪存存储区的容量.

第三步、搞清除STM32内置Flash

在只有一个程序的情况下,是直接加载到Flash区的。

当设计Bootloader程序后:

第四步、 STM32程序运行机制

  • STM32Fx有一个中断向量表,这个中断向量表存放代码开始部分的后4个字节处(即0x08000004),代码开始的4个字节存放的是栈顶地址。

  • 当发生中断后程序通过查找该表得到相应的中断服务程序入口,然后跳到相应的中断服务程序中执行。

  • 上电后从0x08000004处取出复位中断向量的地址,然后跳转到复位中断程序入口(标号1所示),执行结束后跳转到main函数。

  • 在执行main函数的过程中发生中断,则STM32强制将PC指针指回中断向量表处(标号3所示),从中断向量表中找到相应的中断函数入口地址,跳转到相应的中断服务函数,执行完中断服务函数后再返回到main函数中来。

IAP代码实现:

IAP的基础是Flash的操作。

1、实现flash写入,删除,修改。

STM32 官方自带Flash操作的函数文件 stm32f10x_flash.c, 我们只需要在该基础上进一步封装即可。

Flash操作中,STM32只允许页操作。这里需要搞清楚的是一页是2k还是1K的大小。(详细可以查看MCU的datasheet技术文档)

部分关键代码如下:


```c
//从指定地址开始写入指定长度的数据
//WriteAddr:起始地址(此地址必须为2的倍数!!)
//pBuffer:数据指针
//NumToWrite:半字(16位)数(就是要写入的16位数据的个数.)
#if STM32_FLASH_SIZE<256
#define STM_SECTOR_SIZE 1024 //字节
#else
#define STM_SECTOR_SIZE 2048
#endif
u16 STMFLASH_BUF[STM_SECTOR_SIZE/2];//最多是2K字节
void STMFLASH_Write(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite)
{u32 secpos;       //扇区地址u16 secoff;       //扇区内偏移地址(16位字计算)u16 secremain; //扇区内剩余地址(16位字计算)       u16 i;    u32 offaddr;   //去掉0X08000000后的地址if(WriteAddr<STM32_FLASH_BASE||(WriteAddr>=(STM32_FLASH_BASE+1024*STM32_FLASH_SIZE)))return;//非法地址FLASH_Unlock();                     //解锁offaddr=WriteAddr-STM32_FLASH_BASE;        //实际偏移地址.secpos=offaddr/STM_SECTOR_SIZE;           //扇区地址  0~127 for STM32F103RBT6secoff=(offaddr%STM_SECTOR_SIZE)/2;     //在扇区内的偏移(2个字节为基本单位.)secremain=STM_SECTOR_SIZE/2-secoff;       //扇区剩余空间大小   if(NumToWrite<=secremain)secremain=NumToWrite;//不大于该扇区范围while(1) {    STMFLASH_Read(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//读出整个扇区的内容for(i=0;i<secremain;i++)//校验数据{if(STMFLASH_BUF[secoff+i]!=0XFFFF)break;//需要擦除       }if(i<secremain)//需要擦除{FLASH_ErasePage(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE);//擦除这个扇区for(i=0;i<secremain;i++)//复制{STMFLASH_BUF[i+secoff]=pBuffer[i];     }STMFLASH_Write_NoCheck(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//写入整个扇区  }else STMFLASH_Write_NoCheck(WriteAddr,pBuffer,secremain);//写已经擦除了的,直接写入扇区剩余区间.                   if(NumToWrite==secremain)break;//写入结束了else//写入未结束{secpos++;              //扇区地址增1secoff=0;              //偏移位置为0     pBuffer+=secremain;      //指针偏移WriteAddr+=secremain;   //写地址偏移    NumToWrite-=secremain;  //字节(16位)数递减if(NumToWrite>(STM_SECTOR_SIZE/2))secremain=STM_SECTOR_SIZE/2;//下一个扇区还是写不完else secremain=NumToWrite;//下一个扇区可以写完了}   }; FLASH_Lock();//上锁
}
#endif
//从指定地址开始读出指定长度的数据
//ReadAddr:起始地址
//pBuffer:数据指针
//NumToWrite:半字(16位)数
void STMFLASH_Read(u32 ReadAddr,u16 *pBuffer,u16 NumToRead)
{u16 i;for(i=0;i<NumToRead;i++){pBuffer[i]=STMFLASH_ReadHalfWord(ReadAddr);//读取2个字节.ReadAddr+=2;//偏移2个字节.  }
}

软件开发的重要的一项内容在于调试,调试可以让思路进一步清晰明确。

我们调试下该Flash的写入函数是否正确。

在程序中添加如下代码:

     uint16 testbuff[2]= {0x11,0x22}        STMFLASH_Write(0x8008000,testbuff,2);


Keil 中断点查看,指定地址已经写入数据。

接着,我们进一步封装,该函数用于判断按是否按照1K写入:

void iap_write_appbin(u32 appxaddr,u8 *appbuf,u32 appsize)
{u16 t;u16 i=0;u16 temp;u32 fwaddr=appxaddr;//当前写入的地址u8 *dfu=appbuf;for(t=0;t<appsize;t+=2){                           temp=(u16)dfu[1]<<8;temp+=(u16)dfu[0];   dfu+=2;//偏移2个字节iapbuf[i++]=temp;     if(i==512){i=0;STMFLASH_Write(fwaddr,iapbuf,512);    delay_ms(100);fwaddr+=1024;//偏移2048  16=2*8.所以要乘以2.}}if(i){STMFLASH_Write(fwaddr,iapbuf,i);//将最后的一些内容字节写进去.  delay_ms(100);}
}

这里我们进一步可以测试封装后Flash 写入函数是否正确。这里留给读者自行去测试。

2、IAP 通信协议设计

FLash 操作函数搞定之后,接下来就是搞定通信的驱动。这里可以是蓝牙、WIFI 或者有线网络TCP/IP,串口通信等。

针对通信驱动的实现,这里不做展开。我们只针对通信协议部分的探讨。

通信协议可以自定义,也可以使用Y-moden 协议。

这里有一篇文章写得非常详细。
Ymodem协议介绍

我们这里来自行设计一套最简易的协议。不包含起始数据帧和CRC,我们只对控制流进行设计:

服务端采用C#代码编写:界面设计如下:

上位机关键代码:

     private void button2_Click(object sender, EventArgs e)                      //选择固件按钮{OpenFileDialog openfile = new OpenFileDialog();                         //打开文件控件try{openfile.ShowDialog();                                                  //显示打开文件对话框txt_filename.Text = openfile.FileName;                                  //获取所选择固件的名称fs = new FileStream(openfile.FileName, FileMode.Open);                  //获取文件流str = "文件共" + fs.Length.ToString() + "字节" + "\n";                      //获取文件的总字节数textBox1.AppendText(str);                                               //显示文件的总字节数packet_zheng = (int)fs.Length / 1024;                                   //获取文件的整K字节数packet_yu =(int) fs.Length % 1024;                                      //获取不足1K的剩余字节数btn_open.Enabled = false;                                               //禁用选择固件文件按钮btn_send.Enabled = true; }catch (Exception ex){Console.WriteLine(ex.ToString());MessageBox.Show("请重新选择固件");}//使能发送/下载数据按钮}private void send_length()                                                  //发送固件的长度数据{datatosend[0] = 0xb1;datatosend[1] = 0xb2;datatosend[2] = 0xb3;datatosend[3] = (byte)(packet_zheng / 256);                             //获取整K字节数的高八位datatosend[4] = (byte)(packet_zheng % 256);                             //获取整K字节数的低八位datatosend[5] = (byte)(packet_yu / 256);                                //获取不足1K字节数的高八位datatosend[6] = (byte)(packet_yu % 256);                                //获取不足1K字节数的低八位datatosend[7] = 0x1b;datatosend[8] = 0x2b;datatosend[9] = 0x3b;mystream.Write(datatosend, 0, 10);}private void send_data_zheng()                                              //发送整K字节数据{fs.Read(datatosend, 0, 1024);mystream.Write(datatosend, 0, 1024);}private void send_data_yu()                                                //发送不足1K字节数据{fs.Read(datatosend, 0, packet_yu);mystream. Write(datatosend, 0, packet_yu);}private void btn_reset_Click(object sender, EventArgs e)                  //发送进入BootLoader命令{   if (myclient.Connected == false){myclient.Connect(txtip.Text, 1550);}datatosend[0] = 0xe1;datatosend[1] = 0xe2;datatosend[2] = 0xe3;datatosend[3] = 0x1e;datatosend[4] = 0x2e;datatosend[5] = 0x0d;datatosend[6] = 0x0a;mystream .Write(datatosend, 0, 7);}private void receive_data()                         //数据接收线程{while (myclient.Connected)                    {if (mystream.CanRead)                            //当有数据可读时{int len = (int)myclient.ReceiveBufferSize;   //获取数据的长度datarec = new byte[len];                     //定义数据缓冲数组mystream.Read(datarec, 0, len);              //读取数据到数组if (len >= 5){if (datarec[0] == 0xf1 & datarec[1] == 0xf2 & datarec[2] == 0xf3 & datarec[3] == 0x1f & datarec[4] == 0x2f)          //判断是否已经连接到终端{str = "已连接到设备" + "\n";  textBox1.AppendText(str);       //显示状态信息btn_open.Enabled = true;btn_boot.Enabled = true;}else if (datarec[0] == 0xa1 & datarec[1] == 0xa2 & datarec[2] == 0xa3 & datarec[3] == 0x2a & datarec[4] == 0x1a)     //判断终端是否收到连接信息{str = "客户端已确认下载信息" + "\n";textBox1.AppendText(str);      //显示状态信息send_length();                 //发送长度数据}else if (datarec[0] == 0xb1 & datarec[1] == 0xb2 & datarec[2] == 0xb3 & datarec[3] == 0x2b & datarec[4] == 0x1b)     //判断终端是否接受到长度消息{str = "客户端接收长度信息完毕,即将开始下载数据" + "\n";textBox1.AppendText(str);      //显示状态信息send_data_zheng();             //发送整K字节数据}else if (datarec[0] == 0xc1 & datarec[1] == 0xc2 & datarec[2] == 0xc3 & datarec[3] == 0x2c & datarec[4] == 0x1c)     //判断终端1K字节数据是否接受完毕{packet_send++;str = "已下载" + packet_send.ToString() + "K" + "\n";     //显示已经发送的字节数textBox1.AppendText(str);if (packet_send < packet_zheng)                           //若整K字节数未发送完{send_data_zheng();                                    //发送整K字节数据}else if (packet_send == packet_zheng)                     //若整K字节数据已发送完{send_data_yu();                                       //发送余下不足1K的数据}}else if (datarec[0] == 0xd1 & datarec[1] == 0xd2 & datarec[2] == 0xd3 & datarec[3] == 0x2d & datarec[4] == 0x1d)     //判断终端是否完全接受完数据{str = "程序下载完毕" + "\n";textBox1.AppendText(str);fs.Flush();                             //释放流资源fs.Dispose();packet_send = 0;                        //发送整K字节计数清零myclient.Close();                       //释放TcpClient资源myclient = null;mystream.Flush();                       //释放网络流资源btn_connect.Enabled = true;             //使能连接按钮btn_disconnect.Enabled = false;         //禁用断开连接按钮btn_open.Enabled = false;               //禁用选择数据按钮btn_boot.Enabled = false;               //禁用进入BootLoader按钮btn_send.Enabled=false;                 //禁用发送/下载数据按钮thread_recdata.Abort();                 //终端接收数据线程 }else if (datarec[0] == 0xe1 & datarec[1] == 0xe2 & datarec[2] == 0xe3 & datarec[3] == 0x1e & datarec[4] == 0x2e)    //判断终端是否成功进入BootLoader{str = "进入bootloader成功" + "\n";textBox1.AppendText(str);myclient.Close();                      //释放TcpClient资源myclient = null;mystream.Flush();                      //释放网络流资源btn_connect.Enabled = true;            //使能连接按钮btn_disconnect.Enabled = false;        //禁用断开连接按钮btn_open.Enabled = false;              //禁用打开按钮btn_boot.Enabled = false;              //禁用进入BootLoader按钮btn_send.Enabled = false;              //禁用发送/下载按钮thread_recdata.Abort();        //终止数据接收线程}else if (datarec[0] == 0xaa & datarec[1] == 0xbb & datarec[2] == 0xcc & datarec[3] == 0xdd & datarec[4] == 0xee)    //判断终端是否成功进入BootLoader{MessageBox.Show("已经进入BootLoader");}}}        }   }

MCU部分关键代码:

#define CONNECT_CMD1 0x44
#define CONNECT_CMD2 0x4D
#define CONNECT_CMD3 0x46
#define BINLEN_CMD1  0XB1
#define BINLEN_CMD2  0XB2
#define BINLEN_CMD3  0XB3
#define BINLEN_CMD4  0X1B
#define BINLEN_CMD5  0X2B
#define BINLEN_CMD6  0X3B#define BINLEN_CMD6  0X3B
Pocess_Socket_Data(SOCKET s)                     //Socket 接受数据的处理
{   if(bootsta==0)                                   //判断是否为初始状态                               {if(Rx_Buffer[0]==CONNECT_CMD1){delay_ms(1);if(Rx_Buffer[1]==CONNECT_CMD2&&Rx_Buffer[2]==CONNECT_CMD3)//接收到下载连接指令{bootsta=1;          //进入接受长度数据状态Tx_Buffer[0]=0XA1; Tx_Buffer[1]=0XA2; Tx_Buffer[2]=0XA3; Tx_Buffer[3]=0X2A;  Tx_Buffer[4]=0X1A; send(SOCK_TCPS,Tx_Buffer,5);//Write_SOCK_Data_Buffer(s, Tx_Buffer,5,S0_Port);              //向主机发送已接受到下载指令}}}else if(bootsta==1)                                       //判断是否为接受长度信息状态{if(Rx_Buffer[0]==BINLEN_CMD1){/*上位机将BIN程序,分成1K的单位,每次最多发送1K字节,下位机接受后将1K字节写入FlashRx_Buffer[3]为1K个数的高八位,Rx_Buffer[4]为1K个数的低八位。packet_zheng为程序的1K字节个数Rx_Buffer[5]不足1K字节个数的高八位,Rx_Buffer[6]为不足1K字节个数的低八位。packet_yu为不足1K字节的个数*/delay_ms(1);if(Rx_Buffer[1]==BINLEN_CMD2&&Rx_Buffer[2]==BINLEN_CMD3&&Rx_Buffer[7]==BINLEN_CMD4&&Rx_Buffer[8]==BINLEN_CMD5&&Rx_Buffer[9]==BINLEN_CMD6)//读取BIN数据长度{packet_zheng=Rx_Buffer[3]*256+Rx_Buffer[4];packet_yu=Rx_Buffer[5]*256+Rx_Buffer[6];bootsta=2;                                               //进入接受程序数据状态packet_rev=0;Tx_Buffer[0]=0XB1; Tx_Buffer[1]=0XB2; Tx_Buffer[2]=0XB3; Tx_Buffer[3]=0X2B;  Tx_Buffer[4]=0X1B;                      //发送确认接受完毕长度信息命令send(SOCK_TCPS,Tx_Buffer,5);//Write_SOCK_Data_Buffer(s, Tx_Buffer,5,S0_Port);           //发送确认接受完毕长度信息命令}}}else if(bootsta==2)                                                                    //判断是否为接受程序数据状态{if(packet_rev<packet_zheng)                                                          //判断是否为整K字节接受状态{if(size>=1024){iap_write_appbin(FLASH_APP1_ADDR+packet_rev*1024,Rx_Buffer,1024);       //向指定地址写入1K字节的数据packet_rev++;                                                             //接受到的整K字节数加1Tx_Buffer[0]=0XC1; Tx_Buffer[1]=0XC2; Tx_Buffer[2]=0XC3; Tx_Buffer[3]=0X2C;  Tx_Buffer[4]=0X1C;send(SOCK_TCPS,Tx_Buffer,5);//   Write_SOCK_Data_Buffer(s, Tx_Buffer,5,S0_Port);                           //向主机发送确认接收完毕1K字节命令}}else{if(size>=packet_yu)                                                          //发送余下不足1K字节的数据               {iap_write_appbin(FLASH_APP1_ADDR+packet_zheng*1024,Rx_Buffer,size);         //向指定地址写入不足1k字节的数据if(((*(vu32*)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000)               //判断是否为0X08XXXXXX.{   Tx_Buffer[0]=0XD1; Tx_Buffer[1]=0XD2; Tx_Buffer[2]=0XD3; Tx_Buffer[3]=0X2D;  Tx_Buffer[4]=0X1D;send(SOCK_TCPS,Tx_Buffer,5);//Write_SOCK_Data_Buffer(s, Tx_Buffer,5,S0_Port);                       //向主机发送接收完毕全部数据命令iap_load_app(FLASH_APP1_ADDR);                                       //执行FLASH APP代码}else{bootsta=0;                                                          //恢复初始状态}}}}if(Rx_Buffer[0]==0xe1&Rx_Buffer[1]==0xe2&Rx_Buffer[2]==0xe3&Rx_Buffer[3]==0x1e&Rx_Buffer[4]==0x2e)       //判断是否为进入BootLoader命令{Tx_Buffer[0]=0xaa; Tx_Buffer[1]=0xbb; Tx_Buffer[2]=0xcc; Tx_Buffer[3]=0xdd; Tx_Buffer[4]=0xee;send(SOCK_TCPS,Tx_Buffer,5);//Write_SOCK_Data_Buffer(s, Tx_Buffer,5,S0_Port);                                                      //向主机发送确认接收到进入BootLoader命令}
}

通信数据流的控制设计重点在于重点环节的命令设计。一般我们都会设计带CRC校验的通信协议设计。上面的控制流比较简单,但是可靠性不高。

在一些保密行业,还会对数据区进行DES加密等等。

3、APP代码部分修改

1、起始地址修改:

   FLASH_APP1_ADDR       0x08005000     SIZE: 0x1FB000      VECT_TAB_OFFSET      0x5000

2、bin文件生成

第一步:打开Options for Target ‘target 1’对话框,选择User标签页;

第二步:找到fromelf.exe的路径(keil5在ARMCC里)

3、跳转进入Bootloader,并且改写相应的标志位。

void Process_Socket_Data(SOCKET s)
{           if(Rx_Buffer[0]==0xe1&Rx_Buffer[1]==0xe2&Rx_Buffer[2]==0xe3&Rx_Buffer[3]==0x1e&Rx_Buffer[4]==0x2e){printf("go into Bootloader........!")    ;BKP_WriteBackupRegister(BKP_DR1, 0xABCD);  //改写标志位send(SOCK_TCPS,Rx_Buffer,5);//Write_SOCK_Data_Buffer(s, Rx_Buffer,5,S0_Port);SCB->AIRCR =0X05FA0000|(u32)0x04;            //复位重启}
}

总结:

IAP技术的实现,重点要搞清楚Flash的操作,以及通信控制流的设计。抓住这里两个要点,其他的迎刃而解。

基于STM32系列芯片的 IAP实现的探索相关推荐

  1. STM32系列芯片命名规则——简明

    DIRECTORY STM32系列芯片命名规则 1.产品系列: 2.产品类型: 3.产品子系列: 4.管脚数: 5.Flash存储容量: 6.封装: 7.温度范围: STM32系列芯片命名规则 例图: ...

  2. 【STM32F103ZE】TOF250(TTL)基于STM32系列开发板的运用

    目录 @[TOC](目录) 一.前言 二.硬件准备 二.软件准备 三.硬件接线图 四.例程源码 五.烧录说明 5.1 烧录接线示意图 5.2 烧录动态图 六.结果输出 一.前言 此片文章主要介绍如果通 ...

  3. linux嵌入式开发arm7,基于ARM7系列芯片嵌入式平台上实现的设计方案-嵌入式系统-与非网...

    本文介绍的方法是在用ARM7系列芯片S3C4510B和μClinux构建的嵌入式平台上实现的.在嵌入式系统设计过程中,系统的掉电保护越来越受到重视整个掉电保护实现的基本思路是:产生掉电信号,捕捉掉电信 ...

  4. 基于LT8668系列芯片的拼接方案

    LT8668系列芯片是一颗高性能的LCD Controller显示控制器芯片,它可配置输入DP1.4/HDMI2.1   , Type-C/DP1.4/HDMI2.1,audio in,配置输出LVD ...

  5. STM32系列芯片名称定义

    每种STM32的产品都由16个字母或数字构成的编号标示,用户向ST订货时必须使用这个编号指定需要的产品.这16个字符分为8个部分,下面通过一个例子说明它们的意义: STM32 F 103 C 6 T ...

  6. STM32系列芯片命名含义一览

    STM32 命名规则 STM32 F 103 R C T 6 ① ② ③ ④ ⑤ ⑥ ⑦ ①:产品系列名 STM32 代表ST品牌 Cortex-Mx 系列内核(ARM)的32位MCU: ②:产品类型 ...

  7. 适合学习的基于stm32系列--按键控制心形红绿流水灯的转换

    一.硬件设计 1,按键电路 在这次设计中,用到的按键只有WK-UP和KEY2两个按键,按下WK-UP按键红灯闪烁,按下KEY2按键绿灯闪烁. WK_UP电路采用的是下拉模式,常态下是低电平,当按键按下 ...

  8. 基于 STM32 空气质量检测装置设计

    一.毕业设计的背景和依据 现在,人们大约一半的时间是在室内度过的,室内空气质量与我们每个人的工作和生活 都息息相关,因此对生活环境的空气质量提出了更高的要求.针对雾霾.室内装修等污染问 题,人们还没有 ...

  9. 基于STM32的高精度温度测控系统-原理图设计

    基于STM32的高精度温度测控系统,本篇为原理图设计分析篇 高精度温度测控仪设计原理图篇(已更新) 高精度温度测控仪设计PCB篇(已更新) 高精度温度测控仪设计STM32代码篇(未更新) 高精度温度测 ...

最新文章

  1. 深入浅出SharePoint——取消Workflow实例
  2. hive防止数据误删
  3. 光流 | OpenCV实现简单的optical flow(代码类)
  4. 同步模式下的端口映射程序
  5. 程序员偷偷深爱的 9 个不良编程习惯
  6. 程序员面试金典 - 面试题 04.04. 检查平衡性(二叉树高度)
  7. ACE主动对象模式学习
  8. HDU 3551 Hard Problem
  9. vmware 官方下载
  10. 删除服务列表中的Tomcat服务?(或删除服务列表中的任意服务)
  11. Sublime LiveReload
  12. 算法学习笔记(使用追赶法解三对角方程组)
  13. X光,CT扫描,核磁共振的区别
  14. 如何下载网页上的视频和flash的方法
  15. CNN-PS: CNN-Based Photometric Stereo for General Non-convex Surfaces 2018ECCV
  16. go语言的iota是什么意思_golang 使用 iota
  17. 【程序设计】浅拷贝与深拷贝
  18. php 实现二叉树的最大深度_PHP实现二叉树的深度优先遍历(前序、中序、后序)和广度优先遍历(层次)...
  19. 二次规划算法学习笔记
  20. linux串口文件传输助手怎么用,SerialTool: SerialTool是一个实用的串口调试工具,这款工具支持串口调试助手、波形显示和文件传输等功能...

热门文章

  1. 《Neural network and deep learning》学习笔记(一)
  2. [云炬创业基础笔记]第五章创业机会评估测试1
  3. 科大星云诗社动态20210315
  4. 常用3种数据库的Sql分页
  5. 解决IE正常模式与兼容性模式的办法
  6. c#开发中程序集调用时容易忽略的问题
  7. go语言goroutine的取消
  8. 不使用sprintf函数使用共用体进行STM32单片机通讯解析
  9. Java1.使用二分搜索算法查找任意N个有序数列中的指定元素。 2.通过上机实验进行算法实现。 3.保存和打印出程序的运行结果,并结合程序进行分析,上交实验报告。 4.至少使用两种方法进行编程,直接查
  10. Python学习之函数返回多个值