彻底解决单片机BootLoader升级程序失败问题

news2024/11/13 9:41:47

文章目录

    • 1、引言
    • 2、MicroBoot:优雅的解决升级问题
      • 问题1:bootloader 在跳转到app前没有清理干净存在的痕迹
      • 问题2: 需要 APP 传递信息给 Bootloader
      • 问题3: APP单独运行没有问题,通过Bootloader跳转到APP运行莫名死机
      • 问题4: 固件升级过程中频繁中断
      • 问题5: 上位机操作过于复杂,客户难以使用
    • 3、MicorBoot架构
      • 3.1 层次框架
      • 3.2 模块化组件
    • 4、MicorBoot移植教程


MicroBoot 是一个由模块化代码组成的框架,旨在简化和加速嵌入式单片机程序的升级和开发过程。

1、引言

那些让人崩溃的BootLoader升级灾难

你有没有为写BootLoader无从下手发愁过?

你有没有为出厂的产品还需要开盖刷程序苦恼过?

你有没有为程序升级失败,产品变成“砖头”而抓耳挠腮过?

你有没有为升级完成后设备神秘失联而怀疑人生过?

你有没有因为固件升级Bug被老板当众灵魂拷问:“为啥烧进去就起不来了?”

灾难 1:升级一半突然断电

设备断电的瞬间,你的内心崩溃了:“完了,这次开机肯定黑屏。”客户还得拆机刷程序,一切从头再来。

灾难 2:传输到99%时卡死

传输固件时,进度条到99%停住不动——仿佛在嘲讽你:“嘿,看起来你离胜利只差一步哦。”偏偏这时客户来电:“还没好?”你只能干笑:“再给我两分钟……”

灾难 3:现场升级,接线找不到

客户现场的设备密不透风,调试接口深藏不露,调试工程师硬是趴在设备底下捣鼓半天:“线呢?我明明记得它在这儿啊……”

灾难 4:升级完成后设备变“失联王”

终于把固件刷进去,一键重启后,设备再也不响应了。就像刚被你送进冥王星:你呼唤它一万遍,它还是一言不发。

灾难 5:老板的灵魂拷问

你费尽心力搞定一版程序,上线后设备却不停死机。复盘会上,老板一句:“为什么烧进去就起不来了?”让你顿时哑口无言。

2、MicroBoot:优雅的解决升级问题

MicroBoot就是为了解决这些灾难而生的,它是我开发的BootLoader框架,专为嵌入式单片机设备的升级而优化。从断电保护到断点续传,从校验机制到高效通信,它全面提升了升级过程的可靠性,让固件升级变得轻松愉快。

问题1:bootloader 在跳转到app前没有清理干净存在的痕迹

问题描述: 固件更新完毕后从 bootloader 跳转至 APP 前需对所用的外设 deinit ,以使外设恢复至上电时的初始状态。但是当使用复杂的外设收取固件包时, deinit 也将变得复杂,甚至很难排除对 APP 的影响。因此最佳的方法是升级完固件进行软复位,再次进入bootloader在程序运行之前,先通过判断标志的方式,来直接跳转到app,这样就相当于给APP提供了一个干净的外设环境。

程序复位执行流程如下图所示:

请添加图片描述

常见方法及其局限性

传统的设置标志位以实现复位后保留状态的方法通常有以下两种:

  • 备份寄存器 :利用芯片中的不受软件复位影响的可供用户使用的寄存器 (如 STM32 中的备份寄存器);
    这种方法需要额外的设置,并且因单片机型号而异,增加了跨平台兼容的难度。

  • No-Init 数据段:将标志位放在特殊的内存段(例如.bss.noinit),使其在复位后避免被重新初始化。
    这种方法需要修改链接文件(分散加载文件),对不熟悉链接文件的开发者来说操作难度较高。

这两种方法都存在一定的局限性:设置复杂,且在断电情况下标志可能丢失。

解决方案:

MicroBoot采用了一种更优雅的“Magic Flag”方案。该方案在APP分配的FLASH空间末端设置三个标志(Magic1、Magic2、Magic3),总共占用192字节,每个标志占64字节,并根据固件升级的阶段来设置相应的标志。

