STM32深入系列02——BootLoader分析与实现

文章目录

  • 1. STM32程序升级方法
    • 1.1 ST-Link / J-link下载
    • 1.2 ISP(In System Programing)
    • 1.3 IAP(In Applicating Programing)
      • 1.3.1 正常程序运行流程
      • 1.3.2 有IAP时程序运行流程
  • 2. STM32 Bootloader实现
    • 2.1 方式一:Boot_App(已实现)
      • 2.1.1 Bootloader
      • 2.1.2 APP
      • 2.1.3 测试
    • 2.2 方式二:其他接口 / USB拖拽等(未完成)

====>>> 文章汇总 <<<====

1. STM32程序升级方法

1.1 ST-Link / J-link下载

这个应该是最基本的方法,只要自己写过程序的应该都会,将编译生成的hex文件使用ST-Link工具或者J-Link工具直接下载进 Flash 即可。Keil中点击下载也能一键下载。

下载时可以看到地址是从0x0800 0000,即 Flash 的起始地址开始下载的。

优点:简单,插上下载器直接下载即可。
缺点:在产品中嵌入式板卡封装起来之后,因为下载口没有实际功能,所以很多时候下不拆机是没办法插上下载器的。这时候就不方便。

简单补充一句,bin文件hex文件的区别:

  • bin文件不带地址信息,因此下载的时候需要指定下载地址。
  • hex文件自带地址信息,直接点击下载自己会找到要下载到的地址(默认0x0800 0000)。

1.2 ISP(In System Programing)

我们常见的一键下载电路就是用的这种方式。这个是利用了 STM32 自带的 Bootloader 升级程序。

在用户参考手册中,可以看到下表,关于启动模式设置的。
在这里插入图片描述
中文版的如下:
在这里插入图片描述
程序运行时的启动过程

一般在我们的程序运行的时候,BOOT0 是接地的,也就是BOOT0 = 0。也就是程序是从主存储器Flash开始启动的,启动的地址为0x08000000

使用ISP下载时的启动过程

  1. 首先,硬件上将 STM32 的 BOOT0 引脚拉高、BOOT1 引脚拉低,即BOOT0 = 1BOOT1 = 0
  2. 此时,程序会从系统存储器中的程序启动,这段程序会接收串口数据(我们编译好的程序文件),并将这写数据放到主闪存存储器(Flash)当中。
  3. 最后,硬件上重新将 BOOT0 接地,也就是BOOT0 = 0,然后复位引脚拉低,程序复位重启,从 Flash 中开始运行程序。

可以这么理解:芯片出厂时,系统存储器中已经存储了一段程序,这段程序的功能是将串口1(固定的)收到的数据,放到主闪存存储器(Flash)中,从0x0800 0000地址处开始。

这样也就完成了我们的一键下载过程。
比如,正点原子的一键下载电路如下,左侧的三极管就是用来完成 BOOT引脚和复位引脚的操作的。

在这里插入图片描述

具体的硬件引脚电平如何变化百度搜一下,分析应该挺多的。我也没详细看过。

沁恒官网也有专门的一键下载芯片,原理上其实一样的。
在这里插入图片描述

链接:https://www.wch.cn/application/575.html

ISP的下载方式:

优点:提供了一种升级方式,无需代码支持。
缺点:需要相应的硬件支持,成本增加;使用的接口也是固定的,并且很多时候串口可能用于其他功能了已经。

1.3 IAP(In Applicating Programing)

IAP 和 ISP 其实基本上是一样的。

但是:ISP 是由厂商已经提供好的,因此接口固定;IAP可以自定义使用任何接口接收应用程序。也正是因为这一点,使得用户可以用多种不同的方式进行升级。

1.3.1 正常程序运行流程

在看IAP之前,要先看一下正常情况下,程序从Flash启动时的启动流程,如下图所示:

  1. 首先程序从Flash启动,根据中断向量表找到复位中断处理函数的地址(0x0800 0004处是中断向量表的起始地址,记录了复位中断处理函数的地址)。
  2. 执行复位中断处理函数,初始化系统环境之后,该函数最后会跳转到main函数继续运行。
  3. 在main函数的死循环中一直运行,直到有中断发生时(外设中断等等),重新跳转到中断向量表起始处。
  4. 在中断向量表中根据中断信号源来判断要执行的中断处理函数,然后跳转到相应的中断处理函数
  5. 中断处理函数运行完成之后继续跳转到main函数处继续运行。

