基于 STM32F407 的串口 IAP

目录

  • 一、概述
  • 二、IAP 实现
  • 三、IAP 程序
    • 1、串口部分
    • 2、iap 程序
    • 3、内部 flash 读写
    • 4、main 程序


IAP(In Application Programming在应用编程)是用户自己的程序在运行过程中对 User Flash 的部分区域进行烧写。简单来说,就是开发者代码出 bug 了或者添加新功能了,能够利用预留的通讯接口,对代码进行升级。

UART、SPI、IIC、USB 等等,当然还有 WIFI、4G、Bluetooth 等无线通讯手段,都可以作为 IAP 升级的方式,今天主要介绍如何使用串口对固件进行升级。

这里有一点需要特殊注意,就是在 MCU 中,有一个特殊区域被称为 System memory 。在这块区域中存放了 ST 公司自己的 Bootloader 程序,它是在 MCU 出厂时,有 ST 固化到芯片中的,后续不能再更改。其中的 Bootloader 程序也可以对 MCU 进行升级(DFU 对芯片的编程应该就是用的这个 Bootloader)。而且,芯片不同,BootLoader 的功能也是有区别的。ST 官网对于这些也是有详细文档的。下图为部分芯片 BootLoader 版本及功能:

一、概述

在学习 IAP 之前,最好先了解一下 SMT32 芯片的启动过程,可以参考:STM32 芯片启动过程。

这里再简单说一下,权威指南讲到:芯片复位后首先会从向量表里面取出两个值:

  • 0x0000 0000 地址取出 MSP(主堆栈寄存器)的值
  • 0x0000 0004 地址取出 PC(程序计数器)的值
  • 然后取出第一条指令执行

不过,STM32 比较特殊,它对地址做了一个重定向(由 MCU 启动配置决定的),一般它是将 0x0000 0000 地址映射到 0x0800 0000,也就是说:

  • 0x0800 0000 地址取出 MSP(主堆栈寄存器)的值
  • 0x0800 0004 地址取出 PC(程序计数器)的值
  • 然后取出第一条指令执行

为什么要设置到 0x0800 0000,而不直接使用 0x0000 0000

因为 STM32 不仅可以从内部 Flash 启动,还可以从系统存储器(可以实现串口 ISP,USB DFU 等程序下载方式,这个程序是 ST 固化好的程序代码)和从内部 SRAM 启动,
我们将内部 Flash 安排到 0x0000 0000 显然是不行的。这样会导致系统存储器或者内部 SRAM 无法重映射到 0x0000 0000 了。

二、IAP 实现

为了实现 IAP,整个程序分为两个部分:

  • Bootloader:引导程序,接收来自串口的固件包并写入 Flash(擦除和写入) 完成升级
  • App:用户程序

两者在 Flash 中的结构如下:

如下图所示流程中:

  • STM32F407 复位后,还是从 0x08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到 IAP 的 main 函数,如图标号①所示;
  • 在执行完 IAP 以后(即将新的 APP 代码写入 STM32F407 的 FLASH,灰底部分。新程序的复位中断向量起始地址为 0x08000004+N+M ),跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的main 函数,如图标号②和③所示,同样 main 函数为一个死循环,并且注意到此时 STM32F407 的 FLASH,在不同位置上,共有两个中断向量表。

在 main 函数执行过程中,如果 CPU 得到一个中断请求:

  1. PC 指针仍然会强制跳转到地址 0x08000004 中断向量表处,而不是新程序的中断向量表,如图标号④所示;
  2. 程序再根据我们设置的中断向量表偏移量,跳转到对应中断源新的中断服务程序中,如图标号⑤所示;
  3. 在执行完中断服务程序后,程序返回 main 函数继续运行,如图标号⑥所示。

三、IAP 程序

1、串口部分

首先,串口是至关重要的一部分,毕竟数据是通过串口传递过来的。

首先是定义了串口数据接收缓冲区的大小为 120 kb,下面的 UART_RX_BUF_BIN 即数据缓冲区,UART_RX_CNT 记录了 UART_RX_BUF_BIN 数组的大小。

// uart.h
#define RX_BUFFER_SIZE 120*1024extern uint8_t 	UART_RX_BUF_BIN[RX_BUFFER_SIZE];
extern uint32_t UART_RX_CNT;

