STM32IAP学习笔记

单片机不同的程序下载方式

ICP

        ICP是指在电路中编程。使用厂家配套的软件或仿真器进行程序烧录,目前主流的有JTAG接口和SWD接口,常用的烧录工具为J-Link、ST-Link等。在程序开发阶段,通常在连接下载器的情况下直接使用编程软件进行程序下载调试。在MDK软件中可以选择不同的下载器。一般用于调试阶段,实际产品中的电路板一般会密封在外壳中,不便于用下载器烧录程序。

ISP

        在系统中编程。以STM32为例,其内置了一段Bootloader程序,可以通过更改BOOT引脚电平来运行这段程序,再通过ISP编程工具(flymcu通过串口烧录程序)将程序下载进去。下载完毕之后,再更改BOOT至正常状态,使得MCU运行所下载的程序。这种方法需要更改boot模式,实际工程中也不方便。

IAP

        在应用中编程。IAP可以使用微控制器支持的任一种通信接口(如I/O端口、USB、CAN、UART、I2C、SPI等)下载程序或数据到FLASH中。IAP允许用户在程序运行时重新烧写FLASH中的内容。但需要注意,IAP要求至少有一部分程序(Bootloader)已经使用ICP或ISP烧到FLASH中。一般情况下,产品的外壳都会留有通信接口,若能通过这种通信方式对程序进行升级,则可以省去拆装的麻烦。在此基础上,若引入远距离或无线数据传输方案,更可以实现远程编程或无线编程。 ​

        通常实现 IAP 功能时,需要在设计固件程序时编写两个项目代码,第一个项目程序(bootloader)不执行正常的功能操作,而只是通过某种通信方式(如 USB、USART)接收程序或数据,执行对第二部分代码的更新;第二个项目代码(APP)才是真正的功能代码。这两部分项目代码都同时烧录在 User Flash 中,当芯片上电后,首先是bootloader开始运行,它作如下操作:

​①检查是否需要对APP代码进行更新 ​

②如果不需要更新则转到④ ​

③执行更新操作 ​

④跳转到APP执行 ​

        bootloader必须通过其它手段,如 JTAG 或 ISP 烧入;APP代码可以使用bootloader的 IAP 功能烧入,也可以和boot loader一起烧入,以后需要程序更新时再通过boot loader更新。 ​他们存放在 STM32 FLASH 的不同地址范围,一般从最低地址区开始存放 Bootloader(0x08000000)紧跟其后的就是 APP 程序

        判断是否需要更新APP的方法:

1.通过拨码开关、跳线帽等方式设定单片机某一引脚电平状态,程序通过读取引脚电平判断是否需要升级。此种方式需要接触板卡进行操作,当板卡被封闭在外壳中或安装于不便于操作位置时很难实现。

2.软件内设定一标志位(变量),通过判断标志位状态判断是否需要升级。该标志位状态掉电不能改变,故需要存储在外部EEPROM或单片机内部FLASH中。若存储在外部EEPROM,则需要增加额外的电路;若存储在单片机内部FLASH,由于FLASH每次写入都需要擦除一整页,会造成资源浪费。

3.单片机每次上电首先进入BootLoader程序,在BootLoader中等待一定时间,若上位机软件在该时间段内发起通讯,则停留在Bootloader程序中等待固件升级;若该时间段内无通讯,则跳转到正常的APP程序。该方式每次上电都要等待一定时间,需要考虑是否可以接受。

STM32程序运行流程

正常程序运行流程

        STM32 的内部闪存(FLASH)地址起始于 0x08000000,一般情况下,程序文件就从此地址开始写入。此外 STM32 是基于 Cortex-M3 内核的微控制器,其内部通过一张“中断向量表”来响应中断,STM32 在复位后,先从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,如图标号①所示;在复位中断服务程序执行完之后,会跳转到我们的main 函数,如图标号②所示;而我们的 main 函数一般都是一个死循环,在 main 函数执行过程中,如果收到中断请求,此时 STM32 强制将 PC 指针指回中断向量表处,如图标号③所示;然后,根据中断源进入相应的中断服务程序,如图标号④所示;在执行完中断服务程序以后,程序再次返回 main 函数执行,如图标号⑤所示。

 

