news 2026/10/21 18:38:35

嵌入式Day18:中断机制与定时器原理及PWM综合实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
嵌入式Day18:中断机制与定时器原理及PWM综合实践

嵌入式学习Day18,基本意味着你已经摸到了整个嵌入式体系里最有分量的那一块:中断和定时器。前几十天用GPIO点灯、按键扫描、串口打印,说到底还是在让芯片“顺着代码一行一行走”。但从Day18开始,你要面对的是芯片“自己主动干活”的机制,以及“让时间可控”的手段。这篇文章就是我这一天的完整学习记录,包含中断系统怎么理解、定时器怎么算参数、外部中断怎么做按键检测、PWM怎么输出呼吸灯效果,以及我在实际操作中踩过的坑和排查思路。适合正处在Day15到Day25左右的你,尤其是准备把中断和定时器彻底吃透的人,这篇可以直接照着做。

很多人学嵌入式会在中断这块卡住,不是因为难,而是因为前面的路太好走了。点灯、按键、串口都是“我命令你做什么,你就做什么”,代码看得见摸得着。但中断引入了一整套新的思维方式:芯片可能在执行main函数里任何一条指令时,突然被某个事件拉走,处理完再回来。这种“打断”机制一开始确实反直觉。定时器则更是如此,它让芯片自己计时、自己触发,完全不需要CPU在那边傻等。

我当时Day18的思路很明确:第一,把中断原理彻底弄清楚;第二,把定时器工作机制拆明白;第三,把两者组合起来做一个看得见摸得着的小实验。整个晚上我只做了一件事——用外部中断检测按键,用定时器中断做周期性动作,再用PWM输出控制LED亮度变化。今天回头看,这一天学到的东西,比前面十几天加起来都值钱。

1. Day18学习路线定位与内容拆解

1.1 Day18在整个嵌入式学习排程中的位置

嵌入式学习如果按排程来看,前两周大概是这样:第一天到第三天搭环境、学C语言基础,后面连续几天下班式地刷GPIO实验,LED亮灭、蜂鸣器响、按键输入。到了Day18左右,已经具备最基础的外设操作能力,字符串格式化、指针、结构体也都能用一点了,这时候继续点灯意义不大,应该开始进入真正决定嵌入式水平的核心机制。

Day18就是“从基本功走向工程化”的临界点。这个阶段的任务不是多做一个灯,而是把“程序怎么运行”“中断怎么发生”“时间怎么度量”这三件事搞清楚。如果说前面的学习是认识一颗芯片的皮肤,Day18开始就是在认识它的骨架和神经系统。中断就是神经系统,定时器就是心脏节律,这两者的组合,几乎覆盖了后续所有复杂项目的地基。

为什么非要强调Day18这个节点?因为我见过太多人在这里掉队。有些人在中断里写大循环,导致主程序卡死;有些人定时器参数全靠抄,永远不知道自己算的1ms为什么变成了1.2ms;还有些人连NVIC优先级分组都没搞明白,就急着用HAL库一路填API。这些坑,如果在这一天不踩一遍,后面会在DMA、RTOS、通信协议里被无限放大。

1.2 中断与定时器为何是嵌入式学习的“分水岭”

先说中断。嵌入式系统里的CPU不可能一直轮询检查某个事件有没有发生,那样太浪费算力了。中断机制的本质是“设备主动通知CPU”。打个比方,你在餐厅后厨做饭,不能每秒钟回头看一眼门口有没有客人进来,正确做法是门口装个铃铛,客人按铃你才知道要去接待。这个铃铛就是中断源,铃声响起就是触发信号,你放下炒勺去开门就是中断服务程序,处理完回到灶台继续炒菜就是中断返回。芯片里的GPIO检测、定时器溢出、串口收到数据,都是通过这种方式通知CPU的。

定时器的作用更基础也更朴素——让芯片知道时间在流逝。芯片内部跑的是时钟脉冲,定时器本质上就是一个对着脉冲数数的计数器,数到设定值就产生一个事件。这个机制可以用来做延时、做调度、做频率测量、做PWM输出。有了定时器,你才能让芯片在“不阻塞CPU”的情况下精确地每隔多少毫秒执行一次任务。

Day18之所以说是分水岭,是因为这两者一旦掌握,你就从“只会写顺序代码”变成“能给芯片设计并发逻辑”。举个实际场景,一个系统要同时处理按键输入、LED亮灭、串口收发、传感器数据采集,如果全用顺序轮询,代码会越写越乱,响应速度也跟不上。中断+定时器让这些问题变得可以拆解,所有事情各归各的触发源,主程序只需要处理逻辑,剩下的交给硬件去触发。

