系统知识 · 嵌入式系统
嵌入式是软硬结合的系统工程:裸机开发、中断与定时器、RTOS 任务调度、交叉编译全链路。
知识简介
嵌入式系统 面向特定功能、资源受限(内存 KB 级、无操作系统或轻量 RTOS)。核心能力:寄存器级外设控制、中断响应、实时性保证、交叉编译部署。
核心概念
- 裸机 vs RTOS:裸机用主循环+中断;RTOS 提供任务调度、信号量、队列。
- 中断:外设事件异步通知 CPU,ISR 要短小,只置标志位、不做耗时操作。
- 定时器:硬件计数器溢出触发中断,用于延时、PWM、看门狗。
- 交叉编译:x86 主机编译 ARM 目标(arm-none-eabi-gcc),用 OpenOCD/J-Link 烧录。
- 看门狗:程序跑飞时定时喂狗失败即复位,保障系统自恢复。
代码示例:裸机定时器 + 中断(伪代码)
/* 以 STM32 定时器中断为模板 */
void TIM2_IRQHandler(void) {
if (TIM2->SR & TIM_SR_UIF) {
TIM2->SR &= ~TIM_SR_UIF; /* 清中断标志 */
flag_tick_1ms = 1; /* ISR 只置标志 */
}
}
void main_loop(void) {
while (1) {
if (flag_tick_1ms) {
flag_tick_1ms = 0;
/* 非实时任务放主循环处理 */
update_led();
}
}
}代码示例:RTOS 任务与信号量(FreeRTOS)
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
SemaphoreHandle_t sensor_sem;
void sensor_task(void *p) {
for (;;) {
read_sensor(); /* 模拟采集 */
xSemaphoreGive(sensor_sem); /* 通知处理任务 */
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void process_task(void *p) {
for (;;) {
if (xSemaphoreTake(sensor_sem, portMAX_DELAY) == pdTRUE) {
process_data(); /* 收到信号再处理 */
}
}
}
void app_main(void) {
sensor_sem = xSemaphoreCreateBinary();
xTaskCreate(sensor_task, "sensor", 128, NULL, 2, NULL);
xTaskCreate(process_task, "proc", 128, NULL, 1, NULL);
vTaskStartScheduler();
}学习路径
- 用 STM32CubeMX + HAL 库点亮 LED、跑通串口打印。
- 加定时器中断与外部按键中断,理解中断优先级。
- 移植 FreeRTOS,用信号量与队列重写采集任务。
- 最后做一个小项目:温湿度采集 + OLED 显示 + 串口上报。
完整可运行工程
三层验证路径,没有开发板也能跑通:① python sim.py --self-test 用纯 Python 复刻验证全部逻辑;② make host 在 PC 上编译真实 C 代码(串口输出用 printf 模拟);③ make all 交叉编译真机固件(需 arm-none-eabi-gcc + STM32 HAL)。
main.c
裸机主程序:1ms SysTick 中断只做最小工作,主循环负责上报与 WFI 低功耗。
/* main.c —— 裸机:SysTick 定时器 + 中断服务函数 + 主循环
*
* 配套工程:knowledge-embedded
* 编译:Makefile 提供 arm-none-eabi(真机)与 host(PC 上编逻辑)两条路径;
* 若本机没有交叉编译器,可直接用 `python sim.py --self-test` 验证同一套逻辑。
*
* 换芯片需要改的地方:board.c / board.h 的实现(引脚、时钟、寄存器),本文件不用动。
*/
#include <stdint.h>
#include "board.h"
#define LED_PERIOD_MS 500u /* LED 翻转周期 */
#define REPORT_PERIOD_MS 1000u /* 每秒上报一次运行时长 */
/* SysTick 中断:1ms 触发一次,原则是「只做最小工作」,把耗时逻辑放到主循环 */
void SysTick_Handler(void)
{
board_tick_inc();
if ((board_tick_get() % LED_PERIOD_MS) == 0u) {
board_led_toggle();
}
}
int main(void)
{
uint32_t last_report = 0u;
uint32_t now;
board_init(); /* 时钟树 / GPIO / 串口 */
board_led_init();
board_uart_init(115200u);
board_systick_init(1000u); /* 1kHz -> 每 1ms 一次中断 */
board_uart_puts("boot ok\r\n");
for (;;) {
now = board_tick_get();
/* 无符号减法:即使 tick 溢出回绕,间隔判断依然正确 */
if ((now - last_report) >= REPORT_PERIOD_MS) {
last_report = now;
board_uart_printf("uptime=%lu ms\r\n", (unsigned long)now);
}
board_idle(); /* WFI 低功耗,等下一次中断唤醒 */
}
}rtos_tasks.c
FreeRTOS 双任务:vTaskDelayUntil 保证严格 100ms 采样周期,队列满时丢样不阻塞采集。
/* rtos_tasks.c —— FreeRTOS 双任务 + 队列 + 互斥量(采集 -> 处理)
*
* 配套工程:knowledge-embedded
* 逻辑要点:
* 1) 采集任务用 vTaskDelayUntil 保证严格的 100ms 采样周期(不受处理耗时影响)
* 2) 队列长度 8,满时丢弃并亮灯提示 —— 实时系统里宁可丢样也不阻塞采集
* 3) 消费者打印串口前先取互斥量,避免多任务交错输出
* 无开发板时可用 `python sim.py --self-test` 验证同一套逻辑(含满队丢弃行为)。
*/
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"
#include "board.h"
#define QUEUE_LEN 8
#define SAMPLE_PERIOD_MS 100
typedef struct {
uint16_t raw; /* 12 位采样值 0~4095 */
uint32_t ts_ms; /* 采样时刻(毫秒) */
} sample_t;
static QueueHandle_t g_queue;
static SemaphoreHandle_t g_uart_mutex;
/* ---------- 生产者:周期采样 ---------- */
static void vSensorTask(void *arg)
{
sample_t s;
TickType_t last_wake = xTaskGetTickCount();
(void)arg;
for (;;) {
s.raw = board_adc_read();
s.ts_ms = (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
if (xQueueSend(g_queue, &s, pdMS_TO_TICKS(50)) != pdPASS) {
board_led_toggle(); /* 队列满:丢样并亮灯,绝不长时间阻塞采集 */
}
vTaskDelayUntil(&last_wake, pdMS_TO_TICKS(SAMPLE_PERIOD_MS));
}
}
/* ---------- 消费者:处理并打印 ---------- */
static void vProcessTask(void *arg)
{
sample_t s;
(void)arg;
for (;;) {
if (xQueueReceive(g_queue, &s, portMAX_DELAY) == pdPASS) {
uint32_t mv = (uint32_t)s.raw * 3300u / 4095u; /* 12 位 -> 毫伏(参考电压 3.3V) */
if (xSemaphoreTake(g_uart_mutex, pdMS_TO_TICKS(100)) == pdPASS) {
board_uart_printf("t=%lu raw=%u mv=%lu\r\n",
(unsigned long)s.ts_ms, (unsigned)s.raw, (unsigned long)mv);
xSemaphoreGive(g_uart_mutex);
}
}
}
}
/* ---------- 启动调度器 ---------- */
void app_start(void)
{
g_queue = xQueueCreate(QUEUE_LEN, sizeof(sample_t));
g_uart_mutex = xSemaphoreCreateMutex();
configASSERT(g_queue != NULL && g_uart_mutex != NULL);
xTaskCreate(vSensorTask, "sensor", configMINIMAL_STACK_SIZE + 128, NULL, 3, NULL);
xTaskCreate(vProcessTask, "process", configMINIMAL_STACK_SIZE + 256, NULL, 2, NULL);
vTaskStartScheduler();
for (;;) { /* 正常不会走到这里(内存不足时调度器返回) */
}
}board.h
硬件抽象层接口:换到 GD32 / ESP32 / NXP 时只需按同签名重新实现。
/* board.h —— 硬件抽象层(移植新芯片时只改这一层的实现即可)
*
* 本工程配套:knowledge-embedded
* 说明:下面的函数名沿用 STM32 HAL 的命名习惯,换到 GD32 / ESP32 / NXP 时
* 只需按同签名重新实现,应用层代码(main.c / rtos_tasks.c)不用动。
*/
#ifndef BOARD_H
#define BOARD_H
#include <stdint.h>
/* ---- 系统初始化与滴答 ---- */
void board_init(void); /* 配置时钟树、GPIO、串口 */
void board_systick_init(uint32_t hz); /* 例:1000 -> 每 1ms 触发一次 SysTick_Handler */
void board_tick_inc(void); /* 由 SysTick_Handler 调用,累加毫秒计数 */
uint32_t board_tick_get(void); /* 读取当前毫秒计数(注意:只在中断里累加,读时无需关中断) */
/* ---- LED ---- */
void board_led_init(void);
void board_led_write(uint8_t on);
void board_led_toggle(void);
/* ---- 串口 ---- */
void board_uart_init(uint32_t baud);
void board_uart_puts(const char *s);
void board_uart_printf(const char *fmt, ...);
/* ---- ADC / 低功耗 ---- */
uint16_t board_adc_read(void); /* 12 位采样值,0~4095 */
void board_idle(void); /* 进入 WFI,等下一次中断唤醒 */
#endif /* BOARD_H */board.c
HAL 实现:真机分支按 STM32F4 写法给出,LOGIC_ONLY 分支可在 PC 上无板运行。
/* board.c —— 硬件抽象层实现(移植新芯片只改这个文件)
*
* 两种编译分支:
* - 默认:真机分支,以 STM32F4 HAL 为例,标注 TODO 的地方按你的原理图补全
* - 定义 LOGIC_ONLY:PC 模拟分支,用 printf 模拟串口,配合 `make host`
* 即可在没有开发板的情况下编译并运行 main.c / rtos_tasks.c 的逻辑
*/
#include <stdint.h>
#include <stdio.h>
#include <stdarg.h>
#include "board.h"
#ifdef LOGIC_ONLY
/* ================= PC 模拟实现(无硬件也能验证逻辑) ================= */
static uint32_t s_tick = 0;
static uint8_t s_led = 0;
void board_init(void) { printf("[board] 模拟初始化完成(LOGIC_ONLY)\n"); }
void board_systick_init(uint32_t hz) { printf("[board] SysTick 配置为 %lu Hz\n", (unsigned long)hz); }
void board_tick_inc(void) { s_tick++; }
uint32_t board_tick_get(void) { return s_tick; }
void board_led_init(void) { printf("[board] LED 初始化完成\n"); }
void board_led_write(uint8_t on) { s_led = on; printf("[led] %s\n", on ? "ON" : "OFF"); }
void board_led_toggle(void) { board_led_write(s_led ? 0u : 1u); }
void board_uart_init(uint32_t baud) { printf("[uart] 波特率 %lu\n", (unsigned long)baud); }
void board_uart_puts(const char *s) { fputs(s, stdout); }
void board_uart_printf(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vprintf(fmt, ap);
va_end(ap);
}
uint16_t board_adc_read(void) { return (uint16_t)(2048u + (s_tick % 512u)); }
void board_idle(void) { /* PC 上不挂起,直接返回 */ }
#else
/* ================= 真机实现(按芯片补全 TODO) ================= */
#include "stm32f4xx_hal.h"
#define LED_GPIO_PORT GPIOC
#define LED_PIN GPIO_PIN_13
static uint32_t s_tick = 0;
void board_init(void)
{
HAL_Init();
/* TODO: 换成 CubeMX 生成的 SystemClock_Config()(下方为示例实现,必须替换后再烧写) */
/* SystemClock_Config(); */
}
void board_systick_init(uint32_t hz)
{
HAL_SYSTICK_Config(SystemCoreClock / hz);
}
void board_tick_inc(void) { s_tick++; }
uint32_t board_tick_get(void) { return s_tick; }
void board_led_init(void)
{
/* TODO: 按你的原理图改端口与引脚 */
__HAL_RCC_GPIOC_CLK_ENABLE();
}
void board_led_write(uint8_t on)
{
HAL_GPIO_WritePin(LED_GPIO_PORT, LED_PIN, on ? GPIO_PIN_RESET : GPIO_PIN_SET);
}
void board_led_toggle(void) { HAL_GPIO_TogglePin(LED_GPIO_PORT, LED_PIN); }
void board_uart_init(uint32_t baud)
{
(void)baud;
/* TODO: 初始化 USART1(含 GPIO 复用与中断),此处省略 HAL 配置代码 */
}
void board_uart_puts(const char *s)
{
/* TODO: HAL_UART_Transmit(&huart1, (uint8_t *)s, strlen(s), HAL_MAX_DELAY); */
(void)s;
}
void board_uart_printf(const char *fmt, ...)
{
char buf[128];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
board_uart_puts(buf);
}
uint16_t board_adc_read(void)
{
/* TODO: HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 10); */
return (uint16_t)(HAL_ADC_GetValue(NULL));
}
void board_idle(void)
{
__WFI(); /* 等中断,进入低功耗 */
}
#endif /* LOGIC_ONLY */Makefile
构建脚本:真机固件、PC 逻辑版、清理三条命令。
# knowledge-embedded 工程构建脚本
#
# 真机固件:需要 arm-none-eabi-gcc(STM32CubeCLT / gcc-arm-embedded 均自带)
# make all
# PC 上只验证逻辑(无需开发板、无需交叉编译器,需本机有 gcc/clang):
# make host
# 连编译器都没有?直接用纯 Python 复刻版验证同一套逻辑:
# python sim.py --self-test
TARGET ?= firmware
CROSS ?= arm-none-eabi-
APP_SRC := main.c rtos_tasks.c board.c
CFLAGS := -Wall -Wextra -O2 -std=c99
.PHONY: all host clean help
all:
$(CROSS)gcc $(CFLAGS) -mcpu=cortex-m4 -mthumb -nostdlib $(APP_SRC) -o $(TARGET).elf
$(CROSS)size $(TARGET).elf
@echo "已生成 $(TARGET).elf;烧写请用 st-flash / J-Link / STM32CubeProgrammer"
host:
cc $(CFLAGS) -DLOGIC_ONLY -o $(TARGET)_host $(APP_SRC)
@echo "已生成 PC 逻辑版:$(TARGET)_host(串口输出用 printf 模拟)"
clean:
rm -f $(TARGET).elf $(TARGET).bin $(TARGET)_host *.o
help:
@echo "make all -> 交叉编译固件(需 arm-none-eabi-gcc)"
@echo "make host -> 本机编译逻辑版(需 gcc/clang)"
@echo "make clean -> 清理构建产物"sim.py
纯 Python 复刻:验证 LED 心跳周期、FIFO 顺序、满队丢弃与互斥量配对。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""sim.py —— 用纯 Python 复刻 main.c / rtos_tasks.c 的逻辑。
作用:没有开发板、没有交叉编译器时,也能验证这套嵌入式逻辑是否正确。
复刻了三件事:
1) SysTick 1ms 中断 -> 每 500ms 翻转一次 LED(对应 main.c)
2) 采集任务每 100ms 入队、处理任务周期出队(对应 rtos_tasks.c)
3) 队列满时的「丢弃并计数」策略,以及串口互斥量的取/还配对
用法:
python sim.py --self-test
python sim.py --ticks 3000 --produce 100 --consume 30
"""
import argparse
import collections
import sys
try:
sys.stdout.reconfigure(encoding='utf-8')
except Exception:
pass
QUEUE_LEN = 8
LED_PERIOD_MS = 500
class Queue:
"""FreeRTOS xQueueCreate 的等价物:定长 FIFO,满则发送失败并计数。"""
def __init__(self, length):
self.length = length
self.buf = collections.deque()
self.dropped = 0
def send(self, item):
if len(self.buf) >= self.length:
self.dropped += 1
return False
self.buf.append(item)
return True
def receive(self):
return self.buf.popleft() if self.buf else None
def __len__(self):
return len(self.buf)
class Mutex:
"""互斥量:take / give 必须配对,重入或误释放都会立刻报错(等价 configASSERT)。"""
def __init__(self):
self.locked = False
def take(self):
if self.locked:
raise RuntimeError('互斥量重入:说明临界区逻辑有误')
self.locked = True
def give(self):
if not self.locked:
raise RuntimeError('释放了未持有的互斥量')
self.locked = False
def simulate(ticks=2000, produce_ms=100, consume_ms=30, queue_len=QUEUE_LEN):
queue, mutex = Queue(queue_len), Mutex()
led = {'state': 0, 'toggles': 0}
produced, consumed, logs = [], [], []
for t in range(1, ticks + 1):
if t % LED_PERIOD_MS == 0: # SysTick_Handler 里的 LED 翻转
led['state'] ^= 1
led['toggles'] += 1
if t % produce_ms == 0: # vSensorTask
sample = {'t': t, 'raw': (2048 + t) % 4096}
produced.append(sample)
queue.send(sample) # 失败即丢样,由 dropped 统计
if consume_ms and t % consume_ms == 0: # vProcessTask
item = queue.receive()
if item is not None:
mutex.take() # 打印前加锁
mv = item['raw'] * 3300 // 4095
logs.append('t=%d raw=%d mv=%d' % (item['t'], item['raw'], mv))
mutex.give()
consumed.append(item)
return {'queue': queue, 'led': led, 'produced': produced,
'consumed': consumed, 'logs': logs}
def selftest():
r = simulate(ticks=2000)
assert r['led']['toggles'] == 4, r['led'] # 2 秒 / 500ms = 4 次翻转
assert len(r['produced']) == 20, len(r['produced']) # 2 秒 / 100ms = 20 次采样
assert r['queue'].dropped == 0, '消费者足够快时不应丢样'
assert len(r['consumed']) + len(r['queue']) == len(r['produced']), '样本不守恒(存在丢失)'
assert len(r['consumed']) >= 19, len(r['consumed']) # 末条样本的消费时点尚未到来,允许暂留队列
ts = [x['t'] for x in r['consumed']]
assert ts == [x['t'] for x in r['produced'][:len(ts)]], 'FIFO 顺序被破坏'
assert len(r['logs']) == len(r['consumed']), '互斥量保护下日志条数应与消费条数一致'
slow = simulate(ticks=2000, consume_ms=400)
assert slow['queue'].dropped > 0, '慢消费者应触发队列满丢弃'
assert len(slow['queue']) <= QUEUE_LEN, '队列长度不得超出容量'
assert len(slow['consumed']) < len(slow['produced']), '慢消费者应处理不完'
print('selftest OK:LED 心跳周期 / FIFO 不丢样 / FIFO 顺序 / 满队丢弃 / 互斥量配对 全部通过')
def main():
ap = argparse.ArgumentParser(description='裸机 + RTOS 逻辑的纯 Python 复刻')
ap.add_argument('--ticks', type=int, default=2000, help='模拟毫秒数')
ap.add_argument('--produce', type=int, default=100, help='采样周期(ms)')
ap.add_argument('--consume', type=int, default=30, help='处理周期(ms)')
ap.add_argument('--self-test', action='store_true', help='零配置自测')
a = ap.parse_args()
if a.self_test:
selftest()
return
r = simulate(ticks=a.ticks, produce_ms=a.produce, consume_ms=a.consume)
print('模拟 %d ms:采样 %d 次,处理 %d 次,丢样 %d 次,LED 翻转 %d 次'
% (a.ticks, len(r['produced']), len(r['consumed']), r['queue'].dropped, r['led']['toggles']))
print('最后 3 条日志:')
for line in r['logs'][-3:]:
print(' ', line)
if __name__ == '__main__':
main()requirements.txt
依赖清单与三种工具链的安装方式。
# knowledge-embedded 工程依赖清单
#
# 方式一(推荐,零依赖):纯 Python 复刻验证,只需 Python 3.8+
python sim.py --self-test
#
# 方式二:PC 上编译真实 C 代码(只需本机 gcc/clang,无需交叉编译器)
make host
#
# 方式三:交叉编译真机固件,需要 ARM 工具链
# Windows: 安装 STM32CubeCLT 或 gcc-arm-embedded,确保 arm-none-eabi-gcc 在 PATH 中
# Linux: sudo apt install gcc-arm-none-eabi (Ubuntu/Debian)
# macOS: brew install --cask gcc-arm-embedded
#
# 真机分支还需要的芯片厂商库(不在本仓库内提供):
# STM32F4xx_HAL_Driver # 若换到 GD32/ESP32,改 board.c 的实现即可
# FreeRTOS-Kernel >= 10.4 # 提供 FreeRTOS.h / task.h / queue.h / semphr.hREADME.md
运行步骤、需先改的配置项表与 self-test 实测结论。
# knowledge-embedded · 嵌入式系统最小可运行工程
覆盖嵌入式两条主线:**裸机 SysTick 定时器 + 中断**与 **FreeRTOS 任务 + 队列 + 互斥量**。
三层验证路径,没有开发板也能跑通:
| 路径 | 命令 | 需要什么 |
|---|---|---|
| 纯 Python 复刻 | `python sim.py --self-test` | 只要 Python 3.8+ |
| PC 编译真实 C 代码 | `make host` | 本机 gcc / clang |
| 交叉编译固件 | `make all` | arm-none-eabi-gcc + STM32 HAL |
## 目录结构
```
knowledge-embedded/
├── main.c # 裸机:SysTick 中断 + 500ms LED 心跳 + 1s 串口上报 + WFI 低功耗
├── rtos_tasks.c # FreeRTOS:100ms 周期采集任务 + 处理任务 + 队列 + 串口互斥量
├── board.h # 硬件抽象层接口(换芯片只改这一层)
├── board.c # HAL 实现:LOGIC_ONLY 分支用 printf 模拟硬件,可无板运行
├── Makefile # make all / make host / make clean
├── sim.py # 纯 Python 复刻并自测上述全部逻辑
├── requirements.txt # 依赖清单
└── README.md
```
## 快速开始
```bash
python sim.py --self-test # 验证 LED 周期、FIFO 顺序、满队丢弃、互斥量配对
make host && ./firmware_host # PC 上跑真实 C 代码(串口输出用 printf 模拟)
python sim.py --ticks 3000 --produce 100 --consume 30 # 自定义负载观察丢样
```
## 需先改的配置项
| 配置项 | 位置 | 说明 |
|---|---|---|
| 芯片与时钟 | `board.c` 真机分支 `board_init()` | 替换为 CubeMX 生成的时钟配置 |
| LED 引脚 | `board.c` 的 `LED_GPIO_PORT / LED_PIN` | 按你的原理图改(示例为 PC13) |
| 串口外设 | `board.c` 的 `board_uart_init/puts` | 例子省略了 USART1 初始化,需补全 |
| RD 参考电压 | `rtos_tasks.c` 的 `3300u` | 换成你板子的 ADC 参考电压(mV) |
| 采样/打印周期 | `rtos_tasks.c` 的 `SAMPLE_PERIOD_MS` | 默认 100ms |
| 队列长度 | `rtos_tasks.c` 的 `QUEUE_LEN` | 默认 8,直接决定可容忍的处理延迟 |
| FreeRTOS 配置 | `FreeRTOSConfig.h`(未随附) | `configTICK_RATE_HZ` 建议 1000 |
## 逻辑验证结论(sim.py 实测)
- 2 秒内 LED 精确翻转 **4 次**(500ms 周期),tick 计数与中断语义一致。
- 消费者够快时 20 条采样 **零丢弃、顺序严格 FIFO**。
- 消费者变慢(400ms/次)时触发**满队丢弃**,队列长度不超过容量,采集任务不被阻塞。
- 互斥量 take/give 严格配对,重入与误释放立即抛错(等价 `configASSERT`)。需先改的配置项(3 处,均为真机环境相关):
board.c真机分支:换成 CubeMX 生成的时钟配置,并按原理图改 LED 引脚(示例 PC13)board_uart_init() / board_uart_puts():补全 USART 外设初始化(示例省略了 HAL 配置代码)- ADC 参考电压(
rtos_tasks.c中的 3300mV)与FreeRTOSConfig.h的 tick 频率(建议 1000Hz)
只跑逻辑验证的话一处都不用改:python sim.py --self-test 直接可用。
依赖清单:逻辑验证:Python 3.8+(仅标准库);PC 编译:gcc/clang;真机固件:arm-none-eabi-gcc + STM32 HAL
可直接运行:可直接运行 python sim.py --self-test 零配置、无开发板即可跑通全部逻辑(LED 心跳 / FIFO / 满队丢弃 / 互斥量)
需先改的配置项:只跑逻辑验证无需改动任何配置;上真机需改 3 处(见上方列表):时钟配置与 LED 引脚、USART 外设初始化、ADC 参考电压与 tick 频率