在这里插入图片描述

1.3.2 有IAP时程序运行流程

接下来看下有了IAP之后的启动流程,如下图所示,下图中可以看到,在Flash中存储了 两“套” 程序(套的意思是,不仅只有用户程序,配套的中断向量等也都有)。

其中第一套为:Bootloader程序,该程序的功能是,接收某个接口的数据,并把这些数据存储到Flash中。

这些数据其实就是后面的一套APP程序,这套应用程序可以通过Bootlaoder程序保存到Flash中,这样就不用再使用专门的下载器。

第二套才是我们真正的应用程序。

  1. 首先程序从Flash启动,根据中断向量表找到复位中断处理函数的地址(0x0800 0004处是中断向量表的起始地址,记录了复位中断处理函数的地址)。
  2. 执行复位中断处理函数,该函数最后会跳转到Bootloader的main函数继续运行。这个main函数的任务就是,判断是否接收新的APP程序,如果有,就把新的APP程序文件保存到Flash(就是第二套程序的位置)然后跳转到第二套程序中运行。如果无,就直接跳转到第二套程序中运行。
  3. 跳转之后的过程也是和正常程序运行的流程一样,一旦进入新的APP程序的起始地址,就会根据中断向量表找到复位中断处理函数,然后进入App程序的main函数运行。
  4. 但是如果发生中断,是强制跳转到Bootloader程序的中断向量表进行查询的,而我们需要的肯定是需要跳转到APP程序的中断处理函数处运行。所以,在进行到APP程序后,APP程序一定要修改中断向量表的偏移,让查找对应中断处理函数的时候偏移一段地址到App程序的中断处理函数处,否则APP程序中的中断发生时,就无法跳转到APP程序的中断处理函数了。

在这里插入图片描述
到这,我们就知道Bootloader程序是干啥的了。

这个分析过程,是2.1小节中的方式一。其他方式跟这种原理上基本是一样一样的。

对比:

ISPIAP
在系统存储器中存储了一套接收串口1数据的程序IAP是将Flash分成了两份,在第一份中存储了一套接收某个接口的程序
使用硬件BOOT引脚设置进行跳转使用软件(直接修改PC指针)进行跳转
仅能使用芯片厂商设置好的接口(串口1)用户自定义,理论上只要能接收数据的接口都可以用

2. STM32 Bootloader实现

开发板:STM32F401CCU6最小系统板(淘宝十几块钱),Flash大小:256KB,RAM大小:64KB。

TODO:这里记录一下,我看到的或者想到的所有形式。每实现一个会过来贴代码地址。

  1. Bootloader_App
  2. Bootloader_Setting_App
  3. Bootloader_App1_App2
  4. U盘拖拽
  5. 无线升级

代码汇总地址:https://gitee.com/HzoZi/bootloader

2.1 方式一:Boot_App(已实现)

这应该是最常见、也是最简单的一种了。也就是上面1.3.2小节的分析的情况。

在这里插入图片描述

2.1.1 Bootloader

需要实现三个部分:

  1. 串口接收程序,用于接收App程序;
  2. STM32 Flash 写入接口,用于将App程序写入到 Flash;
  3. App跳转实现,用于跳转到App程序处开始运行。
  4. 设置 Keil 参数。

第一步:新建工程

调试口勾上,时钟设置最大,设置生成单独的.c文件。

  1. 勾选一个串口,参数默认即可。
    在这里插入图片描述
  2. 串口中断也勾上

在这里插入图片描述
3. 设置一个按键

在这里插入图片描述
生成工程即可。

第二步:配置串口,实现串口接收功能

  1. 添加 printf 函数支持

我这里放到了 usart.c 的最后。
在这里插入图片描述

// 需要调用stdio.h文件
#include <stdio.h>
// 取消ARM的半主机工作模式
#pragma import(__use_no_semihosting)
struct __FILE
{
	int handle;
};
FILE __stdout;
void _sys_exit(int x) // 定义_sys_exit()以避免使用半主机模式
{
	x = x;
}

int fputc(int ch, FILE *f)
{
	HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
	return ch;
}
  1. 实现中断接收App程序