1.3 这篇文章的学习目标与适合人群

我写这篇记录的时候,设定的目标是三个:第一,能用自己的话解释中断的完整流程;第二,能不查手册、直接算出定时器中断周期和PWM频率所需的分频系数与重装载值;第三,完成一个中断+定时器+PWM联动的综合小实验,而不是只跑单个例程。

适合阅读这篇文章的人,是已经能独立完成GPIO控制、并且知道怎么用开发环境编译下载程序的学习者。如果你还没搞定这些,建议先把基础补上再来看,不然部分代码和术语会有点吃力。如果你已经有一定基础,可以直接跳到第三部分的实操代码和第四部分的排查经验,那几段都是我当天真正遇到的问题。

2. 核心知识点拆解:中断系统与定时器原理

2.1 中断系统的完整流程与优先级机制

中断这个机制,拆开来其实只有五步:中断源产生触发信号、标志位被置位、CPU响应中断、保护现场并跳转到中断服务函数、执行完毕后恢复现场并返回原程序。这五步中,真正需要开发者重点关注的是标量位与优先级。

中断标志位是理解中断的关键。每一个中断源都有一个对应的标志位,中断事件发生时会自动置1,CPU执行完中断服务函数后需要手动清零,表示“这个中断已经处理过了”。如果不清标志,最典型的后果就是中断被反复触发,整个程序看起来像“卡死”了一样。我那天第一次做外部中断时就栽在这个上面,后面细讲。

优先级机制是嵌入式中断的另一个重头戏。优先级可以简单理解为:多个中断同时到达时,CPU先处理谁,以及高优先级中断能不能打断低优先级中断。我用的这颗Cortex-M3内核MCU,优先级分组可以设置抢占优先级和子优先级的位数比例。抢占优先级越高,越能打断正在处理的低优先级中断;子优先级则是在抢占级别相同时,决定谁先执行。配置NVIC时一定要先确定分组方式,再给每个中断源分配优先级,否则可能出现低优先级任务把高优先级任务堵住的诡异现象。

中断服务程序一定要短小精悍。这是嵌入式开发的铁律,因为中断是在打断主程序的情况下执行的,如果中断里写了一堆运算、延时或者循环等待,主程序的实时性就会被牺牲。正确做法是中断里只做标记、清标志、极少量数据读写,具体的耗时逻辑放到主循环里处理。用一个全局变量作为标志位,中断里把它置1,主循环检测到标志后再做真正的事,这是最简单也最可靠的模式。

2.2 定时器的计数原理、分频与重装载

定时器的工作原理并不复杂,本质上是一个数数器。它对着输入时钟脉冲一格格往上加,加到预先设置好的上限值时会溢出,然后产生一个中断或更新事件,同时计数器自动回到初始值重新开始。这个“上限值”在寄存器里叫自动重装载值(ARR),输入时钟要先经过预分频器(PSC)分频,然后计数器再对这个分频后的时钟进行计数。

这里有个最容易犯错的地方:分频系数和重装载值都要减一。因为计数器从0开始计数,比如想得到1000个脉冲,只需要从0到999,所以预分频寄存器要写期望分频倍数减一,重装载寄存器也要写期望计数次数减一。如果直接写1000,实际就变成了1001个脉冲周期。

定时器的工作模式不只是“从0加到上限”这一种。向上计数是最常用的,还有些定时器支持向下计数和中央对齐模式。中央对齐模式下计数器先向上计数到ARR,再向下计数到0,多用于需要对称波形的PWM应用。此外还有输出比较模式和输入捕获模式:输出比较用于在指定计数值时刻产生翻转或者PWM波形,输入捕获用于测量外部信号的高电平宽度或周期。Day18阶段先把向上计数和PWM模式练熟,后两个看懂原理即可。

2.3 寄存器、库函数与HAL之间的关系

学习到Day18,一定会在代码里看到类似“TIM2->PSC = 71”和“HAL_TIM_Base_Start_IT(&htim2)”这样的写法。一个是寄存器操作,一个是库函数封装。很多人喜欢二选一,我的建议是都得碰。库函数方便,但如果你完全不知道它背后操作了哪个寄存器、为什么配置顺序必须是这样,出了问题就像无头苍蝇。寄存器操作繁琐,但能让寄存器手册上的每一位配置和你建立直觉。

