当前位置: 首页 > news >正文

RT-Thread启动过程 :从汇编开始的启动流程

        这个系列参考了《嵌入式实时操作系统RT-Thread设计与实现》,会详细介绍RT-Thread的启动流程,即是如何从零开始在开发板上运行起一个RTOS内核的。本文将会以 ch32v307VCT6 开发板为例展开进行详细介绍。主要包括:startup.S、初始化与系统相关的硬件、初始化系统内核对象(例如定时器、调度器、信号)、创建main线程、初始化线程并启动调度器 这五大部分。

        在这一小节中,本文将讲述通过wsl烧录RT-Thread的基本流程。并在最后讲解startup_ch32v30x.S文件的执行过程。

一、系统运行环境

        本文在Linux系统上通过命令行的方式实现RT-Thread的下载与调试。这有几个准备步骤:

  • RT-Thread的源码获取, 在linux命令行中输入如下命令:
    git clone https://gitee.com/rtthread/rt-thread.git
  • 交叉编译工具链的下载:MRS_Toolchain_Linux_x64_V1.92.1.tar.xz 下载地址。下载好后在linux环境中解压。记录好下载路径
  • Scons工具下载: 在Linux命令行中输入以下命令:
    sudo apt install scons

        准备完毕后,切换目录到:./RT_thread/rt-thread/bsp/wch/risc-v/ch32v307v-r1 下运行(下文称其为根目录):

    scons --exec-path=../../../../../Toolchain_linux/MRS_Toolchain_Linux_x64_V1.92.1/RISC-V_Embedded_GCC/bin

        其中--exec-path=后接下载的交叉编译工具链的路径。运行成功如下所示:

        至此在当前目录下会生成rtthread.bin 和 rtthread.elf两个文件。前期的准备工作就到此结束。

二、烧录至开发板

        本文使用wsl,安装教程:WSL2 最新最全帮助小白一步步详细安装教程。

        由于wsl本身不支持usb连接,因此需要把插入电脑的usb(Windows下)接入到wsl(Linux下)中,所以还需要安装usbipd-win开源项目:usbipd-win安装。想要便捷操作的可以额外下载图形化界面wsl usb gui:wsl usb gui 下载。

        下载完成,并且usb成功接入到wsl后,为了方便操作一般都会有一个Makefile执行相关命令,其中的相对路径均从之前下载的交叉编译工具链给出:

CROSS_COMPILE := ../../../../../Toolchain_linux/MRS_Toolchain_Linux_x64_V1.92.1/RISC-V_Embedded_GCC/bin/riscv-none-embed-
OPENOCD_DIR = ../../../../../Toolchain_linux/MRS_Toolchain_Linux_x64_V1.92.1/OpenOCD/bin
OPENOCD = ${OPENOCD_DIR}/openocd
CFG = ${OPENOCD_DIR}/wch-riscv.cfg
GDB := ${CROSS_COMPILE}gdb
OBJDUMP  := ${CROSS_COMPILE}objdump
.DEFAULT_GOAL := allall:scons --exec-path=../../../../../Toolchain_linux/MRS_Toolchain_Linux_x64_V1.92.1/RISC-V_Embedded_GCC/bin.PHONY : flash
flash: @echo "------------------------"@echo "Flashing os.bin to board"@echo "------------------------"@${OPENOCD} -f ${CFG}  -c init -c halt  -c "program rtthread.bin"  -c exit.PHONY : run_openocd
run_openocd: @echo "-----------------------------------------------"@echo "Please manually kill the openocd after gbd quit"@echo "-----------------------------------------------"@${OPENOCD} -f ${CFG} &.PHONY : run_gdb
run_gdb:@${GDB} rtthread.elf -q -ex "target remote : 3333".PHONY : code
code: @${OBJDUMP} -d rtthread.elf > rtthread.asm.PHONY : clean
clean:rm -rf *.o *.bin *.elf

        把以上Makefile文件放在根目录下,并执行sudo make flash,即可成功烧录:

        使用picocom串口工具观察现象,picocom工具直接命令行输入sudo apt install picocom下载。在这里波特率为115200,默认8个数据位,1个停止位,不启用校验位。 /dev/ttyACM0为Linux系统所识别的新插入的开发板USB设备文件。按下ctrl+A   ctrl+X退出。

        当出现如下图所示的 RT 字样即说明成功启动RT-Thread。

