🎓 总站 🏠 本课目录 01 概述 02 ARM与STM32 03 开发环境 04 最小系统与C 05 GPIO 06 中断 07 串口通信 08 DMA 09 定时器 10 ADC 11 从模块到项目 12 嵌入式操作系统
智能嵌入式系统设计 · 第11章

从模块到项目

基于 STM32F103 · 讲解模块化设计、项目架构、代码规范、调试技巧和项目实战。

📚 学习进度
0%

🎯学习目标

  • 理解模块化设计的核心思想和基本原则;
  • 掌握嵌入式项目的标准目录架构和文件组织方式;
  • 熟悉嵌入式 C 语言的命名规范、注释规范和代码风格;
  • 掌握断点调试、串口打印、LED 指示等常用调试技巧;
  • 了解版本控制工具 Git 的基本使用和工作流程;
  • 能够独立完成一个小型嵌入式项目的架构设计和代码实现;
  • 掌握头文件保护、extern 声明、static 封装等模块化编程技巧;
  • 理解 .c 和 .h 文件的职责划分;
  • 能够将硬件驱动封装为可复用的模块;
  • 掌握项目开发的完整流程:需求分析→模块划分→编码→调试→测试。
💡 学习建议本章是前面所有章节的综合应用。建议先完成 GPIO、中断、串口、定时器、ADC 等章节的学习,再来学习本章。

1模块化设计思想

模块化是将复杂系统分解为多个功能独立、接口清晰的模块,每个模块完成特定的功能。模块化是软件工程的核心思想之一。

💡 一句话理解模块化就像搭积木,每个积木(模块)功能单一,组合起来就能搭建复杂系统。
💡 通俗理解:为什么需要模块化?

没有模块化:所有代码写在一个 main.c 里,几千行,难以阅读、调试和维护。就像把所有衣服堆在一个大箱子里,找什么都得翻半天。

有模块化:每个外设一个 .c/.h 文件,功能清晰,可复用,易维护。就像用衣柜分层分类存放,找衣服一目了然。

好处:① 代码清晰;② 易于复用;③ 方便调试;④ 多人协作;⑤ 可移植性强。

⭐ 模块划分原则
  • 高内聚:模块内部功能紧密相关,一个模块只做一件事;
  • 低耦合:模块之间依赖尽可能少,修改一个模块不影响其他模块;
  • 接口清晰:通过头文件暴露接口,隐藏实现细节;
  • 单一职责:每个模块只负责一个功能,不要"什么都能干"。
💡 通俗理解:高内聚低耦合

高内聚:就像一个厨师只负责做菜,不负责端盘子、收银。每个人专注自己的事,效率最高。

低耦合:就像厨师和服务员之间只需要"菜单"这个接口,不需要知道对方的具体工作流程。换一个厨师,服务员的工作不受影响。

.c 和 .h 文件的职责

文件类型职责示例内容
.h 头文件对外接口声明函数声明、宏定义、类型定义、extern 变量声明
.c 源文件内部实现细节函数实现、static 内部函数、内部变量
💡 通俗理解:.h 和 .c 的关系

.h 文件就像餐厅的菜单:告诉顾客(其他模块)有哪些菜(函数)可以点(调用),但不告诉你怎么做。

.c 文件就像厨房:具体怎么做菜的实现细节,顾客不需要知道。

其他模块只需要 #include "xxx.h" 就能使用这个模块的功能,不需要知道内部怎么实现。

头文件保护(Header Guard)

头文件必须使用 #ifndef 保护,防止被重复包含:

// led.h - 头文件保护的三种写法
#ifndef __LED_H        // 传统写法(推荐,兼容性最好)
#define __LED_H

// 头文件内容...

#endif

// 或者使用 #pragma once(编译器扩展,部分编译器支持)
#pragma once
// 头文件内容...
💡 为什么需要头文件保护?

假设 a.h 包含了 b.h,而 main.c 同时包含了 a.h 和 b.h,如果没有头文件保护,b.h 的内容会被包含两次,导致重复定义错误