单纯用寄存器写定时器初始化的典型流程是:先把定时器所在总线的时钟使能,然后设置预分频寄存器、自动重装载寄存器,再设置计数方向和控制位,接着清理更新标志位,开启定时器更新中断,最后在NVIC里使能定时器中断并配置优先级。这个过程看着繁琐,但其实每一步都能在手册里找到对应寄存器,调通一次之后,你再看任何HAL代码都能直接想象出它底层在干什么。

我实操时用库函数写了大部分代码,但每写一个外设,一定会翻开参考手册找到对应的寄存器页,看一眼默认值和配置位。这个习惯帮我在后面排查定时器时间不准的问题时立了大功——因为库函数把APB1和APB2分频后的时钟隐藏了,如果不去计算实际时钟,定时时间一定会算错。

3. 实操环节:中断+定时器+PWM综合小实验

3.1 实验设计与硬件准备

我做这个实验的目标很简单:按下按键,通过外部中断切换LED的呼吸灯速度档位;同时通过定时器中断,让另一个LED以精确的1Hz频率闪烁,用来验证定时是否准确。整个系统跑起来后,一个LED自然呼吸,一个LED精确闪烁,按一下按键,呼吸速度改变。

硬件方面用了一块常见的Cortex-M3内核开发板,一个外接按键、两个LED、几根杜邦线。按键接在支持外部中断的引脚上,LED分别接在普通GPIO和PWM输出通道上。接按键时要特别注意引脚电平状态,一般用内部上拉电阻,按键一端接地,平时引脚读到高电平,按下时变为低电平,这样产生一个下降沿触发中断,处理起来非常干净。

实验流程我拆成了三步推进:先做定时器中断,验证周期性中断能精确翻转LED;再做外部中断按键检测,验证每次按键都能稳定触发;最后做PWM呼吸灯,并把按键切换档位的逻辑接进去。一个功能一个功能地验证,比一次性拼一大段代码再调试优雅得多。

3.2 外部中断实现按键检测的代码与要点

外部中断的配置分成三块:GPIO、EXTI(外部中断控制器)和NVIC。GPIO负责把引脚配置成输入模式并启用上拉电阻,EXTI负责把引脚的边沿变化转换成中断请求,NVIC负责决定这个中断的优先级并允许CPU响应。

完整初始化可以直接参考下面这段代码框架:

void EXTI_Key_Init(void) { GPIO_InitTypeDef gpio = {0}; EXTI_ConfigTypeDef exti = {0}; NVIC_InitTypeDef nvic = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_SYSCFG_CLK_ENABLE(); gpio.Pin = GPIO_PIN_0; gpio.Mode = GPIO_MODE_INPUT; gpio.Pull = GPIO_PULLUP; HAL_GPIO_Init(GPIOA, &gpio); __HAL_SYSCFG_EXTI_LINE_SEL_GPIOA(); exti.Line = EXTI_LINE_0; exti.Mode = EXTI_MODE_INTERRUPT; exti.Trigger = EXTI_TRIGGER_FALLING; exti.LineCmd = ENABLE; HAL_EXTI_SetConfigLine(&exti); nvic.IRQChannel = EXTI0_IRQn; nvic.IRQChannelPreemptionPriority = 2; nvic.IRQChannelSubPriority = 0; nvic.IRQChannelCmd = ENABLE; HAL_NVIC_EnableIRQ(nvic); }

中断响应的入口和回调处理是这样:

void EXTI0_IRQHandler(void) { HAL_EXTI_IRQHandler(&hexti); } void HAL_EXTI_Callback(uint32_t line) { if (line == EXTI_LINE_0) { key_flag = 1; } }

这里最大的坑就是变量在中断和主循环之间共享的问题。中断函数里修改了key_flag,主循环读取它,如果主循环正在读的中间状态,中断又强行改了值,可能出现难以复现的bug。Day18阶段最简单可靠的解法是把共享变量定义成volatile类型,并保证读写是原子的。字符级的int读写一般没问题,但如果要在中断和主循环之间传递更复杂的数据结构,就需要临时关闭中断对这段读操作做保护。

另一个非常容易踩的坑是按键消抖。机械按键在按下和释放的瞬间,金属触点会经历微秒到毫秒级的抖动,引脚电平在高低之间快速跳变数次。如果只靠外部中断边沿触发,一次按键可能会触发多次中断。我的处理方式是中断里只把key_flag置1,不做任何消抖,复制到主循环里检测到标志后,延时20毫秒再读一次引脚电平,如果确实读到了低电平,才判定为一次有效按键。这样既没有在中断里做耗时操作,又起到了消抖作用。

