1. LED闪烁的分层实现

LED闪烁的分层实现 学习卡片

任务说明

本案例以LED闪烁为最小可验证场景,演示单片机软件分层架构的基本形态。目标不是“如何点亮LED”,而是建立清晰的调用边界:驱动层(Driver)负责操作寄存器或GPIO外设,硬件抽象层(HAL)对上层屏蔽具体芯片寄存器细节,应用层(App)只调用HAL接口完成闪烁逻辑。最终代码可在STM32F103系列(或等效Cortex-M3)上直接编译运行,通过观察板载LED以500ms周期闪烁来验证分层调用链是否打通。

分层结构与调用关系

应用层 App app_led_blink() 调用 hal_led_toggle() 硬件抽象层 HAL hal_led_init() / hal_led_toggle() 调用 drv_gpio_write() 驱动层 Driver drv_gpio_init() / drv_gpio_write() 芯片寄存器(GPIOx_BSRR / CRL) 分层规则 • App 只认识 HAL 接口 • HAL 只调用 Driver 接口 • Driver 直接操作寄存器 • 禁止跨层调用 • 更换芯片只改 Driver • 更换板卡只改 HAL 本例验证点: LED 以 500ms 周期闪烁

代码实现

以下代码包含三个文件:drv_gpio.c/h(驱动层)、hal_led.c/h(HAL层)、app_led.c(应用层)以及main.c(入口)。驱动层直接操作STM32F103的GPIO寄存器,HAL层封装LED对应的引脚与电平逻辑,应用层实现延时翻转。为保持最小可复现,延时函数使用简单的SysTick阻塞方式。

/* ========== drv_gpio.h ========== */
#ifndef DRV_GPIO_H
#define DRV_GPIO_H

#include "stm32f10x.h"   /* 芯片寄存器定义 */

typedef enum {
    DRV_GPIO_LOW  = 0,
    DRV_GPIO_HIGH = 1
} drv_gpio_level_t;

void drv_gpio_init(void);
void drv_gpio_write(drv_gpio_level_t level);

#endif

/* ========== drv_gpio.c ========== */
#include "drv_gpio.h"

/* 板载LED:PC13(STM32F103最小系统板) */
#define LED_GPIO_PORT   GPIOC
#define LED_GPIO_PIN    GPIO_Pin_13
#define LED_GPIO_CLK    RCC_APB2Periph_GPIOC

void drv_gpio_init(void)
{
    GPIO_InitTypeDef gpio;
    
    RCC_APB2PeriphClockCmd(LED_GPIO_CLK, ENABLE);
    
    gpio.GPIO_Pin   = LED_GPIO_PIN;
    gpio.GPIO_Mode  = GPIO_Mode_Out_PP;   /* 推挽输出 */
    gpio.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_Init(LED_GPIO_PORT, &gpio);
}

void drv_gpio_write(drv_gpio_level_t level)
{
    if (level == DRV_GPIO_HIGH) {
        LED_GPIO_PORT->BSRR = LED_GPIO_PIN;   /* 置高 */
    } else {
        LED_GPIO_PORT->BRR  = LED_GPIO_PIN;   /* 置低 */
    }
}

/* ========== hal_led.h ========== */
#ifndef HAL_LED_H
#define HAL_LED_H

#include "drv_gpio.h"

void hal_led_init(void);
void hal_led_toggle(void);

#endif

/* ========== hal_led.c ========== */
#include "hal_led.h"

static drv_gpio_level_t led_state = DRV_GPIO_LOW;

void hal_led_init(void)
{
    drv_gpio_init();
    drv_gpio_write(DRV_GPIO_LOW);   /* 初始熄灭 */
    led_state = DRV_GPIO_LOW;
}

void hal_led_toggle(void)
{
    /* 翻转状态并写入驱动层 */
    led_state = (led_state == DRV_GPIO_LOW) ? DRV_GPIO_HIGH : DRV_GPIO_LOW;
    drv_gpio_write(led_state);
}

/* ========== app_led.h ========== */
#ifndef APP_LED_H
#define APP_LED_H

void app_led_blink(void);

#endif

/* ========== app_led.c ========== */
#include "app_led.h"
#include "hal_led.h"

/* 简单阻塞延时:SysTick 1ms 中断由启动文件配置 */
static void delay_ms(uint32_t ms)
{
    volatile uint32_t i;
    for (i = 0; i < ms * 4000; i++) {
        __NOP();
    }
}

void app_led_blink(void)
{
    hal_led_toggle();
    delay_ms(250);   /* 亮250ms + 灭250ms = 周期500ms */
}

/* ========== main.c ========== */
#include "hal_led.h"
#include "app_led.h"

int main(void)
{
    hal_led_init();
    
    while (1) {
        app_led_blink();
    }
}

工程组织与编译

将上述文件加入STM32标准外设库工程(使用STM32F10x_StdPeriph_Lib_V3.5或等效环境),确保包含stm32f10x.h头文件路径。编译后通过ST-Link下载到目标板。若使用其他Cortex-M3芯片,仅需修改drv_gpio.c中的端口、引脚和时钟宏定义,HAL层与应用层无需改动——这正是分层架构的核心收益。

验证方法与判定标准

验证步骤:

  1. 编译工程,确认无错误、无警告(或仅有无害的类型转换提示);
  2. 将程序下载至STM32F103最小系统板,板载LED(PC13)连接正常;
  3. 上电或复位后,观察LED状态。

判定标准:

  • LED以约500ms为周期持续闪烁(亮250ms、灭250ms),说明应用层→HAL层→驱动层的调用链完整打通;
  • 若LED常亮或常灭,使用调试器在hal_led_toggle()处设置断点,单步执行确认函数是否被循环调用、状态变量是否翻转;
  • 若LED完全不亮,检查drv_gpio_init()中的时钟使能和引脚配置是否与硬件实际连接一致。

预期输出(逻辑推导):由于代码中delay_ms(250)在每次翻转后执行,且hal_led_toggle()每次调用必然翻转一次电平状态,因此LED必然呈现周期性亮灭。具体周期时间取决于delay_ms中循环次数与内核时钟频率的对应关系,若系统时钟为8MHz内部RC,实际周期可能偏离500ms,但闪烁行为必然存在。若需精确周期,应使用SysTick定时器替代空循环延时。