头文件保护就像门票盖章:第一次进来盖章(#define),第二次进来发现已经盖过了(#ifndef 为假),就不再进去了。

extern 和 static 关键字

关键字作用使用场景
extern声明变量/函数在其他文件中定义在 .h 中声明全局变量,供其他模块使用
static限制变量/函数只在当前文件可见在 .c 中定义内部变量和函数,不暴露给外部
// led.h - 外部声明
extern uint8_t led_state;  // 声明变量(不分配内存)
void led_on(void);         // 声明函数

// led.c - 实际定义
#include "led.h"
static uint8_t led_count = 0;  // 内部变量,外部不可见
static void led_check(void);   // 内部函数,外部不可调用

uint8_t led_state = 0;    // 实际定义(分配内存)

void led_on(void)         // 函数实现
{
    led_count++;
    led_check();
    // ...
}

2项目架构

一个规范的嵌入式项目应该有清晰的目录结构,便于管理和维护。

标准项目目录结构

Project/
├── Core/                    # 核心代码
│   ├── Inc/                # 头文件目录
│   │   ├── main.h          # 主程序头文件
│   │   ├── stm32f1xx_hal_conf.h  # HAL 配置
│   │   └── stm32f1xx_it.h  # 中断服务函数声明
│   └── Src/                # 源文件目录
│       ├── main.c          # 主程序
│       ├── stm32f1xx_hal_msp.c  # HAL MSP 初始化
│       ├── stm32f1xx_it.c  # 中断服务函数实现
│       └── system_stm32f1xx.c  # 系统初始化
├── Drivers/                 # 驱动代码
│   ├── BSP/                # 板级支持包(Board Support Package)
│   │   ├── LED/            # LED 驱动模块
│   │   │   ├── led.c
│   │   │   └── led.h
│   │   ├── KEY/            # 按键驱动模块
│   │   ├── OLED/           # OLED 显示模块
│   │   ├── DHT11/          # 温湿度传感器模块
│   │   └── Buzzer/         # 蜂鸣器模块
│   ├── CMSIS/              # ARM Cortex-M 内核头文件
│   └── STM32F1xx_HAL_Driver/  # ST 官方 HAL 库
├── Middlewares/             # 中间件(可选)
│   ├── FreeRTOS/           # 实时操作系统
│   └── FatFs/              # 文件系统
├── MDK-ARM/                 # Keil 工程文件
├── Docs/                    # 项目文档
│   ├── README.md           # 项目说明
│   └── CHANGELOG.md        # 版本更新记录
└── .gitignore               # Git 忽略文件
STM32 项目架构层次图 应用层 main.c BSP 板级驱动 HAL 库 寄存器 📦 应用层:main.c - 用户业务逻辑,调用 BSP 层接口 🔧 BSP 层:led.c/key.c/oled.c - 硬件驱动封装,提供统一接口 ⚙️ HAL 层:STM32 官方库,屏蔽寄存器细节,提供标准化 API
图1 · STM32 项目架构层次:应用层→BSP层→HAL层→寄存器
💡 文件组织建议
  • 每个外设一个文件夹,包含 .c 和 .h 文件;
  • 头文件使用 #ifndef 防止重复包含;
  • 源文件包含对应的头文件;
  • main.c 只包含初始化代码和主循环,不写具体硬件操作;
  • BSP 层封装硬件细节,应用层只调用接口函数。

Keil 工程文件组织

在 Keil MDK 中,通过 Group 来组织文件:

Group 名称包含文件说明
Application/Usermain.c, stm32f1xx_it.c用户应用代码
Drivers/BSPled.c, key.c, oled.c板级驱动
Drivers/HALstm32f1xx_hal_gpio.c 等HAL 库源文件
Drivers/CMSISsystem_stm32f1xx.cARM 内核支持

3代码规范

良好的代码规范是团队协作的基础,也是代码可维护性的保障。嵌入式开发中常用的规范如下:

命名规范

类型规范示例
文件名小写字母+下划线led.c, dht11.h
函数名小写字母+下划线,模块名_动词led_init(), key_scan()
变量名小写字母+下划线led_state, temp_value
宏定义全大写+下划线#define LED_PIN GPIO_PIN_0
类型定义全大写+下划线,_TypeDef 后缀GPIO_InitTypeDef
枚举类型全大写+下划线LED_RED, LED_GREEN
全局变量g_ 前缀g_led_count
静态变量s_ 前缀s_init_flag

注释规范

位置注释内容示例
文件头文件说明、作者、日期、版本/** @file led.c * @brief LED 驱动 */
函数头功能说明、参数、返回值/** @brief 初始化 LED */
关键代码解释为什么这样做// 必须先开时钟
全局变量变量用途说明/** 当前 LED 状态 */

代码风格

规范说明
缩进使用 4 个空格缩进,不要使用 Tab
大括号每个语句块必须使用大括号,即使只有一行
行宽每行不超过 80~120 个字符
空行函数之间空一行,逻辑段落之间空一行
空格运算符两边加空格,逗号后面加空格
完整的模块代码示例(LED 模块)
/**
 * @file    led.c
 * @brief   LED 驱动模块
 * @author  Embedded Dev
 * @date    2024-01-15
 * @version 1.0
 */

#include "led.h"

/* ==================== 私有宏定义 ==================== */
#define LED1_PORT   GPIOB
#define LED1_PIN    GPIO_PIN_0
#define LED2_PORT   GPIOB
#define LED2_PIN    GPIO_PIN_1

/* ==================== 私有变量 ==================== */
static uint8_t s_led_init_flag = 0;  // 初始化标志

/* ==================== 私有函数 ==================== */
/**
 * @brief  配置 LED GPIO
 * @param  port: GPIO 端口
 * @param  pin: GPIO 引脚
 * @retval None
 */
static void led_gpio_config(GPIO_TypeDef* port, uint16_t pin)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin   = pin;
    GPIO_InitStruct.Mode  = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull  = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(port, &GPIO_InitStruct);
}