下面是 USART1 的中断处理函数,当有数据发送过来时,就会执行这段代码:

// uart.c
uint8_t UART_RX_BUF_BIN[RX_BUFFER_SIZE] __attribute__ ((at(0X20001000)));    
uint32_t UART_RX_CNT=0;void USART1_IRQHandler(void)
{if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET){uint8_t data = USART_ReceiveData(USART1);if (UART_RX_CNT < RX_BUFFER_SIZE){UART_RX_BUF_BIN[UART_RX_CNT] = data;UART_RX_CNT++;}}
}

可以看到,这里将接收到的数据放到了 UART_RX_BUF_BIN 缓冲区中,方便后面写入到 Flash 中。注意这里:

__attribute__ ((at(0X20001000)))

通过 __attribute__ 将缓冲区放到地址 0X20001000

2、iap 程序

// iap.h
typedef  int (*entry_t)(void);
#define FLASH_APP1_ADDR		0x08010000void bl_iap_load_app(uint32_t appxaddr);	
void bl_iap_write_app_bin(uint32_t appxaddr,uint8_t *appbuf,uint32_t applen);
void MSR_MSP(uint32_t addr);
// iap.c
uint32_t iapbuf[512];void board_lowlevel_deinit(void)
{/* 关闭全局中断 */__disable_irq(); /* 关闭滴答定时器,复位到默认值 */SysTick->CTRL = 0;SysTick->LOAD = 0;SysTick->VAL  = 0;/* 设置所有时钟到默认状态,使用HSI时钟 */RCC_Deinit();/* 关闭所有中断,清除所有字段挂起标志 */for (int i = 0; i < 8; ++i){NVIC->ICER[i] = 0xFFFFFFFF;NVIC->ICPR[i] = 0xFFFFFFFF;}/* 使能全局中断 */__enable_irq();
}/******************************************************************************* @brief      向Flash写入应用程序  * * @param[in]  addr    :     要写入的地址* @param[in]  buf     :     要写入的数据* @param[in]  size    :     数据大小* * @return     none* 
******************************************************************************/
void bl_iap_write_app_bin(uint32_t addr, u8 *buf, uint32_t size)
{uint32_t t;u16 i = 0;uint32_t fwaddr = addr; // 当前写入的地址for (t = 0; t < size; t += 4){iapbuf[i++] = (uint32_t)(buf[t + 3] << 24) |(uint32_t)(buf[t +2] << 16)  | (uint32_t)(buf[t + 1] << 8)  | (uint32_t)(buf[t]);if (i == 512){i = 0;bl_norflash_write(fwaddr, iapbuf, 512);fwaddr += 2048; // 偏移2048  512*4=2048}}if (i) {bl_norflash_write(fwaddr, iapbuf, i); // 将最后的一些内容字节写进去.}
}/******************************************************************************* @brief      跳转到应用程序段* * @param[in]  addr    :    用户代码起始地址* * @return     none* 
******************************************************************************/
void bl_iap_load_app(uint32_t addr)
{if ( ( ( *(volatile uint32_t *)addr ) & 0x2FFE0000 ) != 0x20000000 ) // 检查栈顶地址是否合法.{printf("Stack pointer is not valid!\r\n");return;}uint32_t _sp = *(volatile uint32_t*)(addr + 0);uint32_t _pc = *(volatile uint32_t*)(addr + 4);entry_t app_entry = (entry_t)_pc;           // 用户代码区第二个字为程序开始地址(复位地址)MSR_MSP(_sp);  							    // 初始化APP堆栈指针board_lowlevel_deinit();					// 关中断app_entry();                                // 跳转到APP.
}__asm void MSR_MSP(uint32_t addr)
{MSR MSP, r0;BX r14;
}

有一点需要注意,在由 IAP 跳转到 APP 时, 一定注意把 IAP 中开启的外设全部关闭(包括 SysTick 中断),否则在刚进入 APP 中时,如果产生中断将导致死机等问题。

3、内部 flash 读写

// flash.h
#define STM32_FLASH_BASE 0x08000000 	//STM32 FLASH的起始地址uint32_t bl_norflash_read_word(uint32_t addr);
void bl_norflash_write(uint32_t write_addr,uint32_t *data,uint32_t size);	
void bl_norflash_read(uint32_t read_addr,uint32_t *data,uint32_t size);