描述Magic1 (64字节)Magic2 (64字节)Magic3 (64字节)
enter_bootloader0XFFFFFFFF0XFFFFFFFF0x00000000
begin_download0XFFFFFFFF0x000000000XFFFFFFFF
finalize_download0x000000000x000000000XFFFFFFFF

Magic Flag在FLASH中的位置,以及变化过程如下图所示:

请添加图片描述

标志的使用阶段

阶段1:进入Bootloader(enter_bootloader)

  • 对于全片擦除过的单片机,此时Magic1Magic2的值为0xFFFFFFFF,表示还未开始下载过程,Magic3被设置为0x00000000,表明这是一个等待升级程序的状态。
void enter_bootloader(void)
{
    uint32_t wData = 0;
    target_flash_write((APP_PART_ADDR + APP_PART_SIZE - 64), (const uint8_t *)&wData, sizeof(wData));
}

阶段2:开始下载(begin_download)

  • 当固件下载开始时,MicroBoot会首先对Magic所在的扇区擦除,然后将Magic2的值设置为0x00000000

  • 此时,Magic10xFFFFFFFFMagic3也保持为0xFFFFFFFF,这些状态便于系统在出现断电时判断下载是否已部分完成,从而支持断电续传。

void begin_download(void)
{
    memset(chBootMagic, 0, sizeof(chBootMagic));
    target_flash_erase(APP_PART_ADDR + APP_PART_SIZE - (192), 3*MARK_SIZE);
    target_flash_write((APP_PART_ADDR + APP_PART_SIZE - (128)), chBootMagic[1], MARK_SIZE);
}

阶段3:完成下载(finalize_download)

  • 当固件下载完成且数据写入成功后,MicroBoot会将Magic1的值设置为0x00000000,标志着下载过程已顺利完成。

  • 此时,Magic2的值仍为0x00000000,而Magic3的值保持为0xFFFFFFFF,从而标识此阶段为下载完成、准备进入应用程序的状态。

void finalize_download(void)
{
    memset(chBootMagic, 0X00, sizeof(chBootMagic));
    target_flash_write((APP_PART_ADDR + APP_PART_SIZE - 192), chBootMagic[0], MARK_SIZE);
}

阶段4:重新进入Bootloader并跳转到APP

  • 在系统完成固件升级后,MicroBoot会执行软复位,系统重新进入bootloader。

  • bootloader在检查到Magic1Magic2均为0x00000000,而Magic30xFFFFFFFF时,就会识别到这是一个升级完成的状态。

  • 这时,MicroBoot无需对外设进行反初始化,而是直接跳转到APP,从而为应用程序提供一个干净的外设环境。

阶段5:从APP再次进入Bootloader

  • 在APP正常运行后,如果想再次进入bootloader进行升级,调用MicroBoot提供的进入bootloader的接口,将会把Magic3设置为0x00000000,复位后,将会再次回到阶段1。

  • 当下次进入bootloader时,看到Magic1Magic2Magic3均为0x00000000

程序复位执行流程如下图所示:

请添加图片描述

对应的代码:

__attribute__((constructor))
static void enter_application(void)
{
    do {
        // User-defined conditions for entering the bootloader
        if(user_enter_bootloader()){
            break;			
        }
        // Read the magic values from flash memory to determine the next action
        target_flash_read((APP_PART_ADDR + APP_PART_SIZE - 3 * MARK_SIZE), chBootMagic[0], 3 * MARK_SIZE);

        // Check if Magic3 is 0x00, indicating to read user data from a specific location
        if ((0 == *(uint32_t *)&chBootMagic[2])) {
            break;
        }

        // Check if Magic2 is 0x00 and Magic1 is 0xFFFFFFFF, indicating to read user data from a different location
        if ((0 == *(uint32_t *)&chBootMagic[1]) && (0XFFFFFFFF == *(uint32_t *)&chBootMagic[0])) {
            break;
        }
		
        // Check if the value at the address (APP_PART_ADDR + 4) has the expected application identifier
        if (((*(volatile uint32_t *)(APP_PART_ADDR + 4)) & 0xff000000) != (APP_PART_ADDR & 0xff000000)) {
            break;
        }
		
        // If all checks are passed, modify the stack pointer and start the application
        modify_stack_pointer_and_start_app(*(volatile uint32_t *)APP_PART_ADDR,
                                           (*(volatile uint32_t *)(APP_PART_ADDR + 4)));

    } while(0);	
}