/* ==================== 公有函数 ==================== */
/**
 * @brief  初始化所有 LED
 * @retval 0: 成功, -1: 已初始化
 */
int led_init(void)
{
    if (s_led_init_flag) {
        return -1;  // 防止重复初始化
    }
    
    /* 使能时钟 */
    __HAL_RCC_GPIOB_CLK_ENABLE();
    
    /* 配置 GPIO */
    led_gpio_config(LED1_PORT, LED1_PIN);
    led_gpio_config(LED2_PORT, LED2_PIN);
    
    /* 默认关闭所有 LED */
    led_off(LED_ALL);
    
    s_led_init_flag = 1;
    return 0;
}

/**
 * @brief  点亮指定 LED
 * @param  led: LED 编号 (LED1/LED2/LED_ALL)
 * @retval None
 * @note   低电平点亮(共阳极接法)
 */
void led_on(LedIdTypeDef led)
{
    if (led == LED1 || led == LED_ALL) {
        HAL_GPIO_WritePin(LED1_PORT, LED1_PIN, GPIO_PIN_RESET);
    }
    if (led == LED2 || led == LED_ALL) {
        HAL_GPIO_WritePin(LED2_PORT, LED2_PIN, GPIO_PIN_RESET);
    }
}

4调试技巧

嵌入式调试是开发过程中最耗时的环节,掌握正确的调试方法能大幅提高效率。

常用调试方法

🐛

断点调试

Breakpoint
在代码中设置断点,程序运行到断点暂停,可以查看变量值、寄存器状态、调用栈。适合定位逻辑错误。
📊

串口打印

Printf Debug
通过串口输出调试信息,是最常用的调试方法。适合观察程序运行流程和变量变化。
💡

LED 指示

LED Debug
用 LED 闪烁指示程序运行状态,简单直观,不需要额外工具。适合最基本的"程序是否在运行"判断。
🔍

逻辑分析仪

Logic Analyzer
抓取 GPIO/通信波形,分析时序问题。适合调试 SPI、IIC、UART 等通信协议。

串口调试输出(重定向 printf)

// 方法1:重定向 printf 到串口(需要 #include <stdio.h>)
int fputc(int ch, FILE *f)
{
    HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, 100);
    return ch;
}

// 使用方法
printf("Temperature: %d C\r\n", temp);
printf("LED State: 0x%02X\r\n", led_state);

// 方法2:条件编译控制调试输出
#define DEBUG_ENABLE  1

#if DEBUG_ENABLE
    #define DEBUG_LOG(fmt, ...) printf(fmt, ##__VA_ARGS__)
#else
    #define DEBUG_LOG(fmt, ...)