内部 flash 的读写操作比较简单。不过,需要注意的是,写操作要注意写之前要保证是没有写过的区域即可。

typedef struct
{uint32_t sector;uint32_t size;
} sector_desc_t;// stm32f4 每个分区的大小描述
static const sector_desc_t sector_descs[] =
{{FLASH_Sector_0, 16 * 1024},{FLASH_Sector_1, 16 * 1024},{FLASH_Sector_2, 16 * 1024},{FLASH_Sector_3, 16 * 1024},{FLASH_Sector_4, 64 * 1024},{FLASH_Sector_5, 128 * 1024},{FLASH_Sector_6, 128 * 1024},{FLASH_Sector_7, 128 * 1024},{FLASH_Sector_8, 128 * 1024},{FLASH_Sector_9, 128 * 1024},{FLASH_Sector_10, 128 * 1024},{FLASH_Sector_11, 128 * 1024},
};uint32_t bl_norflash_read_word(uint32_t faddr)
{return *(volatile uint32_t *)faddr;
}/******************************************************************************* @brief      获取某个地址所在的flash扇区* * @param[in]  addr     :   flash地址* * @return     uint16_t :   0~11,即addr所在的扇区* 
******************************************************************************/
static uint16_t bl_norflash_get_flash_sector(uint32_t addr)
{uint32_t address = STM32_FLASH_BASE;for (uint16_t sector = 0; sector < sizeof(sector_descs) / sizeof(sector_desc_t); ++sector){if (addr < address + sector_descs[sector].size) {return sector_descs[sector].sector;}address += sector_descs[sector].size;}printf("Flash sector not found!");return FLASH_Sector_11;
}/******************************************************************************* @brief 	   从指定地址开始写入指定长度的数据* * @param[in]  write_addr    :    起始地址(此地址必须为4的倍数!!)* @param[in]  data  	     :    要写入的数据* @param[in]  size    		 : 	  写入数据的大小* * @return     none* * @note       1. 该函数对OTP区域也有效!可以用来写OTP区(0X1FFF7800~0X1FFF7A0F)!* 			   2. 因为STM32F4的扇区太大了,没办法本地保存扇区数据,所以本函数*                写地址如果非0XFF,那么会先擦除整个扇区且不保存扇区数据.所以*                写非0XFF的地址,将导致整个扇区数据丢失.建议写之前确保扇区里*                没有重要数据,最好是整个扇区先擦除了,然后慢慢往后写.*    
******************************************************************************/
void bl_norflash_write(uint32_t write_addr, uint32_t *data, uint32_t size)
{if (write_addr < STM32_FLASH_BASE || write_addr % 4) {  // 非法地址printf("Please check the WriteAddr!");return;}					 FLASH_Status status = FLASH_COMPLETE;uint32_t addr_begin = 0;uint32_t addr_end = 0;FLASH_Unlock();				 // 解锁FLASH_DataCacheCmd(DISABLE); // FLASH擦除期间,必须禁止数据缓存/*****************************************************************************/addr_begin = write_addr;					  // 写入的起始地址addr_end = write_addr + size * 4; 	  // 写入的结束地址if (addr_begin < 0X1FFF0000)				  // 只有主存储区,才需要执行擦除操作!!{while (addr_begin < addr_end) // 扫清一切障碍.(对非FFFFFFFF的地方,先擦除){if (bl_norflash_read_word(addr_begin) != 0XFFFFFFFF) // 有非0XFFFFFFFF的地方,要擦除这个扇区{status = FLASH_EraseSector(bl_norflash_get_flash_sector(addr_begin), VoltageRange_3); // VCC=2.7~3.6V之间!!if (status != FLASH_COMPLETE) {printf("Flash erase error!");break; // 发生错误了}}elseaddr_begin += 4;}}if (status == FLASH_COMPLETE){while (write_addr < addr_end) // 写数据{if (FLASH_ProgramWord(write_addr, *data) != FLASH_COMPLETE) // 写入数据{printf("Flash write error!");break; // 写入异常}write_addr += 4;data++;}}/*****************************************************************************/FLASH_DataCacheCmd(ENABLE); // FLASH擦除结束,开启数据缓存FLASH_Lock();				// 上锁
}/******************************************************************************* @brief 	   从指定地址开始读出指定长度的数据* * @param[in]  read_addr    :    起始地址  * @param[in]  data  	    :    存放读取数据* @param[in]  size    		:    要读取数据的大小* * @return     none* 
******************************************************************************/
void bl_norflash_read(uint32_t read_addr, uint32_t *data, uint32_t size)
{if (read_addr < STM32_FLASH_BASE || data == NULL || size == 0){printf("Please check the ReadAddr or the size!");return;}uint32_t i;for (i = 0; i < size; i++){data[i] = bl_norflash_read_word(read_addr); // 读取4个字节.read_addr += 4;							  // 偏移4个字节.}
}

4、main 程序

下面是 main 函数逻辑:

  1. Bootloader 等待 10s
    1. 10s 内如果没有通过串口发送 “yes”,则自动引导进入用户程序
    2. 如果发送了 ”yes“,则会等待用户发送新的固件
      • 等待固件发送完成后,先判断改固件地址信息是否准确
      • 正确则继续执行将其写入 Flash
      • 最后进入用户程序
	uint32_t time = 0;      // 计时(10s)uint32_t oldcount = 0;  // 旧的串口接收数据值uint32_t applenth = 0;  // 接收到的app代码长度uint8_t start_flag = 0; // 开始标志start_printf();while (1){if (UART_RX_CNT && start_flag == 0){if (UART_RX_BUF_BIN[0] == 'y' && UART_RX_BUF_BIN[1] == 'e' && UART_RX_BUF_BIN[2] == 's'){start_flag = 1;printf("请发送更新固件包\r\n");}UART_RX_CNT = 0;}if (UART_RX_CNT && start_flag == 1){if (oldcount == UART_RX_CNT) // 新周期内,没有收到任何数据,认为本次数据接收完成.{applenth = UART_RX_CNT;oldcount = 0;UART_RX_CNT = 0;printf("用户程序接收完成!\r\n");printf("程序包长度: %d Bytes\r\n", applenth);if (applenth){printf("开始更新固件包......\r\n");if ( ( ( *(__IO uint32_t *)(0X20001000 + 4) ) & 0xFF000000 ) == 0x08000000 ) // 判断是否为0X08XXXXXX.{bl_iap_write_app_bin(FLASH_APP1_ADDR, UART_RX_BUF_BIN, applenth); // 更新FLASH代码printf("地址为(0X20001000 + 4): %X\r\n", *(__IO uint32_t *)(0X20001000 + 4));printf("固件包更新完成\r\n");}else{printf("地址错误: %X!!!\r\n", *(__IO uint32_t *)(0X20001000 + 4));}}if ( ( ( *(__IO uint32_t *)(FLASH_APP1_ADDR + 4) ) & 0xFF000000 ) == 0x08000000 ) // 判断是否为0X08XXXXXX.{printf("开始执行Flash应用程序\r\n");bl_iap_load_app(FLASH_APP1_ADDR); // 执行FLASH APP代码}else{printf("地址错误: %X\r\n!!!", *(__IO uint32_t *)(FLASH_APP1_ADDR + 4));}}elseoldcount = UART_RX_CNT;}time++;bl_delay_ms(10);if (time % 100 == 0 && start_flag == 0)printf("倒计时 %d s......\r\n", 11 - time / 100);if (time >= 1000)time = 1000;if (time == 1000 && start_flag == 0){printf("开始运行应用程序\r\n");if ( ( ( *(__IO uint32_t *)(FLASH_APP1_ADDR + 4) ) & 0xFF000000 ) == 0x08000000 ) // 判断是否为0X08XXXXXX.{printf("开始执行FLASH应用程序\r\n");bl_iap_load_app(FLASH_APP1_ADDR); // 执行FLASH APP代码}else{printf("地址错误!!!\r\n");}}}

逻辑比较简单,就不多说了。

下面写一个用户程序来验证一下:

int main(void)
{NVIC_SetPriorityGrouping(NVIC_PriorityGroup_4);/***********************************/NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x10000);/***********************************/RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);bl_led_init();bl_uart_init();bl_tim4_init();printf("\r\n");TIM4_Init(1000 - 1, 8400 - 1);return 0;
}void TIM4_IRQHandler(void)
{if(TIM_GetITStatus(TIM4, TIM_IT_Update) == SET)  //溢出中断{bl_led_toggle(GPIO_Pin_5);TIM_ClearITPendingBit(TIM4, TIM_IT_Update);  //清除中断标志位}  printf("timer 4\r\n");
}