三、startup_ch32v30x.S

        现在本文将从汇编文件开始,讲述RT-Thread是如何一步一步启动的。startup_ch32v30x.S位置在:RT_thread/rt-thread/bsp/wch/risc-v/Libraries/ch32v30x_libraries/bmsis/source

        汇编文件中的入口地址为 _start (由链接脚本ENTRY( _start )指定)。汇编文件中第13行至332行均在设置中断向量入口地址。   

        第345行在设置堆栈指针:

        第348行至357行在将.data段中的数据从flash搬移至ram:

        第360行至366行在清零 .bss 段中的数据:

        第368行至373行在写控制状态寄存器(具体作用不知):

        第377行至378行在写mstatus寄存器,mstatus寄存器结构如下图,其中比较重要的位有:

MIE:全局trap(中断和异常)开关。   

MPIE:trap前系统MIE位的状态,也可以理解为:执行mret时,MIE会被赋值位MPIE。 

MPP:trap前系统运行的模式。也可以理解为:mret时系统会运行于哪种模式。 为11则表示系统会运行在机器模式。

FS:11表示启动浮点运算

        因此这里在把MPP写为11后,执行mret就打开了全局trap(中断和异常)的开关 。

        第380行至382行在设置中断向量入口基地址,其中_vector_base在此汇编文件第29行指定。

        第384行至387行在执行开发板时钟初始化(SystemInit),并进入RT-Thread的初始化函数:

        在这里,mepc寄存器中存入了entry函数地址,在mret时,系统会跳转到mepc寄存器存放的地址继续执行,从而实现了entry函数的跳转。

SystemInit函数位置:

RT_thread/rt-thread/bsp/wch/risc-v/Libraries/ch32v30x_libraries/bmsis/source/system_ch32v30x.c:85

        首先执行SystemInit,个人认为由于系统复位,不管是哪种复位形式,寄存器都会被设置为复位值,所以SystemInit中,除了SetSysClock()函数的跳转,其他操作意义不明(我太菜了)。本文使用的ch32v307VCT6属于CH32V30x_D8C类别。

        除非是软件直接跳转到handel_reset执行复位逻辑。

void SystemInit (void)
{RCC->CTLR |= (uint32_t)0x00000001; #ifdef CH32V30x_D8CRCC->CFGR0 &= (uint32_t)0xF8FF0000; 
#elseRCC->CFGR0 &= (uint32_t)0xF0FF0000; 
#endif RCC->CTLR &= (uint32_t)0xFEF6FFFF;RCC->CTLR &= (uint32_t)0xFFFBFFFF;RCC->CFGR0 &= (uint32_t)0xFF80FFFF;#ifdef CH32V30x_D8CRCC->CTLR &= (uint32_t)0xEBFFFFFF;RCC->INTR = 0x00FF0000;RCC->CFGR2 = 0x00000000;
#elseRCC->INTR = 0x009F0000;   
#endif   SetSysClock();
}

在SetSysClock中将系统时钟设置为定义好的144MHz:

static void SetSysClock(void)
{
#ifdef SYSCLK_FREQ_HSESetSysClockToHSE();
#elif defined SYSCLK_FREQ_24MHzSetSysClockTo24();
#elif defined SYSCLK_FREQ_48MHzSetSysClockTo48();
#elif defined SYSCLK_FREQ_56MHzSetSysClockTo56();  
#elif defined SYSCLK_FREQ_72MHzSetSysClockTo72();
#elif defined SYSCLK_FREQ_96MHzSetSysClockTo96();
#elif defined SYSCLK_FREQ_120MHzSetSysClockTo120();
#elif defined SYSCLK_FREQ_144MHz  //仅144MHz有宏定义SetSysClockTo144();#endif/* If none of the define above is enabled, the HSI is used as System clock* source (default after reset) */ 
}

        在SetSysClockTo144()函数中。主要工作在 启用HSE时钟,配置PLL倍频、设置系统时钟、HCLK、PB1\PB2外设时钟

        逻辑是先启用HSE外部8MHz高速时钟,并PLL 18倍频至 144MHz 作为SYSCLOCK系统时钟。 系统时钟不分频作为HCLK的输入。 HCLK不分频作为PB2CLK输入, HCLK 2分频作为PB1CLK输入。