#endif

// 使用
DEBUG_LOG("Sensor value: %d\r\n", adc_value);

常见错误排查流程

程序不工作 检查电源和时钟 检查接线和焊接 检查代码逻辑 问题解决 ✓ 用万用表测量电压 用示波器看波形 检查虚焊/短路 断点+单步+Watch
图2 · 嵌入式常见错误排查流程:从硬件到软件逐步排查
💡 调试黄金法则
  • 先硬件后软件:先确保硬件正常(电源、时钟、复位);
  • 先简单后复杂:从简单功能开始(如点灯),逐步添加;
  • 善用断点:在关键位置设断点,查看变量和寄存器;
  • 看寄存器:查看寄存器状态,确认配置是否正确;
  • 查手册:遇到问题先查数据手册和参考手册;
  • 缩小范围:二分法注释代码,快速定位问题所在。

使用 Watch 窗口调试

在 Keil 中,可以通过 Watch 窗口实时观察变量值:

调试操作说明
F9在当前行设置/取消断点
F5运行到下一个断点
F10单步执行(不进入函数)
F11单步执行(进入函数)
Watch 窗口添加变量名,实时查看值变化
Memory 窗口查看内存地址内容
Peripherals查看外设寄存器状态

5版本控制

Git 是分布式版本控制系统,用于管理代码版本和协作开发。嵌入式项目也推荐使用 Git 管理。

为什么需要版本控制?

💡 通俗理解

没有 Git:代码改来改去,最后不知道改了什么,想回退到之前的版本只能靠"备份文件夹":project_v1, project_v2, project_final, project_final_final...

有 Git:每次修改都有记录,可以随时查看历史、回退版本、创建分支尝试新功能。就像游戏存档,随时可以读档重来。

Git 基本命令

命令作用使用场景
git init初始化仓库项目开始时执行一次
git add .添加所有文件到暂存区准备提交前执行
git commit -m "说明"提交到本地仓库完成一个功能后提交
git status查看文件状态随时查看哪些文件修改了
git log --oneline查看提交历史查看历史版本
git diff查看修改内容提交前查看改了什么
git branch xxx创建分支开发新功能时创建
git checkout xxx切换分支切换到其他分支工作
git merge xxx合并分支功能完成后合并到主分支
git push推送到远程仓库备份到 GitHub/Gitee

Git 工作流程

工作区 Working add 暂存区 Staging commit 本地仓库 Repository push 远程仓库 GitHub
图3 · Git 工作流程:工作区→暂存区→本地仓库→远程仓库

.gitignore 文件

嵌入式项目中,以下文件不应提交到 Git:

# Keil 编译生成的文件
*.o
*.d
*.axf
*.hex
*.bin
*.lst
*.map
Listings/
Objects/

# IDE 配置文件
*.uvoptx
*.uvguix.*

# 系统文件
.DS_Store
Thumbs.db
⭐ Git 使用建议
  • 每次提交写清楚说明("修复了什么"、"添加了什么");
  • 一个功能一个分支,完成后合并;
  • 定期推送到远程仓库备份;
  • 不要提交编译生成的文件(如 .o、.hex);
  • 提交前先 git diff 检查修改内容。

6项目实战:智能温湿度监测系统

综合运用所学知识,设计一个智能温湿度监测系统。下面按项目开发流程逐步讲解。

第一步:需求分析

项目需求
  • 使用 DHT11 传感器采集温湿度数据;
  • 在 OLED 屏幕上实时显示温度和湿度;
  • 温度超过阈值时蜂鸣器报警;
  • 通过按键设置报警阈值;
  • 通过串口上传数据到电脑。

第二步:模块划分

模块划分表
模块文件功能接口函数
dht11.c/hDHT11 温湿度传感器驱动dht11_init(), dht11_read()
oled.c/hOLED 显示屏驱动oled_init(), oled_show_string()
key.c/h按键检测key_init(), key_scan()
buzzer.c/h蜂鸣器驱动buzzer_init(), buzzer_on/off()
usart.c/h串口通信usart_init(), usart_printf()
main.c主程序,协调各模块main()

第三步:代码实现

main.c 主程序框架
/**
 * @file    main.c
 * @brief   智能温湿度监测系统 - 主程序
 */