加入IAP后程序运行流程

        STM32 复位后,还是从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到bootloader的 main 函数,如图标号①所示,此部分同上图 一样;在执行完boot loader以后(即将新的 APP 代码写入 STM32的 FLASH,新程序的复位中断向量起始地址为 0X08000004+N+M),跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的 main 函数,如图标号②和③所示,同样 main 函数为一个死循环,并且注意到此时 STM32 的 FLASH,在不同位置上,共有两个中断向量表。在 main 函数执行过程中,如果 CPU 得到一个中断请求,PC 指针仍强制跳转到地址0X08000004 中断向量表处,而不是新程序的中断向量表,如图标号④所示;程序再根据我们设置的中断向量表偏移量,跳转到对应中断源新的中断服务程序中,如图标号⑤所示;在执行完中断服务程序后,程序返回 main 函数继续运行,如图标号⑥所示。通过以上两个过程的分析,我们知道 IAP 程序必须满足两个要求:

①新程序必须在 IAP 程序之后的某个偏移量为 x 的地址开始;

②必须将新程序的中断向量表相应的移动,移动的偏移量为 x;

不同型号STM32的FLASH大小

        BootLoader程序和APP 程序存放在 STM32 FLASH 的不同地址范围,一般从最低地址区开始存放 BootLoader,紧跟其后的就是 APP 程序。在进行FLASH空间划分之前,首先需要了解一下不同型号STM32单片机的FLASH大小。下图中的闪存容量就是FLASH的大小。

 

        对于不同容量的STM32F1系列产品,其FLASH页大小是不同的,具体的容量划分规则如下: ​ 小容量产品:FLASH容量在16K至32K字节之间。

中容量产品:FLASH容量在64K至128K字节之间。 ​

大容量产品:FLASH容量在256K至512K字 节之间。

对于小容量和中容量的产品,其页大小为1K,对于大容量产品,其页大小为2K

        用户程序写在flash的主存储器中,对于小容量产品来说,FLASH每页1K,最多32页,也就是32K。 对于中容量产品来说,FLASH每页1K,最多128页,也就是128K。对于大容量产品来说,FLASH每页2K,最多256页,也就是512K,大容量产品的flash模块组织如图:

 keil中配置bootloader和APP

配置boot loader

        在进行FLASH空间划分时,必须知道编写的程序占用FLASH空间大小。用MDK软件进行工程编译之后会生成一个.map文件,在该文件末尾可找到程序需要占用的FLASH空间。在实际设计过程中,主要是确定boot loader的占用空间,在FLASH起始位置给Bootloader留出足够的空间,以便确定APP的起始地址,也就是中断向量表的偏移量。在实际分配过程中,可以给Bootloader多一些空间,以便后续对Bootloader的功能拓展。

         在MDK软件配置项中,可以对程序的起始位置以及大小进行设置。对于BootLoader程序来说,只需要设置其Size,该值可根据刚才map文件中的值进行预估。0x08000000就是FLASH的起始地址,这里设置bootloader的大小为0x4000,也就是16k

 配置APP

        BootLoader程序按照正常的程序编写即可。而APP程序由于其下载位置与默认程序下载位置不同,故需要做一些特殊的配置。首先是APP 程序起始地址设置。起始位置即去除BootLoader程序之后剩余空间的首地址。一般设定为某一页的首地址,因为FLASH写入之前必须进行页擦除,在下图中按照Bootloader的size可得APP首地址为0x08004000。Size <= FLASH原始大小 - 偏移量(0x4000),例子中是STM32F103C8T6,FLASH大小为64K。上面分给Bootloader的大小为0x4000,也就是16K,那么size最大就是48k,也就是0xc000。如下图所示。

 接着设置中断向量表的偏移量,在主函数起始位置(也就是main函数的第一句话)添加:

SCB->VTOR = FLASH_BASE | 0x4000; //0x4000即BootLoader大小(偏移量)