函数修饰符 __attribute__((constructor))告诉编译器在程序启动时自动调用这个函数。即在主程序的 main() 函数之前执行,它的主要功能是检查系统当前的状态,并根据状态决定是进入APP还是停留在bootloader。代码中增加了用户自定义的进入bootloader条件,代码通过调用user_enter_bootloader()检查用户是否指定了进入bootloader模式,这个检查是为了给用户留出手动控制的空间,比如通过外部按键强制进入bootloader,如果返回值为true,则直接退出函数,保持在bootloader中。

程序升级执行流程如下图所示:

请添加图片描述

问题2: 需要 APP 传递信息给 Bootloader

问题描述: 在一些嵌入式设备中,由于产品型号和应用场景的不同,固件升级的接口(如 UART、SPI、I2C 等)和波特率配置并不统一。这种情况下,Bootloader 无法在启动时确定应该使用的通信接口和波特率设置,因此需要从应用程序(APP)传递相关信息给 Bootloader,以便其在固件升级前做好正确的配置。

如果没有有效的机制让 APP 将接口和波特率信息传递给 Bootloader,可能会导致 Bootloader 和 APP 之间的通信不匹配,升级无法进行,甚至引发系统崩溃。

解决方案:

有了以上在flash中设置标志的思路,那就顺水推舟,再增加两个用户区的空间,给bootloader和app一个沟通数据的桥梁。

  • APP 参数写入:在需要固件升级时,APP 会将接口类型、波特率等升级信息写入指定的用户数据区。

  • Bootloader 参数读取:Bootloader 在启动时首先读取该存储区域,提取接口和波特率参数,并据此初始化通信配置。

  • 兼容性检测:如果读取的参数不符合预期,Bootloader 将回退至默认配置或停止启动,以保证通信的可靠性。

这种机制不仅灵活地适配了不同型号设备的升级需求,还提高了 Bootloader 的兼容性和稳定性。

更新后的FLASH空间如下:

描述用户数据备份区(192 字节)用户数据区(192字节)Magic1 (64字节)Magic2 (64字节)Magic3 (64字节)
enter_bootloader0XFFFFFFFF…user_data0XFFFFFFFF0XFFFFFFFF0x00000000
begin_downloaduser_data0XFFFFFFFF…0XFFFFFFFF0x000000000XFFFFFFFF
finalize_downloaduser_data0XFFFFFFFF…0x000000000x000000000XFFFFFFFF

用户数据在FLASH中的位置,以及变化过程如下图所示:

请添加图片描述

修改相应的代码:

阶段1:进入Bootloader(enter_bootloader)

  • 对于全片擦除过的单片机,此时user data的值为0xFFFFFFFF,当APP调用enter_bootloader接口,将会把app传递进来的数据写进用户区。
void enter_bootloader(uint8_t *pchDate, uint16_t hwLength)
{
    uint32_t wData = 0;
    target_flash_write((APP_PART_ADDR + APP_PART_SIZE - (3*MARK_SIZE) - (USER_DATA_SIZE)), pchDate, USER_DATA_SIZE);
    target_flash_write((APP_PART_ADDR + APP_PART_SIZE - MARK_SIZE), (const uint8_t *)&wData, sizeof(wData));
}

阶段2:开始下载(begin_download)

  • 当固件下载开始时,MicroBoot会首先对Magic所在的扇区擦除,然后将用户区的数据重新写到用户数据备份区

  • 此时,用户区0xFFFFFFFF,如果断电重启,系统将从用户数据备份区获取数据。