static void SetSysClockTo144(void)
{__IO uint32_t StartUpCounter = 0, HSEStatus = 0;RCC->CTLR |= ((uint32_t)RCC_HSEON); //启用外部高速时钟/* 等待HSE时钟准备就绪 */do{HSEStatus = RCC->CTLR & RCC_HSERDY;StartUpCounter++;} while((HSEStatus == 0) && (StartUpCounter != HSE_STARTUP_TIMEOUT));/* 判断HSE是否准备就绪 */if ((RCC->CTLR & RCC_HSERDY) != RESET){HSEStatus = (uint32_t)0x01;}else{HSEStatus = (uint32_t)0x00;}/* 如果HSE准备就绪则配置PLL倍频、SYSCLK、HCLK、PB1CLK、PB2CLK */if (HSEStatus == (uint32_t)0x01){/* HCLK = SYSCLK */RCC->CFGR0 |= (uint32_t)RCC_HPRE_DIV1; //设置HCLK为SYSCLK不分频输入/* PCLK2 = HCLK */RCC->CFGR0 |= (uint32_t)RCC_PPRE2_DIV1; //设置PB2CLK为HCLK不分频输入/* PCLK1 = HCLK/2 */RCC->CFGR0 |= (uint32_t)RCC_PPRE1_DIV2; //设置PB1CLK为HCLK2分频输入/*  PLL 配置: PLLCLK = HSE * 18 = 144 MHz */RCC->CFGR0 &= (uint32_t)((uint32_t)~(RCC_PLLSRC | RCC_PLLMULL));#ifdef CH32V30x_D8RCC->CFGR0 |= (uint32_t)(RCC_PLLSRC_HSE | RCC_PLLXTPRE_HSE | RCC_PLLMULL18);
#elseRCC->CFGR0 |= (uint32_t)(RCC_PLLSRC_HSE | RCC_PLLMULL18_EXTEN); //配置PLL时钟源为HSE,并设置18倍频。 实际上这里配置的PLL时钟源是prediv1,只不过prediv1系统默认输入是HSE不分频输入。
#endif/* 启用 PLL */RCC->CTLR |= RCC_PLLON;/* 等待 PLL 准备就绪 */while((RCC->CTLR & RCC_PLLRDY) == 0){}/* 设置 PLL倍频时钟 作为系统时钟 */RCC->CFGR0 &= (uint32_t)((uint32_t)~(RCC_SW));RCC->CFGR0 |= (uint32_t)RCC_SW_PLL;/* Wait till PLL is used as system clock source */while ((RCC->CFGR0 & (uint32_t)RCC_SWS) != (uint32_t)0x08){}}else{/** If HSE fails to start-up, the application will have wrong clock* configuration. User can add here some code to deal with this error*/}
}

        至此系统从汇编文件启动,到跳转到RT-Thread的初始化入口entry函数的逻辑就讲解完毕。这一部分主要是开发板启动的常规操作(数据段搬运、bss段清零、中断向量设置),以及 系统时钟的配置。接下来将介绍entry函数中是如何对RT-Thread进行初始化的。

四、RT-Thread初始化

        单核初始化过程包括:中断关闭、板级初始化、定时器初始化、调度器初始化、信号初始化、初始化创建应用main线程、初始化创建定时器线程、空闲线程初始化、僵尸线程初始化、打开中断并进行线程调度。

int rtthread_startup(void)
{
#ifdef RT_USING_SMPrt_hw_spin_lock_init(&_cpus_lock);
#endifrt_hw_local_irq_disable(); //关中断/* board level initialization* NOTE: please initialize heap inside board initialization.*/rt_hw_board_init(); //板级初始化/* show RT-Thread version */rt_show_version();/* timer system initialization */rt_system_timer_init(); //定时器初始化/* scheduler system initialization */rt_system_scheduler_init(); //调度器初始化#ifdef RT_USING_SIGNALS/* signal system initialization */rt_system_signal_init();
#endif /* RT_USING_SIGNALS *//* create init_thread */rt_application_init(); //创建用户线程/* timer thread initialization */rt_system_timer_thread_init(); //定时器线程初始化/* idle thread initialization */ rt_thread_idle_init(); //空闲线程初始化/* defunct thread initialization */rt_thread_defunct_init(); //僵尸线程初始化#ifdef RT_USING_SMPrt_hw_spin_lock(&_cpus_lock);
#endif /* RT_USING_SMP *//* start scheduler */rt_system_scheduler_start(); //线程调度,开中断并执行main函数/* never reach here */return 0;
}

        其中rt_hw_boart_init板级初始化过程需要我们根据具体的开发板进行修改。需要在其中完成系统时钟配置、为系统提供心跳、串口初始化、将系统输入输出终端绑定到这个串口。

