一,传感器控制LED

在开始编写代码之前,我们首先需要完成硬件电路的搭建。传感器控制 LED 的核心思路是:通过传感器感知外部环境变化,将物理信号转换为电信号,再由 STM32 微控制器读取该信号,并根据预设逻辑控制 LED 的亮灭状态。

具体来说,我们需要将传感器的信号输出引脚连接到 STM32 的某个 GPIO 输入端口,同时将 LED 的正极通过限流电阻连接到另一个 GPIO 输出端口。这样,当传感器检测到目标事件时,STM32 就能通过读取输入引脚的电平状态,来决定是否翻转输出引脚,从而控制 LED 的亮灭。

上图展示了完整的电路连接方式。在接线时需要注意:传感器的供电电压必须与 STM32 的 GPIO 电平兼容,通常为 3.3V;LED 的限流电阻一般取 330Ω 到 1kΩ 之间,以防止电流过大烧毁 LED。

代码

电路搭建完成后,接下来就是编写控制代码。在 STM32 的 HAL 库开发框架下,我们通常使用 CubeMX 进行图形化配置,自动生成初始化代码,然后在用户代码区编写具体的业务逻辑。

读取引脚状态,输出引脚状态

读取引脚状态的核心函数是HAL_GPIO_ReadPin(),它用于读取指定 GPIO 引脚的电平状态,返回GPIO_PIN_SET(高电平)或GPIO_PIN_RESET(低电平)。而输出引脚状态则使用HAL_GPIO_WritePin()HAL_GPIO_TogglePin()函数。

下面是一个典型的读取与输出示例:

// 读取传感器引脚状态 GPIO_PinState sensorState = HAL_GPIO_ReadPin(SENSOR_GPIO_Port, SENSOR_Pin); // 根据传感器状态控制 LED if (sensorState == GPIO_PIN_SET) { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); // 点亮 LED } else { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); // 熄灭 LED }

这种轮询方式实现简单,但 CPU 需要不断循环检测引脚状态,效率较低。如果希望提高响应速度并降低 CPU 占用,可以考虑使用中断方式,也就是接下来要介绍的内容。

中断与 SysTick

中断是嵌入式系统中非常重要的机制。所谓中断,就是当某个事件发生时,CPU 暂停当前正在执行的任务,转而去处理这个突发事件,处理完成后再回到原来的任务继续执行。这样可以大大提高系统的实时响应能力。

在 STM32 中,中断源非常丰富,包括外部中断(EXTI)、定时器中断、串口中断等。而 SysTick 定时器则是 Cortex-M 内核自带的系统节拍定时器,HAL 库的延时函数HAL_Delay()正是依赖它来实现的。

上图展示了中断系统的整体架构。可以看到,各种外设事件通过中断控制器(NVIC)统一管理,NVIC 负责中断的使能、优先级配置和响应调度。

这张图进一步说明了中断触发到响应处理的完整流程。当外设产生中断请求后,NVIC 会根据优先级决定是否立即响应,如果当前没有更高优先级的中断正在执行,CPU 就会跳转到对应的中断服务函数(ISR)中执行处理代码。

NVIC

NVIC(Nested Vectored Interrupt Controller,嵌套向量中断控制器)是 Cortex-M 内核中负责中断管理的核心模块。它支持中断的嵌套处理,即高优先级的中断可以打断低优先级的中断服务函数。

在 HAL 库中,我们通常使用HAL_NVIC_SetPriority()函数来设置中断优先级,使用HAL_NVIC_EnableIRQ()函数来使能中断。中断优先级分为抢占优先级和子优先级两部分,抢占优先级决定中断是否可以打断其他中断,而子优先级则用于同抢占优先级下的响应顺序。

上图是 NVIC 的配置界面。在 CubeMX 中,我们可以直观地为每个中断源分配抢占优先级和子优先级。合理配置中断优先级是保证系统稳定运行的关键,尤其是当多个中断同时发生时,优先级决定了它们的处理顺序。

定时器配置原理

定时器是 STM32 中非常强大的外设,可以实现精确的时间控制、波形生成、输入捕获等功能。在 HAL 库中,SysTick 定时器被用作系统心跳,为延时函数和超时检测提供时间基准。

上图展示了 SysTick 定时器的基本配置。SysTick 是一个 24 位的向下递减计数器,当计数值减到 0 时会产生中断,然后自动重新装载初值,继续下一轮计数。

🧠 这行代码到底在算什么?