void begin_download(void)
{
    memset(chBootMagic, 0, sizeof(chBootMagic));
    target_flash_erase(APP_PART_ADDR + APP_PART_SIZE - (3*MARK_SIZE), 3*MARK_SIZE);
    target_flash_write((APP_PART_ADDR + APP_PART_SIZE - (3*MARK_SIZE) - 2 * (USER_DATA_SIZE)), tUserData.msg_data.B, USER_DATA_SIZE);
    target_flash_write((APP_PART_ADDR + APP_PART_SIZE - (2*MARK_SIZE)), chBootMagic[1], MARK_SIZE);
}

BootLoader程序复位执行的代码也要做相应的改变:

__attribute__((constructor))
static void enter_application(void)
{
    do {
		// User-defined conditions for entering the bootloader
		if(user_enter_bootloader()){
            target_flash_read((APP_PART_ADDR + APP_PART_SIZE - (3 * MARK_SIZE) - USER_DATA_SIZE), tUserData.msg_data.B, USER_DATA_SIZE);
            break;			
		}
        // Read the magic values from flash memory to determine the next action
        target_flash_read((APP_PART_ADDR + APP_PART_SIZE - 3 * MARK_SIZE), chBootMagic[0], 3 * MARK_SIZE);

        // Check if Magic3 is 0x00, indicating to read user data from a specific location
        if ((0 == *(uint32_t *)&chBootMagic[2])) {
            target_flash_read((APP_PART_ADDR + APP_PART_SIZE - (3 * MARK_SIZE) - USER_DATA_SIZE), tUserData.msg_data.B, USER_DATA_SIZE);
            break;
        }

        // Check if Magic2 is 0x00 and Magic1 is 0xFFFFFFFF, indicating to read user data from a different location
        if ((0 == *(uint32_t *)&chBootMagic[1]) && (0XFFFFFFFF == *(uint32_t *)&chBootMagic[0])) {
            target_flash_read((APP_PART_ADDR + APP_PART_SIZE - (3 * MARK_SIZE) - 2 * USER_DATA_SIZE), tUserData.msg_data.B, USER_DATA_SIZE);
            break;
        }
		
        // Check if the value at the address (APP_PART_ADDR + 4) has the expected application identifier
        if (((*(volatile uint32_t *)(APP_PART_ADDR + 4)) & 0xff000000) != (APP_PART_ADDR & 0xff000000)) {
            break;
        }
		
        // If all checks are passed, modify the stack pointer and start the application
        modify_stack_pointer_and_start_app(*(volatile uint32_t *)APP_PART_ADDR,
                                           (*(volatile uint32_t *)(APP_PART_ADDR + 4)));

    } while(0);	
}

BootLoader定义了一个默认的用户数据结构体,一共192个字节,APP可以在192个字节内随意向bootloader传递数据:

// <o>The user data size
//  <i>Default: 192
#define USER_DATA_SIZE            192

typedef struct {
    char chProjectName[16];
    char chHardWareVersion[16];
    char chSoftBootVersion[16];
    char chSoftAppVersion[16];
} msgSig_t;
typedef struct {
    union {
        msgSig_t sig;
        uint8_t B[USER_DATA_SIZE];
    } msg_data;
} user_data_t;

BootLoader为了方便App操作进入bootloader,并正确的传递数据,定义好了进入bootloader的接口,和操作Flash的函数,并将接口位置固定到0x08001000地址,这样APP就可以方便的操作Flash了

typedef struct {
    void (*fnEnterBootloaderMode)(uint8_t *pchDate, uint16_t hwLength);
    bool (*target_flash_init)(uint32_t addr); 
    bool (*target_flash_uninit)(uint32_t addr);
    int  (*target_flash_read)(uint32_t addr, uint8_t *buf, size_t size); 
    int  (*target_flash_write)(uint32_t addr, const uint8_t *buf, size_t size); 
    int  (*target_flash_erase)(uint32_t addr, size_t size); 
} boot_ops_t;

__attribute__((used))
static const boot_ops_t tBootOps  __attribute__ ((section(__ARM_AT(0x08001000)))) = {
    .fnEnterBootloaderMode = enter_bootloader,
    .target_flash_init = target_flash_init,
    .target_flash_erase = target_flash_erase,
    .target_flash_write = target_flash_write,
    .target_flash_read = target_flash_read,
    .target_flash_uninit = target_flash_uninit
};

