从模块到项目
基于 STM32F103 · 讲解模块化设计、项目架构、代码规范、调试技巧和项目实战。
🎯学习目标
- 理解模块化设计的核心思想和基本原则;
- 掌握嵌入式项目的标准目录架构和文件组织方式;
- 熟悉嵌入式 C 语言的命名规范、注释规范和代码风格;
- 掌握断点调试、串口打印、LED 指示等常用调试技巧;
- 了解版本控制工具 Git 的基本使用和工作流程;
- 能够独立完成一个小型嵌入式项目的架构设计和代码实现;
- 掌握头文件保护、extern 声明、static 封装等模块化编程技巧;
- 理解 .c 和 .h 文件的职责划分;
- 能够将硬件驱动封装为可复用的模块;
- 掌握项目开发的完整流程:需求分析→模块划分→编码→调试→测试。
1模块化设计思想
模块化是将复杂系统分解为多个功能独立、接口清晰的模块,每个模块完成特定的功能。模块化是软件工程的核心思想之一。
没有模块化:所有代码写在一个 main.c 里,几千行,难以阅读、调试和维护。就像把所有衣服堆在一个大箱子里,找什么都得翻半天。
有模块化:每个外设一个 .c/.h 文件,功能清晰,可复用,易维护。就像用衣柜分层分类存放,找衣服一目了然。
好处:① 代码清晰;② 易于复用;③ 方便调试;④ 多人协作;⑤ 可移植性强。
- 高内聚:模块内部功能紧密相关,一个模块只做一件事;
- 低耦合:模块之间依赖尽可能少,修改一个模块不影响其他模块;
- 接口清晰:通过头文件暴露接口,隐藏实现细节;
- 单一职责:每个模块只负责一个功能,不要"什么都能干"。
高内聚:就像一个厨师只负责做菜,不负责端盘子、收银。每个人专注自己的事,效率最高。
低耦合:就像厨师和服务员之间只需要"菜单"这个接口,不需要知道对方的具体工作流程。换一个厨师,服务员的工作不受影响。
.c 和 .h 文件的职责
| 文件类型 | 职责 | 示例内容 |
|---|---|---|
| .h 头文件 | 对外接口声明 | 函数声明、宏定义、类型定义、extern 变量声明 |
| .c 源文件 | 内部实现细节 | 函数实现、static 内部函数、内部变量 |
.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 忽略文件
- 每个外设一个文件夹,包含 .c 和 .h 文件;
- 头文件使用
#ifndef防止重复包含; - 源文件包含对应的头文件;
- main.c 只包含初始化代码和主循环,不写具体硬件操作;
- BSP 层封装硬件细节,应用层只调用接口函数。
Keil 工程文件组织
在 Keil MDK 中,通过 Group 来组织文件:
| Group 名称 | 包含文件 | 说明 |
|---|---|---|
| Application/User | main.c, stm32f1xx_it.c | 用户应用代码 |
| Drivers/BSP | led.c, key.c, oled.c | 板级驱动 |
| Drivers/HAL | stm32f1xx_hal_gpio.c 等 | HAL 库源文件 |
| Drivers/CMSIS | system_stm32f1xx.c | ARM 内核支持 |
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 个字符 |
| 空行 | 函数之间空一行,逻辑段落之间空一行 |
| 空格 | 运算符两边加空格,逗号后面加空格 |
/**
* @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 DebugLED 指示
LED Debug逻辑分析仪
Logic Analyzer串口调试输出(重定向 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 窗口调试
在 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 工作流程
.gitignore 文件
嵌入式项目中,以下文件不应提交到 Git:
# Keil 编译生成的文件
*.o
*.d
*.axf
*.hex
*.bin
*.lst
*.map
Listings/
Objects/
# IDE 配置文件
*.uvoptx
*.uvguix.*
# 系统文件
.DS_Store
Thumbs.db
- 每次提交写清楚说明("修复了什么"、"添加了什么");
- 一个功能一个分支,完成后合并;
- 定期推送到远程仓库备份;
- 不要提交编译生成的文件(如 .o、.hex);
- 提交前先
git diff检查修改内容。
6项目实战:智能温湿度监测系统
综合运用所学知识,设计一个智能温湿度监测系统。下面按项目开发流程逐步讲解。
第一步:需求分析
- 使用 DHT11 传感器采集温湿度数据;
- 在 OLED 屏幕上实时显示温度和湿度;
- 温度超过阈值时蜂鸣器报警;
- 通过按键设置报警阈值;
- 通过串口上传数据到电脑。
第二步:模块划分
| 模块文件 | 功能 | 接口函数 |
|---|---|---|
dht11.c/h | DHT11 温湿度传感器驱动 | dht11_init(), dht11_read() |
oled.c/h | OLED 显示屏驱动 | 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() |
第三步:代码实现
/**
* @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 能正确显示温度和湿度
- ✅ 按键能修改报警阈值
- ✅ 温度超限时蜂鸣器报警
- ✅ 串口能正确上传数据
- ✅ 长时间运行稳定不崩溃
⭐重点例题
/* ========== 标准库写法 ========== */
// 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 修饰内部数组,不暴露给外部。
// 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;
}
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 文件的职责分别是什么?
extern 和 static 关键字的作用是什么?
Git 的作用是什么?
嵌入式项目中常用的调试方法有哪些?
调试时应该先检查什么?
如何重定向 printf 到串口?
main.c 应该包含哪些内容?
项目开发的完整流程是什么?
什么是高内聚低耦合?
Git 中 .gitignore 文件的作用是什么?
📝强化题库
选择题点选即时判分;填空题输入后"检查"或"显示答案"。