一定要注意这段代码:

NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x10000);

这里设置了用户程序的中断向量表的偏移地址为 0x10000,如果不设置这个偏移地址,就无法进入定时器4 中断服务函数,LED 就不会闪烁。

另外,同时还要注意设置好用户程序的地址。

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

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

相关文章

红外画面空中目标检测系统源码分享

红外画面空中目标检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Comp…

Spring Boot助力IT领域交流平台开发

2 系统关键技术 2.1 JAVA技术 Java是一种非常常用的编程语言&#xff0c;在全球编程语言排行版上总是前三。在方兴未艾的计算机技术发展历程中&#xff0c;Java的身影无处不在&#xff0c;并且拥有旺盛的生命力。Java的跨平台能力十分强大&#xff0c;只需一次编译&#xff0c;…

vmvare虚拟机centos 忘记超级管理员密码怎么办?

vmvare虚拟机centos 忘记超级管理员密码怎么办?如何重置密码呢? 一、前置操作 重启vmvare虚拟机的过程中,长按住Shift键 选择第一个的时候,按下按键 e 进入编辑状态。 然后就会进入到类似这个界面中。 在下方界面 添加 init=/bin/sh,然后按下Ctrl+x进行保存退出。 init=/bi…

开放式耳机的优缺点?哪个品牌专业?好用的开放式蓝牙耳机分享