APP区代码:

APP需要重新定义用户数据,添加需要向bootloader传递的数据。

typedef struct {
    char chProjectName[16];
    char chHardWareVersion[16];
    char chSoftBootVersion[16];
    char chSoftAppVersion[16];

    /*添加用户数据*/
    char chPort1Name[16];
    int wPort1Baudrate;
    char chPort2Name[16];
    int wPort2Baudrate;
    char chPort3Name[16];
    int wPort3Baudrate;

} msgSig_t;
typedef struct {
    union {
        msgSig_t sig;
        char B[sizeof(msgSig_t)];
    } msg_data;
} user_data_t;

user_data_t  tUserData = {
    .msg_data.sig.chProjectName = "project",
    .msg_data.sig.chHardWareVersion = HARDWARE_VERSION,
    .msg_data.sig.chSoftBootVersion = BOOTWARE_VERSION,
    .msg_data.sig.chSoftAppVersion =  SOFTWARE_VERSION,
};

typedef struct {
    void (*fnGoToBoot)(uint8_t *pchDate, uint16_t hwLength);
    bool (*target_flash_init)(uint32_t addr);
    bool (*target_flash_uninit)(uint32_t addr);
    int  (*target_flash_read)(uint32_t addr, uint8_t *buf, size_t size);
    int  (*target_flash_write)(uint32_t addr, const uint8_t *buf, size_t size);
    int  (*target_flash_erase)(uint32_t addr, size_t size);
} boot_ops_t;

比如通过CAN接口来升级程序,就可以这样做:

void can_boot()
{
    rt_memcpy(tUserData.msg_data.sig.chPort1Name, "CAN1", rt_strlen("CAN1"));
    tUserData.msg_data.sig.wPort1Baudrate = 500000;
    boot_ops_t *ptBootOps = (boot_ops_t *) 0x08001000;
    ptBootOps->fnGoToBoot((uint8_t *)tUserData.msg_data.B, sizeof(tUserData));
    rt_hw_cpu_reset();
}
MSH_CMD_EXPORT(can_boot, go to bootloader);

比如通过UART接口来升级程序,就可以这样做:

void uart_boot()
{
    rt_memcpy(tUserData.msg_data.sig.chPort1Name, "UART1", rt_strlen("UART1"));
    tUserData.msg_data.sig.wPort1Baudrate = 115200;
    boot_ops_t *ptBootOps = (boot_ops_t *) 0x08001000;
    ptBootOps->fnGoToBoot((uint8_t *)tUserData.msg_data.B, sizeof(tUserData));
    rt_hw_cpu_reset();
}
MSH_CMD_EXPORT(uart_boot, go to bootloader);

问题3: APP单独运行没有问题,通过Bootloader跳转到APP运行莫名死机

问题描述:在近几年的嵌入式社区中,流传着不少关于面相Cortex-MBootloader科普文章,借助这些文章,一些较为经典的代码片断和技巧得到了广泛的传播。

在从Bootloader跳转到用户APP的过程中,使用函数指针而非传统的汇编代码则成了一个家喻户晓的小技巧。相信类似下面 JumpToApp() 函数,你一定不会感到陌生:

typedef  void (*pFunction)(void);

void JumpToApp(uint32_t addr)
{
  pFunction Jump_To_Application;

  __IO uint32_t StackAddr;
  __IO uint32_t ResetVector;
  __IO uint32_t JumpMask;

  JumpMask = ~((MCU_SIZE-1)|0xD000FFFF);

  if (((*(__IO uint32_t *)addr) & JumpMask ) == 0x20000000) //�ж�SPָ��λ��
  {
    StackAddr = *(__IO uint32_t*)addr;
    ResetVector = *(__IO uint32_t *)(addr + 4);

    __set_MSP(StackAddr); 
    Jump_To_Application = (pFunction)ResetVector;
    Jump_To_Application(); 
  }
}

但是这段家喻户晓,被世人奉为真理的代码,却隐藏着很深的BUG,相信很多小伙伴都遇到过通过Bootloader跳转到APP后,程序时好时坏的灵异事件,具体详情请看这篇文章:震惊!这个隐藏的Bootloader漏洞究竟有多少人中招?