我这里放到了 main.c 的上面。
在这里插入图片描述

uint8_t single_buff[1];					// 按字节保存APP程序
uint8_t app_buff[40 * 1024] = {0};		// 保存接收到的APP程序(最大40K)
volatile uint32_t app_buff_len = 0;		// APP程序的长度(字节)

void start_uart_rx(void)
{
	while(HAL_UART_Receive_IT(&huart1, single_buff, sizeof(single_buff)) != HAL_OK);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	if(huart->Instance == USART1)
	{
		// 把接收到的数据,放到缓冲区中
		app_buff[app_buff_len] = single_buff[0];
		app_buff_len++;
	}
	
	// 重新开启中断接收
	start_uart_rx();
}

第三步:IAP实现

其中,STM32Flash读写部分 其实有现成的代码可以用。使用 RT_Thread Studio 创建一个一样芯片的工程就可以直接抄了。

在这里插入图片描述
我们新建一个文件,存放 IAP 实现的相关代码。

  1. 头文件:

在这里插入图片描述

#ifndef __IAP_H
#define __IAP_H

#include "stm32f4xx_hal.h"
#include "stdint.h"

/* 以下宏定义名字尽量不要随便换 */

/* Flash定义,根据使用的芯片修改 */
#define ROM_START              			((uint32_t)0x08000000)
#define ROM_SIZE               			(256 * 1024)
#define ROM_END                			((uint32_t)(ROM_START + ROM_SIZE))
#define STM32_FLASH_START_ADRESS		ROM_START
#define STM32_FLASH_SIZE				ROM_SIZE
#define STM32_FLASH_END_ADDRESS			ROM_END


/* Flash扇区定义,STM32F401CCU6:256KB,根据使用的芯片修改 */
#define ADDR_FLASH_SECTOR_0     ((uint32_t)0x08000000) /* Base @ of Sector 0, 16 Kbytes */
#define ADDR_FLASH_SECTOR_1     ((uint32_t)0x08004000) /* Base @ of Sector 1, 16 Kbytes */
#define ADDR_FLASH_SECTOR_2     ((uint32_t)0x08008000) /* Base @ of Sector 2, 16 Kbytes */
#define ADDR_FLASH_SECTOR_3     ((uint32_t)0x0800C000) /* Base @ of Sector 3, 16 Kbytes */
#define ADDR_FLASH_SECTOR_4     ((uint32_t)0x08010000) /* Base @ of Sector 4, 64 Kbytes */
#define ADDR_FLASH_SECTOR_5     ((uint32_t)0x08020000) /* Base @ of Sector 5, 128 Kbytes */


/* Bootloader、APP 分区定义,根据个人需求修改 */
#define BOOT_START_ADDR			0x08000000		// FLASH_START_ADDR
#define BOOT_FLASH_SIZE			0x4000			// 16K
#define APP_START_ADDR			0x08004000		// BOOT_START_ADDR + BOOT_FLASH_SIZE
#define APP_FLASH_SIZE			0x3C000			// 240K


/* 对外接口 */
void show_boot_info(void);
uint8_t jump_app(uint32_t app_addr);

/* 对外接口 */
int stm32_flash_read(uint32_t addr, uint8_t *buf, size_t size);
int stm32_flash_write(uint32_t addr, const uint8_t *buf, size_t size);
int stm32_flash_erase(uint32_t addr, size_t size);

#endif /* __IAP_H */
  1. 源文件:

在这里插入图片描述

在这里插入图片描述

#include "iap.h"
#include "stdio.h"

void show_boot_info(void)
{
    printf("---------- Enter BootLoader ----------\r\n");
    printf("\r\n");
    printf("======== flash pration table =========\r\n");
    printf("| name     | offset     | size       |\r\n");
    printf("--------------------------------------\r\n");
    printf("| boot     | 0x%08X | 0x%08X |\r\n", BOOT_START_ADDR, BOOT_FLASH_SIZE);
    printf("| app      | 0x%08X | 0x%08X |\r\n", APP_START_ADDR, APP_FLASH_SIZE);
    printf("======================================\r\n");
}

typedef void (*jump_callback)(void);
/**
 * @note 跳转至App运行
 *
 * @param App起始地址
 *
 * @return result
 */