在 HAL 库的初始化代码中,有一行非常关键的配置:

HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq))

它的目标很简单:让 SysTick 定时器每 1 毫秒产生一次中断(也就是 HAL 库的uwTick变量每 1ms 加 1,你调HAL_Delay()靠的就是它)。

这行代码的本质是计算 SysTick 定时器的重装载值。SysTick 的时钟源通常是系统主时钟(这里为 72MHz),每来一个时钟脉冲,计数器就减 1。要让计数器从初值减到 0 恰好耗时 1ms,就需要计算出 1ms 内有多少个时钟脉冲,这个数量就是我们要写入的重装载值。


🔢 我们拿实际数字来算一遍

假设你的 STM32F103C8T6 主频是72 MHz(即SystemCoreClock = 72,000,000)。

在 HAL 库里,uwTickFreq默认被定义为1U(表示 1ms)。
那么:

  1. 先算括号里:(1000U / uwTickFreq)=1000 / 1=1000

  2. 再算除法:SystemCoreClock / 1000=72,000,000 / 1000=72,000

所以这行代码实际执行的是:

HAL_SYSTICK_Config(72000);

也就是说,SysTick 定时器的重装载值被设置为 72000。这个值决定了定时器从 72000 开始向下计数,减到 0 时触发一次中断。


⏱️ 72,000 这个数字是什么意思?

SysTick 是一个24 位向下递减的计数器
你把72,000这个值写进去,它就会从 72,000 开始,每来一个时钟脉冲(72MHz),就减 1

  • 减到 0 需要72,000 个脉冲

  • 72,000 个脉冲 ÷ 72,000,000 个脉冲/秒 =0.001 秒 = 1 毫秒

减到 0 的那一刻,它触发中断,然后硬件自动把 72,000 重新装进去,接着再数一遍。
这就实现了每 1ms 准时打断 CPU 一次的效果。

简单来说,72,000 这个数字就是"1 毫秒内有多少个时钟脉冲"的答案。因为系统时钟是 72MHz,也就是每秒有 72,000,000 个脉冲,那么 1 毫秒(千分之一秒)内自然就有 72,000 个脉冲。SysTick 计数器从 72,000 开始倒数,每收到一个脉冲减 1,减到 0 时恰好经过 1 毫秒。


❓ 为什么中间要除一个(1000U / uwTickFreq)

这是 HAL 库为了"兼容性"留的一个口子。

  • 如果你想让心跳变成10ms一次,就把uwTickFreq改成10U

  • 那么1000 / 10 = 100,最终算出来就是72,000,000 / 100 = 720,000(即 10ms 才减到 0)

但因为绝大部分时候我们都是要1ms,所以uwTickFreq就固定等于1U,这一坨括号算出来永远等于1000

这个设计的好处在于:当开发者需要调整系统心跳频率时,只需要修改uwTickFreq这一个宏定义,而不需要去修改底层的 SysTick 配置代码。HAL 库通过这种参数化的方式,提高了代码的可移植性和可维护性。

上图展示了 SysTick 中断的触发流程。当计数器减到 0 时,硬件自动将重装载值重新装入计数器,同时向 NVIC 发出中断请求,CPU 响应后进入 SysTick 中断服务函数。

在中断函数中写执行程序

了解了 SysTick 的工作原理后,我们就可以在中断函数中编写需要周期性执行的任务了。SysTick 的中断服务函数是SysTick_Handler(),在 HAL 库中,它默认会调用HAL_IncTick()来更新系统时钟计数。

每执行一次中断函数时经过一毫秒,每次给 static 变量 count 自加 1,当 count 等于 500 时使用HAL_GPIO_TogglePin翻转电平。这样,LED 就会以 500ms 为周期进行翻转,实现闪烁效果。

具体代码如下:

void SysTick_Handler(void) { HAL_IncTick(); static uint16_t count = 0; count++; if (count >= 500) { count = 0; HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } }

这段代码的逻辑非常清晰:每次 SysTick 中断(每 1ms)进入一次,count 自加 1;当 count 累加到 500 时,说明已经过去了 500ms,此时翻转 LED 的电平状态,并将 count 清零重新计数。这样 LED 就会以 1Hz 的频率闪烁(亮 500ms,灭 500ms)。

最终

上图是最终的运行效果。可以看到,LED 按照预期的频率进行闪烁,说明中断配置和代码逻辑都正确无误。

外部中断