解决方案:

完全用汇编来处理从BootloaderApp的最后步骤,才是最稳定可靠的方案:

#if defined (__ARMCC_VERSION) && (__ARMCC_VERSION >= 6010050)
/* Avoids the semihosting issue */
__asm("  .global __ARM_use_no_argv\n");
#elif defined(__GNUC__)
/* Disables part of C/C++ runtime startup/teardown */
void __libc_init_array (void) {}
#endif

#if defined(__CC_ARM)
__asm void modify_stack_pointer_and_start_app(uint32_t r0_sp, uint32_t r1_pc)
{
    MOV SP, R0
    BX R1
}
#elif defined(__GNUC__)
void modify_stack_pointer_and_start_app(uint32_t r0_sp, uint32_t r1_pc)
{
    uint32_t z = 0;
    __asm volatile (  "msr    control, %[z]   \n\t"
                      "isb                    \n\t"
                      "mov    sp, %[r0_sp]    \n\t"
                      "bx     %[r1_pc]"
                      :
                      :   [z] "l" (z),
                      [r0_sp] "l" (r0_sp),
                      [r1_pc] "l" (r1_pc)
                   );
}
#else
#error "Unknown compiler!"
#endif

问题4: 固件升级过程中频繁中断

问题描述: 在固件升级过程中,由于通信信号不稳定或数据包丢失,传输可能会频繁中断,导致升级失败。用户常常需要多次尝试才能成功完成固件升级,特别是对于大文件传输,这种情况不仅耗时还影响体验。

解决方案:

为了提高传输稳定性,我采用状态机的方式重新开发了 ymodem 协议。此改进后的 ymodem 通过精细管理传输状态,使其更具稳定性和效率,即便在传输过程中断线,也能在超时前重新连接后继续传输,无需重新开始。

  1. ymodem 传输协议:ymodem 提供了文件校验和分段传输机制,提高了传输过程的鲁棒性。
  2. 自动重试机制:ymodem 协议中带有重试机制,确保每个数据包在成功接收前会自动重发,以提升传输成功率。

问题5: 上位机操作过于复杂,客户难以使用

**问题描述:**对于许多客户,传统上位机操作系统设计复杂且需要较高的技术水平,操作不当还可能导致设备升级失败。这不仅增加了技术支持成本,也降低了客户满意度,尤其对于不具备技术背景的用户而言,升级流程显得尤为繁琐。

解决方案:

为了解决客户的操作复杂度问题,我开发了专门的 MicroLink 工具,使固件升级过程简单直观。

MicroLink 是一款多功能嵌入式系统开发工具,专为加速和简化开发者在 研发、调试、量产和售后服务 各阶段的工作流程设计。与改进后的 ymodem 结合使用时,MicroLink 能显著提升升级体验:

  • 拖放式升级:用户只需将固件文件拖放至 MicroLink 提供的虚拟 U 盘,即可自动完成传输和安装。

  • 稳定的断点续传支持:借助重新开发的 ymodem 协议,即使传输中断,也能自动在恢复后继续传输,避免重复操作。

拖放式升级演示视频如下:

microlink U盘拖拽 ymodem下载

产品链接:https://item.taobao.com/item.htm?ft=t&id=826800975011
文档说明:https://microboot.readthedocs.io/zh-cn/latest/tools/microlink/microlink/
开源代码:https://github.com/Aladdin-Wang/MicroBoot
请添加图片描述

3、MicorBoot架构

3.1 层次框架

请添加图片描述

3.2 模块化组件

  • bootloader

  • ymodem

一个使用状态机编写的ymodem协议

  • 统一的Flash驱动管理模块

  • 环形队列

一个用C语言编写的支持多类型、函数重载与线程安全的环形队列

  • 信号槽

一个用C语言模拟QT的信号槽的功能

  • 发布订阅

  • shell

  • 混合数据流引擎

4、MicorBoot移植教程

  • 基于 CMSIS-PACK 移植
  • 基于源码移植
  • 基于rtthread软件包移植

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

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

相关文章

Oracle OCP认证考试考点详解082系列17