extern uint32_t SystemCoreClock;static uint32_t _SysTick_Config(rt_uint32_t ticks)
{NVIC_SetPriority(SysTicK_IRQn, 0xf0);NVIC_SetPriority(Software_IRQn, 0xf0);NVIC_EnableIRQ(SysTicK_IRQn);NVIC_EnableIRQ(Software_IRQn);SysTick->CTLR = 0;SysTick->SR = 0;SysTick->CNT = 0;SysTick->CMP = ticks - 1;SysTick->CTLR = 0xF;return 0;
}/*** This function will initial your board.*/
void rt_hw_board_init()
{/* System Tick Configuration */_SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND);#if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP)rt_system_heap_init((void *) HEAP_BEGIN, (void *) HEAP_END);
#endif/* USART driver initialization is open by default */
#ifdef RT_USING_SERIALrt_hw_usart_init();
#endif
#ifdef RT_USING_CONSOLErt_console_set_device(RT_CONSOLE_DEVICE_NAME);
#endif
#ifdef RT_USING_PIN/* pin must initialized before i2c */rt_hw_pin_init();
#endif/* Call components board initial (use INIT_BOARD_EXPORT()) */
#ifdef RT_USING_COMPONENTS_INITrt_components_board_init();
#endif}void SysTick_Handler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
void SysTick_Handler(void)
{GET_INT_SP();/* enter interrupt */rt_interrupt_enter();SysTick->SR = 0;rt_tick_increase();/* leave interrupt */rt_interrupt_leave();FREE_INT_SP();}

相关文章:

RT-Thread启动过程 :从汇编开始的启动流程

这个系列参考了《嵌入式实时操作系统RT-Thread设计与实现》,会详细介绍RT-Thread的启动流程,即是如何从零开始在开发板上运行起一个RTOS内核的。本文将会以 ch32v307VCT6 开发板为例展开进行详细介绍。主要包括:startup.S、初始化与系统相关的…...

Scala—“==“和“equals“用法(附与Java对比)

Scala 字符串比较—""和"equals"用法 Scala 的 在 Scala 中, 是一个方法调用,实际上等价于调用 equals 方法。不仅适用于字符串,还可以用于任何类型,并且自动处理 null。 Demo: Java 的 在 J…...

$route和$router的区别

在 Vue.js 中,$route 和 $router 都是 Vue Router 提供的对象,但它们有不同的用途和功能。 1. $router $router 是 Vue Router 实例的引用,它允许你通过 JavaScript 进行路由的控制和导航。你可以通过 $router 来执行路由的操作&#xff0c…...

[工具升级问题] 钉钉(linux版)升级带来的小麻烦

本文由Markdown语法编辑器编辑完成。 1. 背景: 今日钉钉又发布了新的升级版本。由于我工作时使用的是Ubuntu 20.04版本,收到的升级推送信息是,可以升级到最新的7.6.25-Release版本。根据钉钉官方给出的历次更新版说明,这个新的版本&#xf…...

Leetcode经典题13--接雨水

题目描述 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。 输入输出示例 输入:height [0,1,0,2,1,0,1,3,2,1,2,1] 输出:6 解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1…...

yarn修改缓存位置

查看缓存位置 以下三个命令分别为:bin是yarn存储命令的二进制文件,global存储全局node_modules ,cache存储用下下载缓存,查看本机目前的目录: 查看bin目录命令:yarn global bin 查看global目录命令&…...

OpenHarmony-3.HDF input子系统(5)

HDF input 子系统OpenHarmony-4.0-Release 1.Input 概述 输入设备是用户与计算机系统进行人机交互的主要装置之一,是用户与计算机或者其他设备通信的桥梁。常见的输入设备有键盘、鼠标、游戏杆、触摸屏等。本文档将介绍基于 HDF_Input 模型的触摸屏器件 IC 为 GT91…...

RabbitMQ 消息持久化/镜像队列/lazy对时延影响

测试背景: 不同条件下RabbitMQ不同队列类型的生产时延测试: 测试环境: 机型:rabbimtq.2u4g.cluster 背景流量:1000 TPS 测试条件: 消息大小 4k,消息条数为1000条,时延取值为平均…...