3.3 定时器中断实现毫秒级精确定时

定时器的配置关键在于搞清楚时钟频率。我这块开发板的主时钟是72MHz,挂在APB1总线上的定时器时钟经过两倍频后也是72MHz,APB2总线上的也是72MHz。定时器的计算方式是这样的:

计数频率 = 定时器时钟 / (预分频系数 + 1) 定时中断周期 = (自动重装载值 + 1) / 计数频率

我想得到一个1毫秒的定时中断,那就设计数频率为1kHz,自动重装载值为999。这样计数器每从0数到999,就正好是1毫秒。预分频值PSC等于72减一,也就是71;ARR等于1000减一,也就是999。全部写在代码里就是这样:

void TIM_Base_Init(void) { TIM_HandleTypeDef htim = {0}; __HAL_RCC_TIM2_CLK_ENABLE(); htim.Instance = TIM2; htim.Init.Prescaler = 71; htim.Init.CounterMode = TIM_COUNTERMODE_UP; htim.Init.Period = 999; htim.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_Base_Init(&htim); HAL_NVIC_SetPriority(TIM2_IRQn, 1, 0); HAL_NVIC_EnableIRQ(TIM2_IRQn); HAL_TIM_Base_Start_IT(&htim); }

定时器中断回调里,我用一个计数变量累加。每1000次就意味着过了1000个1毫秒,也就是1秒,此时翻转一次LED。这里我刻意不用HAL_Delay函数,就是想验证定时器中断的精确度。

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { tick_ms++; if (tick_ms >= 1000) { tick_ms = 0; HAL_GPIO_TogglePin(LED_PORT, LED_PIN); } } }

实测下来,用手机秒表对时间,LED闪一分钟基本能控制在59到60次之间,对这个阶段的学习而言已经足够精确。后来我把预分频改成35,ARR改成999,也就是0.5毫秒中断一次,实际跑下来也没有问题,说明公式掌握之后,任何定时需求都只是改两个参数的事。

3.4 PWM输出实现呼吸灯效果

PWM本质上就是利用定时器输出一个频率固定、占空比可调的方波。占空比就是高电平时间占整个周期的百分比。LED的亮度感知与平均电流成正比,所以占空比越大,LED看起来越亮。呼吸灯效果就是让占空比从0逐渐增大到最大,再逐渐减小到0,循环往复。

PWM初始化时需要关注几个参数:输出通道、极性、脉冲宽度、比较值。脉冲宽度就是比较寄存器CCR的值,它决定了在计数值到达多少时把输出电平翻转。用向上计数模式举例,如果ARR是999,CCR设成500,那么占空比就是50%;CCR设成250,占空比就是25%。

呼吸灯的核心算法不需要任何复杂数学,只需要在定时器中断里改CCR值:

void update_breath_light(void) { if (breath_dir == 1) { pwm_ccr += 2; if (pwm_ccr >= 900) breath_dir = 0; } else { pwm_ccr -= 2; if (pwm_ccr <= 100) breath_dir = 1; } __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pwm_ccr); }

每1毫秒进一次定时器中断,把CCR值加2或者减2,可以看到LED亮度非常平滑地变化。如果想更自然,可以用正弦查表法生成亮度序列,效果会更接近真实呼吸。但我当时只是想验证PWM控制链路,线性递增递减已经完全够用。

3.5 综合整合:按键切换亮度档位

最后把三个功能合到一起。按一次按键,呼吸灯的速度档位就切换一次。我用一个mode变量,每检测到一次有效按键就加1,对3取模。模式0是慢速呼吸,模式1是中速,模式2是快速。

主循环代码很简洁:

int main(void) { HAL_Init(); SystemClock_Config(); LED_Init(); EXTI_Key_Init(); TIM_Base_Init(); PWM_Init(); while (1) { if (key_flag) { key_flag = 0; delay_ms(20); if (HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN) == GPIO_PIN_RESET) { mode = (mode + 1) % 3; if (mode == 0) pwm_speed = 1; else if (mode == 1) pwm_speed = 3; else pwm_speed = 6; } } static int breath_counter = 0; if (++breath_counter >= (1000 / pwm_speed)) { breath_counter = 0; update_breath_light(); } } }

注意这里用了一个技巧:PWM速度档位不是直接改定时器中断频率,而是在1毫秒中断基础上通过判断跳过一次更新。这样做的好处是定时器参数完全不受影响,所有定时相关的计算都保持稳定。整个系统听起来比较绕,但实际就是把“事件检测”和“逻辑处理”分离,这正是中断设计思想的应用。