uint8_t jump_app(uint32_t app_addr)
{
    uint32_t jump_addr;
    jump_callback cb;
	
    if (((*(volatile uint32_t *)app_addr) & 0x2FFE0000 ) == 0x20000000) 
	{
		// 复位向量位于程序起始地址+4处
        jump_addr = *(volatile uint32_t*)(app_addr + 4);
		
        cb = (jump_callback)jump_addr;
		
		// 设置主堆栈指针指向 APP 程序起始地址
        __set_MSP(*(volatile uint32_t*)app_addr);  
        
		cb();
		
        return 1;
    }
    return 0;
}

/**
  * @brief  Gets the sector of a given address
  * @param  None
  * @retval The sector of a given address
  */
static uint8_t GetSector(uint32_t Address)
{
    if((Address < ADDR_FLASH_SECTOR_1) && (Address >= ADDR_FLASH_SECTOR_0))
    {
        return FLASH_SECTOR_0;
    }
    else if((Address < ADDR_FLASH_SECTOR_2) && (Address >= ADDR_FLASH_SECTOR_1))
    {
        return FLASH_SECTOR_1;
    }
    else if((Address < ADDR_FLASH_SECTOR_3) && (Address >= ADDR_FLASH_SECTOR_2))
    {
        return FLASH_SECTOR_2;
    }
    else if((Address < ADDR_FLASH_SECTOR_4) && (Address >= ADDR_FLASH_SECTOR_3))
    {
        return FLASH_SECTOR_3;
    }
    else if((Address < ADDR_FLASH_SECTOR_5) && (Address >= ADDR_FLASH_SECTOR_4))
    {
        return FLASH_SECTOR_4;
    }
	else
	{
		return FLASH_SECTOR_5;
	}
}

/**
 * Read data from flash.
 * @note This operation's units is word.
 *
 * @param addr flash address
 * @param buf buffer to store read data
 * @param size read bytes size
 *
 * @return result
 */
int stm32_flash_read(uint32_t addr, uint8_t *buf, size_t size)
{
    size_t i;

    if ((addr + size) > STM32_FLASH_END_ADDRESS)
    {
        printf("read outrange flash size! addr is (0x%p)", (void*)(addr + size));
        return -1;
    }

    for (i = 0; i < size; i++, buf++, addr++)
    {
        *buf = *(uint8_t *) addr;
    }

    return size;
}

/**
 * Write data to flash.
 * @note This operation's units is word.
 * @note This operation must after erase. @see flash_erase.
 *
 * @param addr flash address
 * @param buf the write data buffer
 * @param size write bytes size
 *
 * @return result
 */
int stm32_flash_write(uint32_t addr, const uint8_t *buf, size_t size)
{
    int8_t result = 0;
    uint32_t end_addr = addr + size;

    if ((end_addr) > STM32_FLASH_END_ADDRESS)
    {
        printf("write outrange flash size! addr is (0x%p)", (void*)(addr + size));
        return -1;
    }

    if (size < 1)
    {
        return -1;
    }

    HAL_FLASH_Unlock();

    __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_OPERR | FLASH_FLAG_WRPERR | FLASH_FLAG_PGAERR | FLASH_FLAG_PGPERR | FLASH_FLAG_PGSERR);

    for (size_t i = 0; i < size; i++, addr++, buf++)
    {
        /* write data to flash */
        if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE, addr, (uint64_t)(*buf)) == HAL_OK)
        {
            if (*(uint8_t *)addr != *buf)
            {
                result = -1;
                break;
            }
        }
        else
        {
            result = -1;
            break;
        }
    }

    HAL_FLASH_Lock();

    if (result != 0)
    {
        return result;
    }

    return size;
}

/**
 * Erase data on flash.
 * @note This operation is irreversible.
 * @note This operation's units is different which on many chips.
 *
 * @param addr flash address
 * @param size erase bytes size
 *
 * @return result
 */
