2024 全国大学生电子设计竞赛 H 题 — 自动行驶小车
8 路灰度 · MPU6050 · TB6612 · 编码器闭环 · OLED 显示
本仓库包含 两个版本 :
git clone https://github.com/5ee511/car.git
# v1.3: car_v1.3(24电赛H)/car/user/Project.uvprojx
# Keil MDK → F7 编译 → F8 烧录 (ST-Link/J-Link)
开机 OLED 菜单,Btn3 切换 ,Btn2 确认 。
模式
功能
Q1
A→B 直线盲走,遇线停车
Q2
A→B→C→D→A 矩形巡线
Q3
A→C 弧线 → B→D 弧线
Q4
Q3 × 4 圈,独立角度表补偿陀螺漂移
TEST
电机 PID 调参,串口实时调 KP/KI/KD
组件
型号
主控
STM32F103C8T6 (Cortex-M3, 72MHz)
陀螺仪
MPU6050 (I2C, DMP 解算)
灰度
亚博智能 8 路红外
驱动
TB6612FNG 双 H 桥
编码器
霍尔编码器 AB 相
显示
0.96" OLED (I2C)
按键
3 键 (PB11/PB12/PA12)
功能
引脚
电机 PWM
PA0(A) PA1(B)
电机方向
PA5/PA7(A) PA4/PA3(B)
编码器
PA6/PA2(A) PB4/PB5(B)
灰度
PB13/14/15(AD0~2) PA15(OUT)
I2C
PB6(SCL) PB7(SDA)
按键
PB11 PB12 PA12
USART1
PA9(TX) PA10(RX)
层级
位置
频率
职责
方向控制
main 循环
~12.5Hz
灰度/角度 → 目标速度
速度控制
TIM3 中断
10Hz
编码器 → 增量 PID → PWM
传感器权重外大内小(5-4-3-1-1-3-4-5) + 差比和归一化 + 变化率限制(±8/次) → 平滑过弯
KP
KI
KD
OUT_MAX
0.8
0.05
3.0
35
car/ # 仓库根目录
├── car_v1.3(24电赛H)/car/ # ⭐ v1.3: 四问全覆盖
│ ├── code/ # 应用层 (motor/pid/track/key)
│ ├── ml_libs/ # 底层驱动
│ ├── user/ # 入口 (main.c + Keil工程)
│ ├── 移植代码相关/ # 参考文档
│ └── AGENTS.md # 项目全景说明书
├── car_v1.2(直角弯+蜂鸣器)/ # v1.2: 早期版本
└── platformio.ini # PlatformIO 烧录
Made with ❤️ for 2024 全国大学生电子设计竞赛
│ ├── pid.c/h # PID 控制器 & 速度闭环
│ ├── filter.c/h # 卡尔曼 & 互补滤波(姿态解算)
│ ├── grayscale_sensor.c/h # 8 路灰度传感器驱动
│ ├── gray_track.c/h # 巡线算法(核心)
│ └── readme.txt
├── menu/ # ⚠️ 已停用 — OLED 两级菜单系统(源码未纳入仓库,仅残留 .o/.crf)
│ ├── oled.c/h # OLED 驱动(显存架构+像素坐标+反显)
│ ├── oled_font.c/h # ASCII + 中文字模数据
│ ├── key.c/h # 3 按键驱动(PB11/PB12/PA12)
│ └── menu.c/h # 两级菜单框架(2024 电赛 H 题)
├── ml_libs/ # 底层驱动库
│ ├── ml_gpio.c/h # GPIO 配置
│ ├── ml_pwm.c/h # PWM 输出(TIM2~4)
│ ├── ml_tim.c/h # 定时器中断
│ ├── ml_uart.c/h # 串口通信
│ ├── ml_i2c.c/h # 模拟 I2C(SCL=PB6, SDA=PB7)
│ ├── ml_exti.c/h # 外部中断
│ ├── ml_mpu6050.c/h # MPU6050 六轴传感器
│ ├── ml_hmc5883l.c/h # HMC5883L 磁力计
│ ├── ml_oled.c/h # OLED 旧驱动(已被 menu/oled 替代,保留备用)
│ └── ml_delay.c/h # 微秒/毫秒延时
├── sys/ # CMSIS 系统文件
│ ├── stm32f10x.h # 寄存器定义
│ ├── system_stm32f10x.c/h # 系统时钟初始化
│ └── startup_*.s # 启动文件
├── user/ # 用户入口
│ ├── main.c # 主函数 & 初始化
│ └── isr.c # 中断服务函数├── skills/ # AI Agent 调试方法论(协作效率提升)└── .vscode/ # VS Code C/C++ 配置
└── c_cpp_properties.json
```
引脚
信号
说明
PA0
PWMA
TIM2_CH1,电机 A 速度 (PWM)
PA1
PWMB
TIM2_CH2,电机 B 速度 (PWM)
PA5
AIN1
电机 A 方向控制 1
PA7
AIN2
电机 A 方向控制 2
PA4
BIN1
电机 B 方向控制 1
PA3
BIN2
电机 B 方向控制 2
PB12
STBY
已硬接 3.3V,不再占用(引脚释放给 Key2)
引脚
按键
说明
PB11
Key1
上翻
PB12
Key2
下翻
PA12
Key3
确认 / 进入菜单
引脚
信号
说明
PA6
编码器1 A相
EXTI6,下降沿计数
PA2
编码器1 B相
方向判断(输入上拉)
PB4
编码器2 A相
EXTI4,下降沿计数
PB5
编码器2 B相
方向判断(输入上拉)
引脚
信号
说明
PB13
AD0
通道选择 bit0(输出)
PB14
AD1
通道选择 bit1(输出)
PB15
AD2
通道选择 bit2(输出)
PA15
OUT
传感器数字输出(输入)
I2C 总线(MPU6050 + HMC5883L + OLED)
引脚
信号
说明
PB6
SCL
I2C 时钟(开漏)
PB7
SDA
I2C 数据(开漏)
引脚
信号
说明
PA9
UART1 TX
调试串口 (115200)
PA10
UART1 RX
调试串口
PB0
MPU6050 INT
EXTI0 上升沿,数据就绪中断
PA13
SWDIO
SWD 调试
PA14
SWCLK
SWD 调试
⚠️ 原基于 2024 电赛 H 题的两级菜单系统已从 main.c 中移除。当前 OLED 仅用于实时状态显示(4 行:传感器值/巡线状态/编码器速度/PID 参数),无按键交互。详见 AGENTS.md 中「OLED 显示布局约定」。
原始菜单系统文档(仅供参考,已停用)
一级菜单(6项,2页)
├── <- 返回(退出菜单回到巡线)
├── 任务模式选择 → 任务1: A→B / 任务2: A-B-C-D-A / 任务3: A-C-B-D-A / 任务4: 自动4圈
├── 速度参数设置 → 5 档巡线速度(FAST/MED/SLOW/SHARP/MIN)
├── PID参数调节 → 电机 A/B 的 P/I/D 值
├── 传感器数据 → 8 路灰度 + 编码器 实时刷新
└── 系统信息 → 版本 + 当前任务编号
按键
主循环
菜单内
Key3 (PA12)
按下进入菜单
确认选中项
Key1 (PB11)
—
上翻
Key2 (PB12)
—
下翻
旧驱动 ml_oled.c:行/列 API,直接写屏,无反显 → 已被 headfile.h 注释掉
新驱动 menu/oled.c:像素坐标 X/Y API,显存架构 OLED_DisplayBuf[8][128],支持 OLED_ReverseArea() 高亮、OLED_Update() 批量刷新
I2C 仍共用 PB6(SCL)/PB7(SDA),与 MPU6050/HMC5883L 同一条总线
flowchart TB
subgraph Main["main 循环 (~12.5Hz)"]
GS["Grayscale_Sensor_Read_All()<br/>读取 8 路传感器"] --> TR["track()<br/>巡线算法计算目标速度"]
TR --> MTS["motor_target_set(L,R)<br/>设置电机目标速度"]
end
subgraph TIM3["TIM3 中断 (10Hz)"]
ENC["读取编码器脉冲<br/>Encoder_count1/2"] --> PC["pid_control()"]
PC --> CAL["pid_cal(&motorA/B)<br/>增量式速度 PID"]
CAL --> PWM["motorA_duty() / motorB_duty()<br/>PWM 输出到 TB6612"]
end
subgraph EXTI0["EXTI0 中断 (MPU6050 INT)"]
MPU["MPU6050_GetData() + HMC5883L_GetData()"] --> KF["Kalman_Filter()<br/>姿态角融合"]
end
MTS -.->|"motorA.target<br/>motorB.target"| CAL
Loading
层级
运行位置
频率
功能
上层:方向控制
main 循环 track()
~12.5Hz
读灰度传感器 → 算偏差 → 设目标速度
下层:速度控制
TIM3 中断 pid_control()
10Hz
读编码器 → 增量式 PID → PWM 输出
上层决定"往哪走、走多快",下层保证"实际速度跟得上目标速度"。两层解耦,互不干扰。
flowchart LR
subgraph 输入["8 路二值传感器 (0/1)"]
D1["d1"] --> D2["d2"] --> D3["d3"] --> D4["d4"]
D5["d5"] --> D6["d6"] --> D7["d7"] --> D8["d8"]
end
subgraph 左半["左侧计算"]
D1 -->|"×5"| SL["sum_L"]
D2 -->|"×4"| SL
D3 -->|"×3"| SL
D4 -->|"×1"| SL
end
subgraph 右半["右侧计算"]
D5 -->|"×1"| SR["sum_R"]
D6 -->|"×3"| SR
D7 -->|"×4"| SR
D8 -->|"×5"| SR
end
SL --> FORMULA["error = (sum_R − sum_L) × STEER_GAIN ÷ (cnt × 5)"]
SR --> FORMULA
FORMULA --> STEER["steer<br/>正=右转, 负=左转"]
Loading
传感器位置: d1 d2 d3 d4 │ d5 d6 d7 d8
最左 ←─────────────────中心────────────────→ 最右
权重: 5 4 3 1 │ 1 3 4 5
外层传感器权重大 (离中心远 → 位置信息多 → 对偏差贡献大)
内层传感器权重小 (离中心近 → 位置信息少 → 起微调作用)
$$\text{steer} = \frac{\sum_{i=4}^{7} (d[i] \times w[i]) - \sum_{i=0}^{3} (d[i] \times w[i])}{cnt \times 5} \times \text{STEER_GAIN}$$
分子:右侧加权和 − 左侧加权和(正 = 线偏右)
分母:踩线传感器总数 × 最大权重(自动归一化,不受踩线数量影响)
STEER_GAIN:比例系数(可调,默认 80)
flowchart TD
STEER["steer (转向量)"] --> ABS["abs(steer)"]
ABS --> SPD["speed = BASE_SPEED − abs(steer) ÷ 3<br/>弯道自动减速,最低 20"]
SPD --> TL["target_L = speed − steer"]
SPD --> TR["target_R = speed + steer"]
TL --> RL["RATE LIMIT (±8/次)<br/>防突变,平滑过渡"]
TR --> RL
RL --> OUT["motor_target_set(last_L, last_R)"]
Loading
// 不是直接把 target 塞给电机
// 而是每次最多变化 ±RATE_LIMIT(默认 8)
diff = target - last ;
if (diff > 8 ) diff = 8 ; // 单次最多加速 8
if (diff < -8 ) diff = -8 ; // 单次最多减速 8
last = last + diff ; // 平滑过渡
为什么需要 :传感器在 d4/d5 边界时值可能快速跳变 0↔1,不加限制会导致电机反复急加速急减速(抖舵)。变化率限制充当"低通滤波器",让转向平滑。
参数
位置
默认值
调法
BASE_SPEED
track()
50
直道速度,越大越快
STEER_GAIN
track()
80
转弯力度,不够→加大
RATE_LIMIT
track()
8
越小越平滑,越大越灵敏
电机 PID P
main.c
10
速度跟随响应速度
电机 PID I
main.c
10
消除稳态误差
电机 PID D
main.c
0
抑制超调
flowchart LR
TARGET["motorA.target<br/>(目标速度)"] --> ERR["error = target − now"]
ENC["Encoder_count1<br/>(编码器脉冲)"] --> NOW["motorA.now"]
NOW --> ERR
ERR --> DELTA["增量式 PID<br/>out += P×Δe + I×e + D×(Δe−Δe_prev)"]
DELTA --> PWM_OUT["motorA_duty(out)<br/>PWM 占空比"]
Loading
增量式 PID :输出 = 上次输出 + 增量。天然抗积分饱和,适合速度控制。
运行频率 10Hz(TIM3, 100ms),每次清零编码器计数。
MPU6050 的陀螺仪积分角度 + 加速度计/磁力计观测角度 → 卡尔曼滤波融合:
$$\text{Angle}_k = \text{Angle}_{k-1} + (\omega - \text{bias}) \cdot dt \quad \text{(预测)}$$
$$\text{Angle}_k = K \cdot \text{obs} + (1-K) \cdot \text{predict} \quad \text{(更新)}$$
三个轴(Roll/Pitch/Yaw)各有一个卡尔曼滤波器实例。
flowchart TD
START["系统上电"] --> CLK["系统时钟初始化<br/>(startup + system_stm32f10x)"]
CLK --> OLED["OLED_Init()"]
OLED --> MOTOR["motor_init()<br/>PWM + 方向引脚"]
MOTOR --> ENC["encoder_init()<br/>EXTI + 编码器引脚"]
ENC --> UART["uart_init(UART_1, 115200)"]
UART --> PID["pid_init() × 3<br/>motorA/B(增量) + angle(位置)"]
PID --> I2C["I2C_Init()"]
I2C --> MPU["MPU6050_Init() + HMC5883L_Init()"]
MPU --> EXTI["exti_init(EXTI_PB0, RISING)<br/>MPU6050 INT 使能"]
EXTI --> TIM["tim_interrupt_ms_init(TIM_3, 100)<br/>启动速度 PID 定时器"]
TIM --> LOOP["进入 while(1) 主循环"]
LOOP --> PRINT["printf 传感器调试数据"]
PRINT --> DELAY["delay_ms(20)"]
DELAY --> LOOP
Loading
🔧 巡线功能通过取消注释 Grayscale_Sensor_Read_All() 和 track() 启用。
项
说明
IDE
Keil MDK v5(编译)+ VS Code(编辑)
编译器
ARMCC v5/v6
编码
UTF-8 with BOM(所有 .c/.h 文件)
调试器
DAP-Link(SWD 接口,PA13/PA14)
芯片型号
STM32F103C8T6(中等容量,STM32F10X_MD)
项目根目录 .vscode/c_cpp_properties.json 已配置:
包含路径:user/, ml_libs/, sys/, code/, Keil ARMCC 头文件
宏定义:STM32F10X_MD, __CC_ARM
IntelliSense:ARM 模式
库文件前缀 ml_:底层驱动(MengLiang Library)
应用层无前缀:motor, pid, filter, gray_track
中文注释:UTF-8 编码,Keil → Edit → Configuration → Encoding → UTF-8
寄存器操作:直接操作 STM32 寄存器(非标准库)