4. 常见问题与排查技巧实录

4.1 中断进不去或频繁误触发的排查

中断进不去,是我当天遇到的第一个问题。症状是按键按下去,LED没有任何反应。我当时先查了GPIO配置,确认引脚模式正确、内部上拉也已经开启;又查了EXTI线路映射,确认外部中断事件确实连接到了这个引脚;最后检查NVIC配置,发现中断优先级分组没设置。某些固件库在系统初始化时默认优先级分组是全部为抢占优先级,而我配置中断时传的子优先级还是0,导致后面的中断设置代码根本没生效。

中断频繁误触发,这个问题的原因通常是引脚浮空。按键引脚如果不使能内部上拉电阻,外部又没有接上拉,引脚电平就会在悬空状态下受到干扰,产生毛刺,EXTI检测到下降沿或者上升沿就会误触发中断。用了内部上拉之后,这个现象基本消失。

排查步骤我总结成一个固定顺序:先看中断服务函数有没有进入,用LED或者串口打印作为指示;再看标志位是否被正确清除;然后查EXTI采集到的触发边沿是否符合预期;最后才考虑硬件连接。一次排查不要同时改多个东西,不然永远分不清是哪个步骤解决了问题。

4.2 定时器时间不准的几种原因

定时器时间不准,几乎每个人都会遇到。最典型的错误是直接把APB1时钟当成定时器时钟来算。很多MCU的定时器时钟是APBx时钟的两倍频,如果APB1分频到36MHz,它的定时器时钟反而是72MHz。库函数会把这一步处理好,但你用公式自己算的时候特别容易漏。

另一个常见问题是我把预分频和重装载的值搞反了。我记得很清楚,有一段时间总想把“计数1000次”写成预分频值1000-1,把分频写成71。实际上这两个寄存器互相关联,但作用不同:预分频决定计数频率,自动重装载决定计数长度。把公式写在纸上再填参数,是最靠谱的。

调试定时器时,我强烈建议用一个频率尽量高的IO口,在中断里翻转电平,然后用示波器或者逻辑分析仪测实际波形。比如目标1毫秒中断,实际测到的是1.2毫秒,不用猜,直接测量就能定位问题。示波器就是嵌入式学习的照妖镜,很多想不通的问题一测就知道怎么回事。

4.3 按键消抖与临界区保护的常见误区

按键消抖这个事,看起来小,坑却不少。网上很多教程的做法是在中断里直接调用延时函数。中断里做延时,会让整个系统的主循环被拖住,如果同时还有定时器中断在跑,就会出现一个中断阻塞另一个中断的情况。我当时因为按键抖动触发了几次EXTI中断,一怒之下写了一个“中断内消抖”,结果导致LED闪烁明显变卡,后来才明白消抖必须放到主流程里做。

临界区保护是另一个容易忽略的问题。外部中断里更新了一个int变量,主循环读它,看起来没问题,但如果这个变量是16位的,而芯片的读取分两步,中断在两步之间插入改变了值,读出来的就是撕裂数据。解决方法是把这类变量定义成volatile,并且保证读写操作在单条指令内完成。如果涉及更复杂的数据结构,就需要在读取前临时关中断。

当时我把所有关键标志变量都定义成volatile,按键标志、模式标志全部正确处理,整个系统再也没有出现过抖动和撕裂问题。这一点看起来细节,但嵌入式开发中许多难排查的bug都出在这种“看起来没问题”的地方。

4.4 排查代码问题时的日志方法

写嵌入式代码,日志调试有时比仿真器更直观。我最常用的日志方法是在串口初始化完成后,用printf打印关键状态,例如进入中断的次数、当前模式、定时器计数值。中断里打印太快会导致系统卡住,所以一般用计数方式在慢速循环里统一输出。

完整流程跑一遍后,我通常会在串口终端看到类似这样的输出:按键触发次数、当前模式、每次按键后呼吸灯速度参数。这能帮助我从整体流程上确认逻辑无误,而示波器和逻辑分析仪则用来验证信号层面的精确度。两者结合才能对系统有完整把握。

5. Day18之后的延伸方向与个人体会

5.1 下一步学习建议

Day18把中断和定时器打通之后,后面可以选的方向很多。我建议优先学三样:输入捕获、DMA和看门狗。输入捕获可以用定时器测量外部信号的频率和脉宽,配合传感器测距、测速非常实用;DMA可以在不占用CPU的情况下搬运串口或ADC数据,是提高系统吞吐量的关键;看门狗则是一切稳定性系统的最后兜底。