除了定时器中断,外部中断(EXTI)也是嵌入式开发中非常常用的中断类型。外部中断允许 GPIO 引脚在检测到电平变化(上升沿、下降沿或双边沿)时触发中断,非常适合用于按键检测、传感器信号触发等场景。

上图展示了外部中断的配置界面。在 CubeMX 中,我们可以为每个 GPIO 引脚单独配置外部中断模式,并选择触发方式(上升沿、下降沿或双边沿触发)。

与轮询方式相比,外部中断的最大优势在于实时性和效率。CPU 不需要持续检测引脚状态,只有当引脚电平发生变化时才会被中断唤醒,从而大大降低了 CPU 的占用率。

中断优先级

当系统中存在多个中断源时,中断优先级的配置就显得尤为重要。STM32 的中断优先级分为抢占优先级(Preemption Priority)和子优先级(Sub Priority)两部分。

抢占优先级决定了中断是否可以打断正在执行的其他中断。如果两个中断的抢占优先级相同,则它们之间不能互相打断,只能按照子优先级的高低依次响应。如果抢占优先级和子优先级都相同,则按照中断号的大小决定响应顺序。

上图展示了中断优先级的配置方法。在 CubeMX 中,我们可以为每个中断源分别设置抢占优先级和子优先级。数值越小,优先级越高。合理配置优先级可以确保关键任务(如紧急故障处理)能够及时响应,而不被次要中断阻塞。

外部中断/事件控制器原理图

外部中断/事件控制器(EXTI)的原理图如下所示。它负责将 GPIO 引脚的电平变化转换为中断请求信号,并传递给 NVIC 进行处理。

从原理图中可以看到,EXTI 支持两种输出模式:中断模式和事件模式。中断模式会触发 CPU 执行中断服务函数,而事件模式则可以直接触发其他外设的动作(如 DMA 传输),不需要 CPU 参与。在实际开发中,我们最常用的是中断模式。

回调函数

在 HAL 库的中断处理机制中,回调函数(Callback Function)扮演着非常重要的角色。HAL 库将中断处理分为两个层次:底层的中断服务函数(ISR)和上层的回调函数。

底层 ISR 负责处理硬件相关的操作,如清除中断标志位等;而回调函数则是 HAL 库留给用户的"钩子",用户可以在回调函数中编写自己的业务逻辑,而不需要修改 HAL 库的底层代码。

上图展示了 HAL 库中断处理的整体流程。当外部中断触发时,CPU 跳转到对应的 ISR,ISR 完成硬件处理后调用回调函数,用户编写的业务逻辑就在回调函数中执行。

MX 配置 GPIO 端口,初始化端口记录上升沿或下降沿

在使用外部中断之前,我们需要先在 CubeMX 中完成 GPIO 的初始化配置。配置内容包括:引脚模式(输入/输出)、上下拉电阻、触发方式(上升沿/下降沿/双边沿)等。

上图展示了 GPIO 外部中断的配置界面。在配置时,我们需要将引脚模式设置为"External Interrupt Mode with Falling edge trigger detection"(下降沿触发)或"Rising edge"(上升沿触发),具体取决于传感器的输出特性。

找到端点专属中断函数

配置完成后,CubeMX 会自动生成中断服务函数的框架。对于外部中断,每个引脚组(如 PA0-PA15)都有对应的中断服务函数,例如EXTI0_IRQHandler()对应 PA0 引脚的中断。

上图展示了中断服务函数的位置。在生成的代码中,HAL_GPIO_EXTI_IRQHandler()函数负责处理中断标志位的清除,并调用对应的回调函数。

找到回调函数(弱函数)

HAL 库为每个外设都提供了默认的回调函数,这些函数被定义为"弱函数"(Weak Function),意味着用户可以重新定义同名的函数来覆盖默认实现,而不需要修改 HAL 库的源代码。

上图展示了回调函数的默认定义。对于外部中断,对应的回调函数是HAL_GPIO_EXTI_Callback(),它接收一个参数GPIO_Pin,用于指示具体是哪个引脚触发了中断。

自己写入

最后,我们需要在用户代码区重新实现这个回调函数,编写自己的业务逻辑。例如:

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == SENSOR_Pin) { // 传感器触发,翻转 LED 状态 HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } }

这样,当传感器引脚检测到设定的边沿触发时,就会自动进入回调函数,执行 LED 翻转操作。整个过程不需要 CPU 轮询,实现了高效的事件驱动处理。