int stm32_flash_erase(uint32_t addr, size_t size)
{
    int8_t result = 0;
    uint32_t FirstSector = 0, NbOfSectors = 0;
    uint32_t SECTORError = 0;

    if ((addr + size) > STM32_FLASH_END_ADDRESS)
    {
        printf("ERROR: erase outrange flash size! addr is (0x%p)\n", (void*)(addr + size));
        return -1;
    }

    /*Variable used for Erase procedure*/
    FLASH_EraseInitTypeDef EraseInitStruct;

    /* Unlock the Flash to enable the flash control register access */
    HAL_FLASH_Unlock();

    __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_OPERR | FLASH_FLAG_WRPERR | FLASH_FLAG_PGAERR | FLASH_FLAG_PGPERR | FLASH_FLAG_PGSERR);

    /* Get the 1st sector to erase */
    FirstSector = GetSector(addr);
    /* Get the number of sector to erase from 1st sector*/
    NbOfSectors = GetSector(addr + size - 1) - FirstSector + 1;
    /* Fill EraseInit structure*/
    EraseInitStruct.TypeErase     = FLASH_TYPEERASE_SECTORS;
    EraseInitStruct.VoltageRange  = FLASH_VOLTAGE_RANGE_3;
    EraseInitStruct.Sector        = FirstSector;
    EraseInitStruct.NbSectors     = NbOfSectors;

    if (HAL_FLASHEx_Erase(&EraseInitStruct, (uint32_t *)&SECTORError) != HAL_OK)
    {
        result = -1;
        goto __exit;
    }

__exit:
    HAL_FLASH_Lock();

    if (result != 0)
    {
        return result;
    }

    printf("erase done: addr (0x%p), size %d", (void*)addr, size);
    return size;
}

  1. main函数:

在这里插入图片描述

#include "iap.h"
#include "stdio.h"

int main(void)
{
	// 这里只是我自己写的部分,cubemx自动生成的这里没有,记得填上
	start_uart_rx();	// 开始中断接收
	show_boot_info();	// 输出分区信息
  while (1)
  {
		printf("waitting input... \r\n");
	  
		// 判断是否需要更新程序(5s)
		for(uint16_t i = 0; i < 5000; i++)
		{
			// 如果按键按下, 说明需要更新程序
			if(0 == HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0))
			{
				HAL_Delay(20);
				if(0 == HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0))
				{
					// 按键按下了,更新程序并跳转(先传程序,再按下按键)
					// 擦除APP区域
					printf("erase app... \r\n");
					stm32_flash_erase(APP_START_ADDR, APP_FLASH_SIZE);
					HAL_Delay(100);
					
					// 写入APP程序
					printf("write app... \r\n");
					stm32_flash_write(APP_START_ADDR, app_buff, app_buff_len);//更新FLASH代码
					break;
				}
			}
			HAL_Delay(1);
		}
		
		// 跳转到APP
		if(0 == jump_app(APP_START_ADDR))
		{
			printf("jump app failed... \r\n");
			while(1);
		}
	}
}
  1. keil 配置:

这里 Bootloader程序给分配大小是 16KB(看 iap 头文件),不是默认的全部 Flash,因此在 Keil 中修改一下。

在这里插入图片描述
至此,Bootloader程序就完成了。

2.1.2 APP

App程序比较简单,创建一个LED闪烁工程 或 串口输出工程即可。

  1. 修改中断向量表偏移

在这里插入图片描述

// 设置中断向量偏移量
#define NVIC_VTOR_MASK       0x3FFFFF80
#define APP_PART_ADDR        0x08004000
SCB->VTOR = APP_PART_ADDR & NVIC_VTOR_MASK;
  1. 修改中断向量表偏移

在这里插入图片描述

  1. 设置编译输出 bin 文件(App只能用 bin 文件,程序保存地址由 bootloader 程序指定)
    fromelf --bin --output "$L@L.bin" "#L"
    在这里插入图片描述

  2. app程序

在这里插入图片描述

至此,App程序就写完了,编译完成之后可以在工程的输出文件夹中看到编译生成的 bin 文件。
使用同样的方法可以创建两个App,分别为app1、app2两个的效果可以不一样。

这里设置app1输出:app_1 run,app2输出:app_2 run。

2.1.3 测试

测试步骤

  1. 先把Bootloader程序烧录到芯片中
  2. 复位运行,可以看到bootloader程序的提示信息
  3. 使用串口助手传输app程序,传输完成之后按下按键,等待写入完成之后自动跳入app程序开始运行
  4. 如果一直没有按键按下,5秒之后直接跳入app开始运行

尝试更换两个app,查看不同效果。
在这里插入图片描述