【深度学习】深刻理解Swin Transformer

Swin Transformer 是一种基于 Transformer 的视觉模型,由 Microsoft 研究团队提出,旨在解决传统 Transformer 模型在计算机视觉任务中的高计算复杂度问题。其全称是 Shifted Window Transformer,通过引入分层架构和滑动窗口机制,S…...

[2015~2024]SmartMediaKit音视频直播技术演进之路

技术背景 2015年,因应急指挥项目需求,我们实现了RTMP推送音视频采集推送(采集摄像头和麦克风数据)模块,在我们做好了RTMP推送模块后,苦于没有一个满足我们毫秒级延迟诉求的RTMP播放器,于是第一…...

redis 使用Lettuce 当redis挂掉重启之后 网络是怎么重新连接

Lettuce是一个高性能的Java Redis客户端,支持同步、异步和反应式编程模式 Lettuce的核心功能包括: ‌高性能‌:通过使用Netty作为底层网络通信框架,实现了非阻塞IO,提高了性能。‌丰富的API‌:提供了丰富…...

【IntelliJ IDEA 集成工具】TalkX - AI编程助手

前言 在数字化时代,技术的迅猛发展给软件开发者带来了更多的挑战和机遇。为了提高技术开发群体在繁多项目中的编码效率和质量,他们需要一个强大而专业的工具来辅助开发过程,而正是为了满足这一需求,TalkX 应运而生。 一、概述 1…...

二叉搜索树Ⅲ【东北大学oj数据结构8-3】C++

二叉搜索树 III B:在二叉搜索树II中加入delete指令,创建程序对二叉搜索树T执行如下指令。 插入 k:将key k 插入到 T 中。 find k:报告T中是否存在key k。 delete k:删除key为 k 的节点。 打印:使用中序树遍…...

【面试笔记】CPU 缓存机制

CPU 缓存机制 1. CPU Cache 与 MMU1.1 MMU 是什么?TLB 又是什么?他们是怎么工作的?2.2 简述 Cache 与 MMU 的协作关系?2.3 简述 Cache 与 MMU 的协作工作流程? 2. CPU 多层次缓存2.1 什么是 CPU 的多层次缓存结构&…...

MySQL基础函数使用

目录 简介 1. 单行函数 1.1 字符串函数 1.2 日期函数 1.3 数值函数 1.4 转换函数 1.5 其他函数 2. 多行函数 示例: 3. 数据分组 示例: 4. DQL单表关键字执行顺序 示例: 5. 多表查询 示例: 6. 表与表的外连接 示例…...

解决docker环境下aspose-words转换word成pdf后乱码问题

描述 环境&#xff1a;docker 部署工具&#xff1a;Jenkins 需求&#xff1a;本地上传的word文档需要转换成pdf 问题&#xff1a;转换之后的pdf文档出现小框框&#xff08;乱码&#xff09; 转换成PDF的操作 pom&#xff1a; <dependency><groupId>org.apach…...

C# 生成随机数的方法

C# 提供了一种强大而方便的工具类 Random &#xff0c;用于生成随机数。这里将分类讨论如何通过 C# 实现随机数生成&#xff0c;以及应用于实际情况中的一些具体方案。 一、Random 类概述 Random 类表示一个伪随机数生成器&#xff0c;用于生成满足随机性统计要求的数字序列。…...

ip_done

文章目录 路由结论 IP分片 数据链路层重谈Mac地址MAC帧报头局域网的通信原理MSS&#xff0c;以及MAC帧对上层的影响ARP协议 1.公司是不是这样呢? 类似的要给运营商交钱&#xff0c;构建公司的子网&#xff0c;具有公司级别的入口路由器 2&#xff0e;为什么要这样呢?? IP地…...

3D可视化引擎HOOPS Visualize与HOOPS Luminate Bridge的功能与应用

HOOPS Visualize HPS / HOOPS Luminate Bridge为开发者提供了强大的工具&#xff0c;用于在CAD应用中集成逼真的渲染能力。本文旨在梳理该桥接产品的核心功能、使用方法及应用场景&#xff0c;为用户快速上手并充分利用产品特性提供指导。 桥接产品的核心功能概述 HOOPS Lumi…...

Docder 搭建Redis分片集群 散片插槽 数据分片 故障转移 Java连接