我相信很多想入手的开放式耳机的家人都想知道开放式耳机是什么&#xff0c;开放式耳机有什么优缺点&#xff0c;开放式耳机是不是智商税、值不值得购买以及如果要购买的话&#xff0c;有什么专业的开放式耳机品牌推荐。因为我毕竟是测评过三十多款开放式耳机的数码博主&#xf…

mdm监管锁系统功能说明

普通用户后台功能说明 设备管理 设备列表 添加的设备列表 点击序列号可以进入设备详细信息 可以修改设备使用人的姓名 手机号 分组等 还可以导出报表 设备信息 展示了一些设备信息和可以下发指令 指令说明 指令分为异步和非异步 下发指令是和手机设备有交互&#xff0c;一…

使用默认不可变的Rust变量会踩什么坑

讲动人的故事&#xff0c;写懂人的代码 Rust的变量真的是名不副实。名字中明明有个“变”字&#xff0c;却默认不可变。还美其名曰“不可变变量”。要想让变量名副其实&#xff0c;还必须费心额外加个mut关键字&#xff0c;并必须称其为“可变变量”&#xff0c;才能与前者区分…

【AI人工智能】文心智能体,陪爸妈去旅游,国庆假期不容错过,旅游搭子首选

文章目录 背景创作灵感陪爸妈去旅游简介角色与目标思考路径个性化开场白调优 智能体体验总结和感受 背景 文心智能体平台&#xff0c;开启新一轮活动&#xff0c;超级创造营持续百日活动。 在AI 浪潮席卷的今天&#xff0c;如雨后春笋般丛生的 AI 应用&#xff0c;昭告着时代风…

MySQL优化实战 解决CPU100%

问题表象 在24年初有一个日经问题困扰着我们&#xff0c;每到正点03分DB的CPU开始打满&#xff0c;持续1分钟又恢复正常水平。但由于日常业务交付压力较大且权限限制没有登录DB主机的权限&#xff0c;大家也就得过且过一直没有去认真排查。直到某天我来兴趣了也有时间了&#…

基于51单片机的家用防火防盗控制系统设计

本设计基于51单片机的家用防火防盗控制系统&#xff0c;该系统通过模块间的协同作用实现了对烟雾与天然气浓度的监测、温度监测、人体红外监测、通信传输、声光报警等功能。利用按键模块设置报警的阈值&#xff0c;将处理后的信息与阈值进行对比。判断气体浓度和温度是否超过阈…