#include "main.h"
#include "dht11.h"
#include "oled.h"
#include "key.h"
#include "buzzer.h"
#include "usart.h"

/* 全局变量 */
static float g_temperature = 0.0f;
static float g_humidity    = 0.0f;
static float g_temp_threshold = 30.0f;  // 报警阈值

int main(void)
{
    /* 1. HAL 初始化 */
    HAL_Init();
    SystemClock_Config();
    
    /* 2. 外设初始化 */
    usart_init(115200);
    dht11_init();
    oled_init();
    key_init();
    buzzer_init();
    
    /* 3. 显示开机界面 */
    oled_show_string(0, 0, "Temp Monitor v1.0");
    HAL_Delay(1000);
    oled_clear();
    
    /* 4. 主循环 */
    while (1)
    {
        /* 读取传感器 */
        if (dht11_read(&g_temperature, &g_humidity) == 0)
        {
            /* 显示数据 */
            oled_show_string(0, 0, "Temp:");
            oled_show_float(40, 0, g_temperature, 1);
            oled_show_string(0, 2, "Humi:");
            oled_show_float(40, 2, g_humidity, 1);
            
            /* 串口上传 */
            usart_printf("T:%.1f H:%.1f\r\n", g_temperature, g_humidity);
            
            /* 报警判断 */
            if (g_temperature > g_temp_threshold) {
                buzzer_on();
            } else {
                buzzer_off();
            }
        }
        
        /* 按键检测 */
        key_scan();
        
        HAL_Delay(1000);
    }
}

第四步:调试测试

💡 测试清单
  • ✅ DHT11 能正确读取温湿度数据
  • ✅ OLED 能正确显示温度和湿度
  • ✅ 按键能修改报警阈值
  • ✅ 温度超限时蜂鸣器报警
  • ✅ 串口能正确上传数据
  • ✅ 长时间运行稳定不崩溃

重点例题

例题1:LED 模块封装(标准库 + HAL 库) 要求:① 支持多个 LED;② 提供初始化、开、关、翻转函数;③ 使用枚举定义 LED;④ 支持标准库和 HAL 库两种写法。
/* ========== 标准库写法 ========== */
// led.h
#ifndef __LED_H
#define __LED_H
#include "stm32f10x.h"

typedef enum {
    LED1 = 0,
    LED2,
    LED3,
    LED_ALL,
    LED_NUM
} LedIdTypeDef;

void led_init(void);
void led_on(LedIdTypeDef led);
void led_off(LedIdTypeDef led);
void led_toggle(LedIdTypeDef led);

#endif

// led.c
#include "led.h"

typedef struct {
    GPIO_TypeDef* port;
    uint16_t pin;
} LedPinTypeDef;

static const LedPinTypeDef led_pin[LED_NUM - 1] = {
    {GPIOB, GPIO_Pin_0},  // LED1
    {GPIOB, GPIO_Pin_1},  // LED2
    {GPIOB, GPIO_Pin_2}   // LED3
};

void led_init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    GPIO_InitTypeDef g;
    for (int i = 0; i < LED_NUM - 1; i++) {
        g.GPIO_Pin   = led_pin[i].pin;
        g.GPIO_Mode  = GPIO_Mode_Out_PP;
        g.GPIO_Speed = GPIO_Speed_2MHz;
        GPIO_Init(led_pin[i].port, &g);
        led_off((LedIdTypeDef)i);
    }
}

void led_on(LedIdTypeDef led)
{
    if (led == LED_ALL) {
        for (int i = 0; i < LED_NUM - 1; i++)
            GPIO_ResetBits(led_pin[i].port, led_pin[i].pin);
    } else {
        GPIO_ResetBits(led_pin[led].port, led_pin[led].pin);
    }
}

void led_toggle(LedIdTypeDef led)
{
    uint16_t odr = led_pin[led].port->ODR;
    led_pin[led].port->ODR = odr ^ led_pin[led].pin;
}
/* ========== HAL 库写法 ========== */
// led.h
#ifndef __LED_H
#define __LED_H
#include "stm32f1xx_hal.h"

typedef enum {
    LED1 = 0,
    LED2,
    LED3,
    LED_ALL,
    LED_NUM
} LedIdTypeDef;