代码汇总地址:https://gitee.com/HzoZi/bootloader

2.2 方式二:其他接口 / USB拖拽等(未完成)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

西电期末1026.删除特定字符后排序输出

一.题目 二.分析与思路 题目名字很有意思&#xff0c;先删除后排序&#xff0c;难死了&#xff0c;还是先排序后删除简单吧&#xff1f;注意字符串里有空格&#xff0c;前面提到过了&#xff1a;只能用fgets!! 三.代码实现 #include<bits/stdc.h>//万能头 #define MAX…

SpringBoot-项目引入Redis依赖

在使用Spring Boot开发应用时&#xff0c;可以使用Redis来实现缓存、分布式锁等功能。在编写业务逻辑代码时&#xff0c;可以通过注入RedisTemplate或StringRedisTemplate对象来操作Redis&#xff0c;如存取数据、设置过期时间、删除数据等。同时&#xff0c;还可以使用Redis的…

西电期末1027.判断同构数

一.题目 二.分析与思路 不用把他转成字符串再转成数字之类的&#xff0c;用数学解决就好&#xff01;找出一个数的最后位就是将其对求余啊&#xff0c;找一个数有几位以前也有过啊&#xff0c;那不就过了嘛&#xff01; 三.代码实现 #include<bits/stdc.h>//万能头 in…

Windows.OpenSSL生成ssl证书配置到nginx

一、下载OpenSSL程序安装 到E:\soft\OpenSSL-Win64 二、打开一个CMD控制台窗口&#xff0c;设置好openssl.cnf路径 E: cd E:\soft\OpenSSL-Win64\bin set OPENSSL_CONFE:\soft\OpenSSL-Win64\bin\openssl.cnf 三、在当前目录 E:\soft\OpenSSL-Win64\bin 里创建两个子目录 m…

MySQL 8.0 InnoDB Tablespaces之Temporary Tablespaces(临时表空间)

文章目录 MySQL 8.0 InnoDB Tablespaces之Temporary Tablespaces&#xff08;临时表空间&#xff09;会话临时表空间会话临时表空间的磁盘分配和回收会话临时表空间的创建创建临时表和查看临时表信息会话临时表空间相关的设置参数innodb_temp_tablespaces_dir 全局临时表空间查…

CentOS安装gcc及g++

目录 一、在线安装 二、离线安装 1、收集对应.rpm文件 2、rpm文件上传 3、在该目录下执行安装命令 3、测试 一、在线安装 联网安装比较简单只需使用如下命令即可 yum install gcc yum install gcc-c 二、离线安装 1、收集对应.rpm文件 &#xff08;1&#xff09;、解压…

OS_lab——保护模式之GDT、 Descriptor、Selector、GDTR 及其之间关系

1. 保护模式的相关数据结构 保护模式必要的数据结构定义 • GDT&#xff1a;即为 Global Descriptor Table&#xff08;全局描述符表&#xff09;&#xff0c;又称段描述符表&#xff0c; 为保护模式下的一个数据结构。其中包含多个 descriptor&#xff0c;定义了段的起始地…

pyinstaller生成的exe文件启动时间漫长的原因

加-F慢的原因是&#xff0c;pyinstaller把所有资源文件包括python解释器的依赖文件和库都打包到exe一个文件中&#xff0c;用户打开时&#xff0c;pyinstaller需要先执行一边解压操作&#xff0c;把依赖文件全部解压出来。慢就慢在这里。 如果不加-F&#xff0c;你会发现那些文…

STL标准库与泛型编程(侯捷)笔记3

STL标准库与泛型编程&#xff08;侯捷&#xff09; 本文是学习笔记&#xff0c;仅供个人学习使用。如有侵权&#xff0c;请联系删除。 参考链接 Youbute: 侯捷-STL标准库与泛型编程 B站: 侯捷 - STL Github:STL源码剖析中源码 https://github.com/SilverMaple/STLSourceCo…

常用服务器管理面板整理汇总

服务器管理面板是用于管理和控制服务器的软件&#xff0c;可以帮助管理员更轻松地进行服务器管理和维护。以下是几种常用的服务器管理面板&#xff1a; 1、宝塔面板【官网直达】 宝塔面板是一款服务器运维管理软件&#xff0c;支持Windows和Linux等操作系统&#xff0c;提供了…