bin文件生成

        IAP过程中传输的数据文件一般为后缀名为bin的文件,该文件内容与正常烧录进FLASH中的数据内容一致,便于程序升级。但是MDK软件并不能直接生成bin文件,需要进行一些配置。这里我们通过 MDK 自带的格式转换工具 fromelf.exe,来实现.axf 文件到.bin 文件的转换。该工具在 MDK 的安装目录\ARM\BIN40 文件夹里面。我们通过在 MDK 点击 Options for Target→User 选项卡,在 After Build/Rebuild 栏,勾选 Run #1,并写入:D:\MDK5.21A\ARM\ARMCC\bin\fromelf.exe --bin -o ..\OBJ\TEST.bin ..\OBJ\TEST.axf,如下图所示。加粗部分根据实际MDK安装路径进行修改,TEST.bin和TEST.axf中的TEST根据工程名称修改。

         除了上述方法生成bin文件还可以在网上下载hex2bin等软件来生成bin文件。

代码解析

        以正点原子的串口IAP实验为例,该实验通过串口接收APP的bin文件,通过按键控制固件更新和APP代码执行。

bootloader代码

串口接收部分
#define USART_REC_LEN  			41*1024 	//定义最大接收字节数 41K
u8 USART_RX_BUF[USART_REC_LEN] __attribute__ ((at(0X20001000)));//接收缓冲,最大USART_REC_LEN个字节,起始地址为0X20001000. void uart_init(u32 bound){//GPIO端口设置GPIO_InitTypeDef GPIO_InitStructure;USART_InitTypeDef USART_InitStructure;NVIC_InitTypeDef NVIC_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE);	//使能USART1,GPIOA时钟//USART1_TX   GPIOA.9GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	//复用推挽输出GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.9//USART1_RX	  GPIOA.10初始化GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.10  //Usart1 NVIC 配置NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;		//子优先级3NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//IRQ通道使能NVIC_Init(&NVIC_InitStructure);	//根据指定的参数初始化VIC寄存器//USART 初始化设置USART_InitStructure.USART_BaudRate = bound;//串口波特率USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;	//收发模式USART_Init(USART1, &USART_InitStructure); //初始化串口1USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接受中断USART_Cmd(USART1, ENABLE);                    //使能串口1 }void USART1_IRQHandler(void)                	//串口1中断服务程序{u8 Res;
#if SYSTEM_SUPPORT_OS 		//如果SYSTEM_SUPPORT_OS为真,则需要支持OS.OSIntEnter();    
#endifif(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  //接收中断(接收到的数据必须是0x0d 0x0a结尾){Res =USART_ReceiveData(USART1);//(USART1->DR);	//读取接收到的数据if(USART_RX_CNT<USART_REC_LEN){USART_RX_BUF[USART_RX_CNT]=Res;//将读取到的数据存到缓冲区USART_RX_CNT++;			 									     } 		 } 
main函数
#define FLASH_APP1_ADDR		0x08004000  	//第一个应用程序起始地址(存放在FLASH)
int main(void){ u8 t;u8 key;u16 oldcount=0;	//老的串口接收数据值u16 applenth=0;	//接收到的app代码长度NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);// 设置中断优先级分组2delay_init();	    	 //延时函数初始化	  uart_init(115200);	 	//串口初始化为256000LED_Init();		  		//初始化与LED连接的硬件接口KEY_Init();				//按键初始化 	  while(1){if(USART_RX_CNT)//如果串口接收到的数据不为空{if(oldcount==USART_RX_CNT)//新周期内,没有收到任何数据,认为本次数据接收完成.{applenth=USART_RX_CNT;//得到数据长度oldcount=0;USART_RX_CNT=0;printf("用户程序接收完成!\r\n");printf("代码长度:%dBytes\r\n",applenth);}else oldcount=USART_RX_CNT;			}	  	 key=KEY_Scan(0);//按键扫描if(key==WKUP_PRES)			//WK_UP按键按下{if(applenth)//如果接收到的数据不为空{printf("开始更新固件...\r\n");	if(((*(vu32*)(0X20001000+4))&0xFF000000)==0x08000000)//判断是否为0X08XXXXXX.{	 iap_write_appbin(FLASH_APP1_ADDR,USART_RX_BUF,applenth);//更新FLASH代码   printf("固件更新完成!\r\n");}else {	   printf("非FLASH应用程序!\r\n");}}else {printf("没有可以更新的固件!\r\n");}									 } if(key==KEY1_PRES)//KEY1按下{printf("开始执行FLASH用户代码!!\r\n");if(((*(vu32*)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000)//判断是否为0X08XXXXXX.{	 iap_load_app(FLASH_APP1_ADDR);//执行FLASH APP代码}else {printf("非FLASH应用程序,无法执行!\r\n");   }									   }}   	   
}
 检查地址合法性解析