酒店智能门锁SDK接口pro[V10] 门锁校验C#-SAAS本地化-未来之窗行业应用跨平台架构

一、代码 int 酒店标识_int Convert.ToInt32(酒店标识);StringBuilder 锁号2024 new StringBuilder(8);//信息 "未知返回值&#xff1a;" bufCard_原始;GetGuestLockNoByCardDataStr_原始(酒店标识_int, bufCard_原始.ToString(), 锁号2024);StringBuilder 退…

C++语言学习(4): identifier 的概念

1. 什么是 identifier identifier 中文意思是标识符&#xff0c;在 cppreference 中明确提到&#xff0c;identifier 是任意长度的数字、下划线、大写字母、小写字母、unicode 字符 的序列&#xff1a; An identifier is an arbitrarily long sequence of digits, underscores…

nginx打包部署前端vue项目全过程【保姆级教程】

&#x1f939;‍♀️潜意识起点&#xff1a;个人主页 &#x1f399;座右铭&#xff1a;得之坦然&#xff0c;失之淡然。 &#x1f48e;擅长领域&#xff1a;前端 是的&#xff0c;我需要您的&#xff1a; &#x1f9e1;点赞❤️关注&#x1f499;收藏&#x1f49b; 是我持…

Python字符串string方法大全及使用方法[1]以及FastAPI框架文件上传的处理-client使用postman

一、Python字符串string方法大全及使用方法[1] 1. Python字符串string方法大全及意义解释 #将字符串的第一个字符转换为大写 capitalize() #返回一个指定的宽度 width 居中的字符串&#xff0c;fillchar 为填充的字符&#xff0c;默认为空格。 center(width, fillchar) #返…

磁编码器磁铁要求和安装要求

总结来说&#xff0c; 磁铁需要是径向两极充磁、牌号N35、直径10mm、高度2.5mm的烧结钕铁硼磁铁。 磁铁的固定套必须是非导磁材料&#xff0c;比如铜、铝、塑料。 磁铁要距离电机轴至少2mm以上。 磁铁距离磁编码芯片0.5~3mm&#xff0c;最好1到2mm。 使用磁编码器的伺服&#…

css设置文本样式属性

目录 1.font-size&#xff1a;字体大小 案例&#xff1a;通过font-size属性设置字体的大小 1.代码 2.效果 2.font-family:字体的展现形式 案例&#xff1a;使用font-family属性设置字体的风格 1.代码实现 2.效果 3. font-weight:字体的粗细 案例&#xff1a;使用font-weight定义…

基于SSM+Vue技术的定制式音乐资讯平台

文未可获取一份本项目的java源码和数据库参考。 一、选题的背景与意义&#xff1a; 随着个人计算机的普及和互联网技术的日渐成熟&#xff0c;网络正逐渐成为人们获取信息及消费的主要渠道。然而在当前这个信息时代&#xff0c;网络中的信息种类和数量呈现爆炸性增长的趋势&a…

爱拼才会赢,甲骨文公司智算中心标配英伟达GPU10万颗

【科技明说 &#xff5c; 科技热点关注】 之前有有外媒消息&#xff0c;甲骨文宣布推出了多款智算集群&#xff0c;可通过甲骨文云基础设施提供AI训练服务&#xff0c;其中最顶级的一款配备了超过10万块的NVIDIA Blackwell GPU。 它一共使用了多达131072万块B200 GPU加速卡&…

单链表及其代码实现

目录 前言单链表1.1 单链表的定义1.2单链表代码实现1.2.1 头文件1.2.2 函数实现文件1.2.3 测试文件1.2.4 野指针问题 总结 前言 本文介绍单链表&#xff0c;主要是创销、增删改查代码实现。 注&#xff1a;文章中函数命名采取STL库。 单链表 1.1 单链表的定义 单链表是链线…

北京市大兴区启动乐享生活 寻味大兴 美食嘉年华 系列促销费活动

北京市大兴区启动乐享生活 寻味大兴 系列促销费活动 区商务局副局长 兰莉 致开幕辞 区餐饮行业协会会长 董志明 介绍活动内容 2024年9月30日&#xff0c;由大兴区商务局主办、大兴区餐饮行业协会承办&#xff0c;并得到高米店街道和大兴绿地缤纷城大力支持的“乐享生活 寻味大…