void led_init(void);
void led_on(LedIdTypeDef led);
void led_off(LedIdTypeDef led);
void led_toggle(LedIdTypeDef led);

#endif

// led.c
#include "led.h"

typedef struct {
    GPIO_TypeDef* port;
    uint16_t pin;
} LedPinTypeDef;

static const LedPinTypeDef led_pin[LED_NUM - 1] = {
    {GPIOB, GPIO_PIN_0},
    {GPIOB, GPIO_PIN_1},
    {GPIOB, GPIO_PIN_2}
};

void led_init(void)
{
    __HAL_RCC_GPIOB_CLK_ENABLE();
    GPIO_InitTypeDef g = {0};
    for (int i = 0; i < LED_NUM - 1; i++) {
        g.Pin   = led_pin[i].pin;
        g.Mode  = GPIO_MODE_OUTPUT_PP;
        g.Pull  = GPIO_NOPULL;
        g.Speed = GPIO_SPEED_FREQ_LOW;
        HAL_GPIO_Init(led_pin[i].port, &g);
        led_off((LedIdTypeDef)i);
    }
}

void led_on(LedIdTypeDef led)
{
    if (led == LED_ALL) {
        for (int i = 0; i < LED_NUM - 1; i++)
            HAL_GPIO_WritePin(led_pin[i].port, led_pin[i].pin, GPIO_PIN_RESET);
    } else {
        HAL_GPIO_WritePin(led_pin[led].port, led_pin[led].pin, GPIO_PIN_RESET);
    }
}

void led_toggle(LedIdTypeDef led)
{
    HAL_GPIO_TogglePin(led_pin[led].port, led_pin[led].pin);
}
关键点:① 使用枚举定义 LED 编号;② 使用结构体数组存储引脚信息;③ 头文件使用 #ifndef 防止重复包含;④ static 修饰内部数组,不暴露给外部。
例题2:按键模块封装(消抖 + 事件检测) 要求:① 支持多个按键;② 软件消抖;③ 支持按下、松开、长按事件。
// key.h
#ifndef __KEY_H
#define __KEY_H
#include "stm32f1xx_hal.h"

typedef enum {
    KEY1 = 0,
    KEY2,
    KEY_NUM
} KeyIdTypeDef;

typedef enum {
    KEY_EVENT_NONE = 0,
    KEY_EVENT_PRESS,      // 按下
    KEY_EVENT_RELEASE,    // 松开
    KEY_EVENT_LONG_PRESS  // 长按
} KeyEventTypeDef;

void key_init(void);
KeyEventTypeDef key_scan(void);

#endif

// key.c
#include "key.h"

#define KEY_DEBOUNCE_MS   20    // 消抖时间
#define KEY_LONG_PRESS_MS 1000  // 长按时间

typedef struct {
    GPIO_TypeDef* port;
    uint16_t pin;
} KeyPinTypeDef;

static const KeyPinTypeDef key_pin[KEY_NUM] = {
    {GPIOA, GPIO_PIN_0},
    {GPIOB, GPIO_PIN_12}
};

static uint32_t s_key_press_time[KEY_NUM] = {0};
static uint8_t  s_key_last_state[KEY_NUM] = {1, 1};  // 默认高电平(未按下)

void key_init(void)
{
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_GPIOB_CLK_ENABLE();
    GPIO_InitTypeDef g = {0};
    g.Mode  = GPIO_MODE_INPUT;
    g.Pull  = GPIO_PULLUP;
    g.Speed = GPIO_SPEED_FREQ_LOW;
    for (int i = 0; i < KEY_NUM; i++) {
        g.Pin = key_pin[i].pin;
        HAL_GPIO_Init(key_pin[i].port, &g);
    }
}