再往后就可以考虑RTOS了。中断+定时器是在裸机环境下模拟并发的基础,RTOS则是把这种并发思维体系化。学会了Day18的内容,再接触RTOS的消息队列、事件标志组,就不会觉得云里雾里,因为它们解决的本质问题和中断标志位是类似的。

但我更想提醒的是不要急着往前赶。嵌入式学习最大的误区是“刷例程”,把每个外设的示例代码跑一遍就认为自己会了。Day18之后最好的训练方式是自己设计一个小项目,把中断、定时器、PWM、串口全部塞进去,比如做一个按键调光的小台灯,或者一个用PWM驱动蜂鸣器的电子琴,再或者用定时器中断做一组交通灯。项目不需要复杂,但一定要自己动手接线、自己写代码、自己调试。

5.2 我在当天实际运行中的一点体会

做完整合实验的时候,我盯着那个呼吸灯看了很久。一个LED从一个亮度到另一个亮度,中间没有任何人在干预,按键按下时它改变速度,定时器精确地维持着节奏。这种东西不是高级人工智能,只是几个寄存器配置加简单的逻辑判断,但它让我第一次真切感受到“程序在芯片里自己跑起来”的感觉。

一个人在学习过程中,真正建立起对技术的热爱,往往不是从第一行Hello World开始的,而是从这种“看似简单的机制组合出复杂效果”的瞬间开始的。Day18的收获不只是中断和定时器的用法,更重要的是学会了怎样把一个复杂的问题拆成几段,每段单独验证再合起来跑通。这种感觉,往后的任何一个复杂项目都用得上。

最后再分享一个小技巧:把每个外设的初始化独立封装成函数,比如LED_Init、KEY_Init、TIM_Base_Init,每个函数都非常短,调试时只需要单独调某一个,不用在几百行代码里上下翻找。等你逐渐做更大的项目,就会越来越觉得这个习惯是最值得保留的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 18:38:29

让代码生成有温度的画:artcraft参数化创作工具实践

做了快两个月的一个项目&#xff0c;终于有底气拿出来说说了。 artcraft 是我最近在业余时间折腾的一个参数化创作工具&#xff0c;核心目标就一句话&#xff1a;让代码生成的画面&#xff0c;努力摆脱“冷冰冰的计算机图”印象&#xff0c;呈现出类似手绘、版画、水彩那种“…

作者头像 李华
网站建设 2026/10/21 18:37:42

C++ std::future 异步编程:核心原理、常见坑位与实战指南

最近在梳理异步编程相关的东西&#xff0c;发现很多写了不少年代码的人&#xff0c;对std::future的理解还停留在“从std::async里拿返回值”这个层面。真到要处理复杂并发任务、做超时控制、跨线程传异常的时候&#xff0c;就开始各种踩坑。这玩意儿看着简单&#xff0c;实际上…

作者头像 李华
网站建设 2026/10/21 18:37:32

从零实现CBOW词向量:中英文双语训练与PyTorch工程实践

1. 项目概述&#xff1a;从零开始跑通CBOW词向量训练&#xff0c;为什么这次作业值得你亲手敲一遍“中英文语料训练CBOW模型获得词向量”——这行标题里藏着NLP入门路上最扎实的一块基石。我带过十几届某高校自然语言处理课程的助教&#xff0c;每年都有学生在作业二卡住&#…

作者头像 李华
网站建设 2026/10/21 18:37:02

Spring Boot JPA 删除操作深度解析:deleteById、批量删除与软删除实战

1. 为什么删除操作值得单独拿出来讲很多刚接触 Spring Boot JPA 组合的开发者&#xff0c;在写增删改查时&#xff0c;最不放在心上的就是删除。新增要考虑主键生成、字段校验&#xff0c;修改要考虑脏检查、乐观锁&#xff0c;查询要考虑索引、懒加载&#xff0c;唯独删除&am…

作者头像 李华
网站建设 2026/10/21 18:36:02

51单片机+DAC0832波形发生器Proteus仿真详解

玩51单片机有一阵子的人&#xff0c;多半都会琢磨做一个小波形发生器。我也不例外——传感器项目玩腻了&#xff0c;就想搞一个能输出标准正弦波、方波、三角波的小玩意儿。直接去买现成信号发生器模块当然省事&#xff0c;但那就没意思了。我选择从51单片机和DAC0832开始&…

作者头像 李华