题记&#xff1a; 本系列主要讲解Oracle OCP认证考试考点&#xff08;题目&#xff09;&#xff0c;适用于19C/21C,跟着学OCP考试必过。 81. 第81题&#xff1a; 题目 81.Examine these SOL statements which execute successfully Which two statements are true after exec…

【EFK】Linux集群部署Elasticsearch最新版本8.x

【EFK】Linux集群部署Elasticsearch最新版本8.x 摘要环境准备环境信息系统初始化启动先决条件 下载&安装修改elasticsearch.yml控制台启动Linux服务启动访问验证查看集群信息查看es健康状态查看集群节点查询集群状态 生成service token验证service tokenIK分词器下载 摘要 …

基于python的天气数据采集与可视化分析,对20个城市的天气适宜出行度分析

摘要 本项目旨在基于Python对20个城市的天气数据进行采集与可视化分析&#xff0c;以评估天气的适宜出行度。该分析通过四个主要指标进行量化&#xff0c;这些指标分别是天气状况良好率、空气质量优良率、气温适宜率和安全天气率。通过这些指标&#xff0c;我们能够有效地判断…

外贸管理利器7选,助力高效办公

推荐7款外贸管理软件&#xff0c;包括ZohoBooks、ZohoCRM、富通天下等&#xff0c;各具特色&#xff0c;满足外贸企业不同需求&#xff0c;提高管理效率&#xff0c;助力企业全球化竞争。、 一、Zoho Books Zoho Books是一款外贸财务管理软件&#xff0c;不仅为用户提供了一个…

【JWT】Asp.Net Core中JWT刷新Token解决方案

Asp.Net Core中JWT刷新Token解决方案 前言方案一:当我们操作某个需要token作为请求头的接口时,返回的数据错误error.response.status === 401,说明我们的token已经过期了。方案二:实现用户无感知的刷新token值,我们希望当响应返回的数据是401身份过期时,响应阻拦器自动帮我…

当AI遇上时尚:未来的衣橱会由机器人来打理吗?

内容概要 在当今这个快速发展的时代&#xff0c;人工智能与时尚的结合正在逐渐改写我们对衣橱管理的认知。传统的衣橱管理常常面临着空间不足、穿搭单调及库存过多等挑战&#xff0c;许多人在挑选服饰时难以做出决策。然而&#xff0c;随着技术的进步&#xff0c;智能推荐和自…

编写虚拟的GPIO控制器的驱动程序:和pinctrl的交互使用

往期内容 本专栏往期内容&#xff1a; Pinctrl子系统和其主要结构体引入Pinctrl子系统pinctrl_desc结构体进一步介绍Pinctrl子系统中client端设备树相关数据结构介绍和解析inctrl子系统中Pincontroller构造过程驱动分析&#xff1a;imx_pinctrl_soc_info结构体Pinctrl子系统中c…

【MySQL】数据库整合攻略 :表操作技巧与详解

前言&#xff1a;本节内容讲述表的操作&#xff0c; 对表结构的操作。 是对表结构中的字段的增删查改以及表本身的创建以及删除。 ps&#xff1a;本节内容本节内容适合安装了MySQL的友友们进行观看&#xff0c; 实操更有利于记住哦。 目录 创建表 查看表结构 修改表结构 …

CocoaPods安装步骤详解 - 2024

引言 CocoaPods的安装&#xff0c;如果有VPN就一直开启&#xff0c;会让整个流程非常顺畅。 在现代 iOS 开发中&#xff0c;依赖管理变得越来越重要&#xff0c;CocoaPods 成为开发者们首选的依赖管理工具。它不仅可以简化库的安装与更新&#xff0c;还能帮助开发者更高效地管…

二叉树-堆

树的几个重要定义 1.树根子树根亲缘关系 2.节点的度:有几个子树或根有几个孩子 3.叶子节点:没有孩子的终端节点 度为0 4.分支节点:度不为0的节点 5.树叶子分支节点 6.父亲节点/双亲节点 7.子节点 8.树的度:最大节点的度就是树的度 9.树的层:一般从第一层开始数,也有从0层开始数…

内置RTK北斗高精度定位的4G执法记录仪、国网供电服务器记录仪