if(((*(vu32*)(0X20001000+4))&0xFF000000)==0x08000000)
//0X20001000是串口接收数据缓冲区的起始地址,接收的数据是APP,0X20001000处存放的是栈顶地址,0X20001000 + 4处存放的是中断向量表起始地址,也就是复位中断服务函数的地址,对其进行强制类型转换,然后解引用,得到复位中断服务函数的地址,判断地址是不是在flash范围内
if(((*(vu32*)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000)
//上面将串口缓冲区接收到的数据存到了flash中,FLASH_APP1_ADDR处存放的东西和上面0X20001000存放的东西是一样的
跳转到APP代码解析
typedef  void (*iapfun)(void);				//重命名一个函数指针类型.
iapfun     jump2app;                             //创建一个函数指针jump2app
//跳转到应用程序段
//appxaddr:用户代码起始地址.
void iap_load_app(u32 appxaddr)
{if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000)	//检查栈顶地址是否合法.{ jump2app=(iapfun)*(vu32*)(appxaddr+4);		//用户代码区第二个字为程序开始地址(复位地址)		MSR_MSP(*(vu32*)appxaddr);					//初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)jump2app();									//跳转到APP.}
}		 
检查栈顶地址解析

        STM32的RAM起始地址为0x20000000,上述代码中appxaddr处存放的是栈顶地址,栈顶地址在RAM中,appxaddr+4处存放的是复位中断向量,在FLASH中

        appxaddr是存放栈顶地址的指针,所以对appxaddr解引用就得到了栈顶地址。由于栈顶地址应该在RAM中,

        1.对于128kRAM的芯片,堆栈地址的空间是0x20000000 - 0x2001FFFF,地址与上0x2FFE0000,就是得到bit17~bit27位,只需判断这些位,如果有1,与的结果就不等于0x20000000,就表示地址不合法,低十七位不需要判断。

        2.对于48KRAM的芯片,堆栈地址空间是0x20000000 - 0x2000BFFF,地址应该与上0x2FFFC000,就是得到bit14~bit27位,只需判断这些位,如果有1,与的结果就不等于0x20000000,就表示地址不合法,低十四位不需要判断。

        3.对于512KRAM的芯片,堆栈地址空间是0x20000000 - 0x2007FFFF,地址应该与上0x2FF80000,就是得到bit19~bit27位,只需判断这些位,如果有1,与的结果就不等于0x20000000,就表示地址不合法,低十九位不需要判断。

        ST32最早的时候内部ram的大小基本在128Kb以内,RAM小于128K的芯片使用0x2FFE0000也基本没问题,就一直在沿用。

APP跳转解析
jump2app=(iapfun)*(vu32*)(appxaddr+4);	
(vu32*)(appxaddr+4)//将复位中断向量地址值强制转化为32位的指针变量
*(vu32*)(appxaddr+4)//取出复位中断向量地址保存的数据=复位中断的中断服务程序入口地址
(iapfun)*(vu32*)(appxaddr+4)//将中断服务程序入口地址再转化成定义的函数指针类型
jump2app=(iapfun)*(vu32*)(appxaddr+4);//中断服务程序入口地址(iapfun类型的函数指针)赋值jump2app(定义的iapfun类型的函数指针其名为jump2app)
MSR_MSP(*(vu32*)appxaddr);		//初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
jump2app();                   //调用函数且jump2app函数的起始地址在上面赋值,可实现PC指针的跳转;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/837514.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

护照OCR识别接口如何对接

护照OCR识别接口也叫护照文字识别OCR,指的是传入护照照片&#xff0c;精准识别静态护照图像上的文字信息&#xff0c;包括姓名、签发地点、签发机关、护照号码、签发日期等信息。那么护照文字识别OCR接口如何对接呢&#xff1f; 首先我们找到一家有护照OCR识别接口的服务商数脉…

【万字面试题】Redis

文章目录 常见面试题布隆过滤器原理和数据结构&#xff1a;特点和应用场景&#xff1a;缺点和注意事项&#xff1a;在python中使用布隆过滤器 三种数据删除策略LRU (Least Recently Used)工作原理&#xff1a;应用场景&#xff1a; LFU (Least Frequently Used)工作原理&#x…

Navicat16小白式安装和激活详解《简单》

简介&#xff1a; Navicat 是一款强大的数据库管理和开发工具&#xff0c;它支持多种数据库系统&#xff0c;包括 MySQL、MariaDB、MongoDB、SQL Server、Oracle、PostgreSQL 以及 SQLite。Navicat 提供了图形界面&#xff08;GUI&#xff09;来简化数据库的管理、操作和维护。…

柔性数组+结构体类型转换

柔性数组&#xff1a;在结构体中声明的时候仅作为占位符&#xff0c;好处是地址是连续的 强制类型转换&#xff1a;可用于通信双方进行信息交流 #include <iostream> #include <string.h>struct DataWater {int count;float size;char buf[0]; }; // dbuf相当于是…

MYSQL中的DQL

语法&#xff1a; select 字段列表 from 表名列表 where 条件列表 group by 分组字段列表 having 分组后条件列表 order by 排序字段 limit 分页参数 条件查询 语法&#xff1a; 查询多个字段&#xff1a;select 字段1&#xff0c;字段2 from表名 查询所有字段&#xff1a…

“打工搬砖记”中首页的功能实现(一)

文章目录 打工搬砖记秒薪的计算文字弹出动画根据时间数字变化小结 打工搬砖记 先来一个小程序首页预览图&#xff0c;首页较为复杂的也就是“秒薪”以及弹出文字的动画。 已上线小程序“打工人搬砖记”&#xff0c;进行预览观看。 秒薪的计算 秒薪计算公式&#xff1a;秒薪…

RPA的全新形态—Agent智能体:当机器人开始“听”话

随着人工智能技术的不断进步&#xff0c;RPA正迈向其全新形态——Agent智能体。想象一下&#xff0c;如果你的日常工作中有一个智能助手&#xff0c;它不仅能理解你的需求&#xff0c;还能自动帮你完成那些繁琐的任务&#xff0c;这会是怎样的体验&#xff1f;这就是RPA技术正在…

从零创建一个vue2项目

标题从零创建一个vue2项目&#xff0c;项目中使用TensorFlow.js识别手写文字 npm切换到淘宝镜像 npm config set registry https://registry.npm.taobao.org安装vue/cli -g npm install -g vue/cli检查是否安装成功 vue -V创建项目 vue create 项目名安装TensorFlow npm …

Arduino-ILI9341驱动-SPI接口TFTLCD实现触摸功能系列之触控开关二

Arduino-ILI9341驱动-SPI接口TFTLCD实现触摸功能系列之触控开关二 1.概述 这篇文章在触摸屏上绘制一个开关&#xff0c;通过点击开关实现控制灯的开关功能。 2.硬件 硬件连接参考第一篇文章介绍 Arduino-ILI9341驱动-SPI接口TFTLCD实现触摸功能系列之获取触控坐标一 3.实现…

在线caj转换成pdf免费吗?caj变成pdf很容易!点进来!

在数字化阅读日益盛行的今天&#xff0c;各种电子文献格式层出不穷&#xff0c;其中CAJ和PDF无疑是两种最为常见的格式。CAJ是中国知网推出的一种专用全文阅读格式&#xff0c;而PDF则因其跨平台、不易被修改的特性&#xff0c;受到了广大读者的青睐。因此&#xff0c;将CAJ格式…

【C++】 类的新成员:static成员和类的好朋友:友元

欢迎来到CILMY23的博客 &#x1f3c6;本篇主题为&#xff1a; 类的新成员&#xff1a;static成员和类的好朋友&#xff1a;友元 &#x1f3c6;个人主页&#xff1a;CILMY23-CSDN博客 &#x1f3c6;系列专栏&#xff1a;Python | C | C语言 | 数据结构与算法 | 贪心算法 | Li…

网络编程套接字(一) 【简单的Udp网络程序】

网络编程套接字<一> 理解源端口号和目的端口号PORT VS PID认识TCP协议和UDP协议网络字节序socket编程接口sockaddr结构简单的UDP网络程序服务端创建套接字服务端绑定运行服务器客户端创建套接字关于客户端的绑定问题启动客户端启动客户端本地测试INADDR_ANY 理解源端口号…

怎么给视频加水印?2招轻松搞定

在数字媒体时代&#xff0c;视频水印作为一种有效的版权保护手段&#xff0c;被广泛应用于各种场景。给视频添加水印不仅可以防止内容被恶意盗用&#xff0c;还能增加视频的辨识度&#xff0c;提升品牌形象。本文将为您介绍2种简单易行的方法&#xff0c;教您怎么给视频加水印&…

Linux进程控制——Linux进程等待

前言&#xff1a;接着前面进程终止&#xff0c;话不多说我们进入Linux进程等待的学习&#xff0c;如果你还不了解进程终止建议先了解&#xff1a; Linux进程终止 本篇主要内容&#xff1a; 什么是进程等待 为什么要进行进程等待 如何进程等待 进程等待 1. 进程等待的概念2. 进…

软件杯 深度学习的水果识别 opencv python

文章目录 0 前言2 开发简介3 识别原理3.1 传统图像识别原理3.2 深度学习水果识别 4 数据集5 部分关键代码5.1 处理训练集的数据结构5.2 模型网络结构5.3 训练模型 6 识别效果7 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 深度学习…

【数据结构】平衡二叉树(插入、查找、删除)解析+完整代码

3.2 平衡二叉树 3.2.1 定义 平衡二叉树&#xff0c;简称平衡树&#xff08;AVL树&#xff09; 树上任一结点的左右子树高度差不超过1。 结点的平衡因子左子树高-右子树高 3.2.2 插入操作 插入结点后&#xff0c;可能造成不平衡 要调整最小不平衡子树&#xff0c;使其恢复平衡。…

【回溯 代数系统】679. 24 点游戏

本文涉及知识点 回溯 代数系统 LeetCode679. 24 点游戏 给定一个长度为4的整数数组 cards 。你有 4 张卡片&#xff0c;每张卡片上都包含一个范围在 [1,9] 的数字。您应该使用运算符 [‘’, ‘-’, ‘*’, ‘/’] 和括号 ‘(’ 和 ‘)’ 将这些卡片上的数字排列成数学表达式…

SQLserver - 笔记

1 SQLserver - 用户管理 4、SQL SERVER数据库用户管理_哔哩哔哩_bilibili 创建用户 - user 2.选择用户&#xff0c;修改属性

Qt---绘图和绘图设备

一、QPainter绘图 绘图事件 void paintEvent() 声明一个画家对象&#xff0c;OPainter painter(this) this指定绘图设备 画线、画圆、画矩形、画文字 设置画笔QPen 设置画笔宽度、风格 设置画刷QBrush 设置画刷风格 代码示例&#xff1a; #includ…

LeetCode---循环队列

循环队列就是只有固定的内存&#xff0c;存数据&#xff0c;出数据&#xff0c;但是也和队列一样&#xff0c;先进先出。如下图所示&#xff0c;这是他的样子 在head出&#xff0c;tail进&#xff0c;但是这个如果用数组解决的话&#xff0c;就有问题&#xff0c;力扣给我们的接…