介绍 使多个 Redis 实例共同工作&#xff0c;实现数据的水平扩展。通过将数据分片到多个节点上&#xff0c;Redis 集群能够在不牺牲性能的前提下扩展存储容量和处理能力&#xff0c;从而支持更高并发的请求。Redis 集群不仅支持数据分片&#xff0c;还提供了自动故障转移和高可…...

校园交友app/校园资源共享小程序/校园圈子集合二手物品交易论坛、交友等综合型生活服务社交论坛

多客校园社交圈子系统搭建 校园交友多功能系统源码: 1、更改学校为独立的模块。整体UI改为绿色&#xff0c;青春色&#xff0c;更贴近校园风格。2、圈子归纳到学校去进行运营。每个学校可建立多个圈子。和其他学校圈子互不干扰。3、增加用户绑定学校&#xff0c;以后进入将默认…...

Chaos Mesh云原生的混沌测试平台搭建

Chaos Mesh云原生的混沌测试平台搭建 一.环境准备 ​ 确认已经安装helm&#xff0c;如要查看 Helm 是否已经安装&#xff0c;请执行如下命令&#xff1a; helm version二.使用helm安装 1.添加 Chaos Mesh 仓库 ​ 在 Helm 仓库中添加 Chaos Mesh 仓库&#xff1a; helm re…...

Vue3之组合式API详解

Vue 3引入了一种新的API风格——组合式API&#xff08;Composition API&#xff09;&#xff0c;旨在提升组件的逻辑复用性和可维护性。本文将详细阐述Vue 3中的组合式API&#xff0c;包括其定义、特点、使用场景、优势等&#xff0c;并给出具体的示例代码。 一、定义 组合式…...

大模型的构建与部署(3)——数据标注

版权声明 本文原创作者:谷哥的小弟作者博客地址:http://blog.csdn.net/lfdfhl1. 数据标注的重要性 1.1 增强数据可解释性 数据标注通过为原始数据添加标签或注释,显著增强了数据的可解释性。在机器学习和深度学习领域,模型的训练依赖于大量带标签的数据。这些标签不仅帮助…...

AI发展与LabVIEW程序员就业

人工智能&#xff08;AI&#xff09;技术的快速发展确实对许多行业带来了变革&#xff0c;包括自动化、数据分析、软件开发等领域。对于LabVIEW程序员来说&#xff0c;AI的崛起确实引发了一个值得关注的问题&#xff1a;AI会不会取代他们的工作&#xff0c;导致大量失业&#x…...

本地事务 + 消息队列事务方案设计

Spring Boot 和 RocketMQ 在Spring Boot项目中实现“本地事务 消息队列事务”的方案&#xff0c;可以按照以下步骤实现&#xff1a; 先执行MySQL本地事务操作&#xff08;未提交&#xff09;随后发送消息到消息队列&#xff08;如RocketMQ事务消息&#xff09;等待消息队列确…...

pinctrl子系统学习笔记

一、背景 cpu的gpio引脚可以复用成多个功能&#xff0c;如可以配置成I2C或者普通GPIO模式。配置方式一般是通过写引脚复用的配置寄存器&#xff0c;但是不同芯片厂商配置寄存器格式内容各不相同&#xff0c;设置引脚复用无法做到通用且自由的配置&#xff0c;只能在启动初始化…...

使用vue-element 的计数器inputNumber,传第三个参数

使用vue-element 的计数器inputNumber。 其中的change 事件中&#xff0c;默认自带两个参数&#xff0c;currentValue和oldValue&#xff0c;分别代表改变后的数和改变前的数&#xff0c; 如果想要传第三个参数&#xff0c; change"(currentValue, oldValue) > numCha…...

如何从0构建一个flask项目,直接上实操!!!

项目结构 首先&#xff0c;创建一个项目目录&#xff0c;结构如下&#xff1a; flask_app/ │ ├── app.py # Flask 应用代码 ├── static/ # 存放静态文件&#xff08;如CSS、JS、图片等&#xff09; │ └── style.css # 示例…...

Mongoose连接数据库操作实践

文章目录 介绍特点&#xff1a;Mongoose 使用&#xff1a;创建项目并安装&#xff1a;连接到 MongoDB&#xff1a;定义 Schema&#xff1a;创建模型并操作数据库&#xff1a;创建文档&#xff1a;查询文档&#xff1a;更新文档&#xff1a;删除文档&#xff1a;使用钩子&#x…...