《动手学深度学习》学习笔记 第6章 卷积神经网络

本系列为《动手学深度学习》学习笔记 书籍链接&#xff1a;动手学深度学习 笔记是从第四章开始&#xff0c;前面三章为基础知道&#xff0c;有需要的可以自己去看看 关于本系列笔记&#xff1a; 书里为了让读者更好的理解&#xff0c;有大篇幅的描述性的文字&#xff0c;内容很…

TypeScript 从入门到进阶之基础篇(六) 类型(断言 、推论、别名)| 联合类型 | 交叉类型

系列文章目录 TypeScript 从入门到进阶系列 TypeScript 从入门到进阶之基础篇(一) ts基础类型篇TypeScript 从入门到进阶之基础篇(二) ts进阶类型篇TypeScript 从入门到进阶之基础篇(三) 元组类型篇TypeScript 从入门到进阶之基础篇(四) symbol类型篇TypeScript 从入门到进阶…

鱼类识别Python+深度学习人工智能+TensorFlow+卷积神经网络算法

一、介绍 鱼类识别系统。使用Python作为主要编程语言开发&#xff0c;通过收集常见的30种鱼类&#xff08;‘墨鱼’, ‘多宝鱼’, ‘带鱼’, ‘石斑鱼’, ‘秋刀鱼’, ‘章鱼’, ‘红鱼’, ‘罗非鱼’, ‘胖头鱼’, ‘草鱼’, ‘银鱼’, ‘青鱼’, ‘马头鱼’, ‘鱿鱼’, ‘鲇…

如何批量自定义视频画面尺寸

在视频制作和编辑过程中&#xff0c;对于视频画面尺寸的调整是一项常见的需求。有时候&#xff0c;为了适应不同的播放平台或满足特定的展示需求&#xff0c;我们需要对视频尺寸进行批量调整。那么&#xff0c;如何实现批量自定义视频画面尺寸呢&#xff1f;本文将为您揭示这一…

github action

https://www.bilibili.com/video/BV1PM411B7um/?spm_id_frompageDriver&vd_sourcefd0f4be6d0a5aaa0a79d89604df3154a workflow pipeline

PyTorch 进阶指南,这个宝典太棒了

最新写了很多关于 Pytorch 的文章&#xff0c;主要针对刚刚接触 Pytorch 的同学&#xff0c;文章我给大家列出来了&#xff0c;喜欢可以从0开始学习&#xff1a; 小白学 PyTorch 系列&#xff1a;这一次&#xff0c;我准备了 20节 PyTorch 中文课程小白学 PyTorch 系列&#x…

【深度deepin】深度安装,jdk,tomcat,Nginx安装

目录 一 深度 1.1 介绍 1.2 与别的操作系统的优点 二 下载镜像文件及VM安装deepin 三 jdk&#xff0c;tomcat&#xff0c;Nginx安装 3.1 JDK安装 3.2 安装tomcat 3.3 安装nginx 一 深度 1.1 介绍 由深度科技社区开发的开源操作系统&#xff0c;基于Linux内核&#xf…

学完 Pinia 再也不想用 vuex 真香啊!!!!

&#x1f495;Pinia 注册 ✔ vue3 与 Pinia 注册 import { createApp } from vue import { createPinia } from pinia import App from ./App.vueconst app createApp()app.use(createPinia()) app.mount(#app)✔ vue2 与 Pinia 注册 import Vue from vue import App from …

java推荐系统:好友推荐思路

1.表的设计 表里面就两个字段&#xff0c;一个字段是用户id&#xff0c;另外一个字段是好友id&#xff0c;假如A跟B互为好友&#xff0c;那在数据库里面就会有两条数据 2.推荐好友思路 上面的图的意思是&#xff1a;h跟a的互为好友&#xff0c;a跟b&#xff0c;c&am…

Python笔记04-数据容器列表、元组、字符串、集合、字典

文章目录 listtuple 元组str序列&#xff08;切片&#xff09;setdict集合通用功能 Python中的数据容器&#xff1a; 一种可以容纳多份数据的数据类型&#xff0c;容纳的每一份数据称之为1个元素 每一个元素&#xff0c;可以是任意类型的数据&#xff0c;如字符串、数字、布尔等…