内置RTK北斗高精度定位的4G执法记录仪、国网供电服务器记录仪BD311R 发布时间: 2024-10-23 11:28:42 一、 产品图片&#xff1a; 二、 产品特性&#xff1a; 4G性能&#xff1a;支持2K超高清图传&#xff0c;数据传输不掉帧&#xff0c;更稳定。 独立北…

浮动路由:实现出口线路的负载均衡冗余备份。

浮动路由 Tip&#xff1a;浮动路由指在多条默认路由基础上加入优先级参数&#xff0c;实现出口线路冗余备份。 ip routing-table //查看路由表命令 路由优先级参数&#xff1a;越小越优 本次实验测试两条默认路由&#xff0c;其中一条默认路由添加优先级参数&#xff0c;设置…

ssm077铁岭河医院医患管理系统+vue(论文+源码)_kaic

毕业设计&#xff08;论文) 题 目&#xff1a; 医院医患管理系统 姓 名&#xff1a; 学 号&#xff1a; 所属学院&#xff1a; 专业班级&#xff1a; 指导&#xff1a; 职 称&#xff1a; 完成日期 2021年 月 摘 要 21世纪的今天&#xf…

关于在VS中使用Qt不同版本报错的问题

最开始需要配置的地方 首先看一下我的Qt有关的环境变量&#xff1a; Path环境变量里&#xff1a; 这里就是把对应Qt编译器环境下的bin目录放进来&#xff1a;比如你使用的是msvc2017_64或者MinGW QMAKESPEC环境变量&#xff1a; 这个就选择Qt对应的编译器目录下的\mkspecs\w…

Redis 权限控制(ACL)|ACL 命令详解、ACL 持久化

官网文档地址&#xff1a;https://redis.io/docs/latest/operate/oss_and_stack/management/security/acl/ 使用版本&#xff1a;Redis7.4.1 什么是 ACL&#xff1f; ACL&#xff08;Access Control List&#xff09;&#xff0c;权限控制列表&#xff0c;是 Redis 提供的一种…

任务中心全新升级,新增分享接口文档功能,MeterSphere开源持续测试工具v3.4版本发布

2024年11月5日&#xff0c;MeterSphere开源持续测试工具正式发布v3.4版本。 在这一版本中&#xff0c;系统设置方面&#xff0c;任务中心支持实时查看系统即时任务与系统后台任务&#xff1b;接口测试方面&#xff0c;新增接口文档分享功能、接口场景导入导出功能&#xff0c;…

GEE 数据集——美国gNATSGO(网格化国家土壤调查地理数据库)完整覆盖了美国所有地区和岛屿领土的最佳可用土壤信息

目录 简介 代码 引用 网址推荐 知识星球 机器学习 gNATSGO&#xff08;网格化国家土壤调查地理数据库&#xff09; 简介 gNATSGO&#xff08;网格化国家土壤调查地理数据库&#xff09;数据库是一个综合数据库&#xff0c;完整覆盖了美国所有地区和岛屿领土的最佳可用土…

3.PyCharm工具

第三方IDE&#xff0c;集成开发工具&#xff0c;官网下载。 社区版本&#xff0c;免费使用。 创建项目

Rust移动开发:Rust在iOS端集成使用介绍

iOS调用Rust 上篇介绍了 Rust移动开发&#xff1a;Rust在Android端集成使用介绍, 这篇主要看下iOS上如何使用Rust&#xff0c;Rust可以给移动端开发提供跨平台&#xff0c;通用组件支持。 该篇适合对iOS、Rust了解&#xff0c;想知道如何整合调用和编译的&#xff0c;如果想要…

video素材格式转换--mp4转webm(vue3+Nodejs)

总体实现使用ffmpeg 自动化demo实现 vue3Nodejsffmpeg 一、官网下载ffmpeg https://ffmpeg.org/ 1-1选择对应系统下载 1-2下载完成后配置环境变量 1-2-1将下载文件的bin目录配置到环境变量中 例如:D:\ffmpeg\bin 1-3测试ffmpeg是否安装成功 ffmpeg -version 如图 证明安装成…