KeyEventTypeDef key_scan(void)
{
    for (int i = 0; i < KEY_NUM; i++) {
        uint8_t state = HAL_GPIO_ReadPin(key_pin[i].port, key_pin[i].pin);
        
        // 检测下降沿(按下)
        if (s_key_last_state[i] == 1 && state == 0) {
            s_key_press_time[i] = HAL_GetTick();
            s_key_last_state[i] = 0;
            return KEY_EVENT_PRESS;
        }
        
        // 检测上升沿(松开)
        if (s_key_last_state[i] == 0 && state == 1) {
            s_key_last_state[i] = 1;
            uint32_t duration = HAL_GetTick() - s_key_press_time[i];
            if (duration >= KEY_LONG_PRESS_MS) {
                return KEY_EVENT_LONG_PRESS;
            }
            return KEY_EVENT_RELEASE;
        }
    }
    return KEY_EVENT_NONE;
}
例题3:项目架构设计 题目:设计一个"智能小车"项目的目录结构和模块划分。 要求:支持电机控制、超声波避障、红外循迹、蓝牙遥控。
SmartCar/
├── Core/
│   ├── Inc/
│   │   ├── main.h
│   │   └── stm32f1xx_it.h
│   └── Src/
│       ├── main.c
│       └── stm32f1xx_it.c
├── Drivers/
│   ├── BSP/
│   │   ├── Motor/          # 电机驱动模块
│   │   │   ├── motor.c
│   │   │   └── motor.h
│   │   ├── Ultrasonic/     # 超声波模块
│   │   │   ├── ultrasonic.c
│   │   │   └── ultrasonic.h
│   │   ├── Infrared/       # 红外循迹模块
│   │   │   ├── infrared.c
│   │   │   └── infrared.h
│   │   ├── Bluetooth/      # 蓝牙通信模块
│   │   │   ├── bluetooth.c
│   │   │   └── bluetooth.h
│   │   └── Servo/          # 舵机模块
│   │       ├── servo.c
│   │       └── servo.h
│   ├── CMSIS/
│   └── STM32F1xx_HAL_Driver/
├── MDK-ARM/
└── README.md
模块接口设计:
// motor.h - 电机控制接口
void motor_init(void);
void motor_set_speed(int left, int right);  // -100~100
void motor_stop(void);
void motor_forward(int speed);
void motor_backward(int speed);
void motor_turn_left(int speed);
void motor_turn_right(int speed);

// ultrasonic.h - 超声波接口
void ultrasonic_init(void);
float ultrasonic_get_distance(void);  // 返回 cm

// infrared.h - 红外循迹接口
void infrared_init(void);
uint8_t infrared_get_status(void);  // 返回传感器状态

🎯自测(点击展开)

模块化设计的好处是什么?
代码清晰、易于复用、方便调试、多人协作、可移植性强。
头文件中为什么要使用 #ifndef?
防止头文件被重复包含,避免重复定义错误。就像门票盖章,第一次进来盖章,第二次发现已经盖过就不再进去了。
.h 文件和 .c 文件的职责分别是什么?
.h 文件是"菜单",对外暴露接口(函数声明、宏定义);.c 文件是"厨房",包含具体实现细节。
extern 和 static 关键字的作用是什么?
extern 用于声明在其他文件中定义的变量/函数;static 用于限制变量/函数只在当前文件可见,实现内部封装。
Git 的作用是什么?
版本控制,管理代码版本,支持多人协作开发。可以查看历史、回退版本、创建分支。
嵌入式项目中常用的调试方法有哪些?
断点调试、串口打印、LED 指示、逻辑分析仪等。
调试时应该先检查什么?
先硬件后软件:先检查电源、时钟、复位、接线是否正常,再检查代码逻辑。
如何重定向 printf 到串口?
重写 fputc 函数,在其中调用 HAL_UART_Transmit 发送数据到串口。
main.c 应该包含哪些内容?
main.c 应该只包含初始化代码和主循环,不写具体硬件操作。具体硬件操作应该封装到 BSP 层的各个模块中。
项目开发的完整流程是什么?
需求分析→模块划分→编码实现→调试测试→优化完善。
什么是高内聚低耦合?
高内聚:模块内部功能紧密相关;低耦合:模块之间依赖尽可能少。就像厨师只负责做菜(高内聚),和服务员通过菜单接口协作(低耦合)。
Git 中 .gitignore 文件的作用是什么?
告诉 Git 哪些文件不需要提交到版本库,如编译生成的 .o、.hex 文件,IDE 配置文件等。

📝强化题库

选择题点选即时判分;填空题输入后"检查"或"显示答案"。

已答 0/0答对 0正确率
已答 0/0答对 0