智能骑行头盔技术传承备课教案

第一课时:项目总体架构与双平台对比

1.1 STM32F407 毕业设计架构(C语言 + FreeRTOS)

硬件拓扑框图:

┌─────────────────────────────────────────────────────────────────┐
│                    STM32F407VET6 最小系统                         │
│                    ARM Cortex-M4 @ 168MHz                        │
│                    Flash: 512KB  RAM: 192KB                      │
│                                                                  │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────────┐    │
│  │ I2C1     │  │ I2C2     │  │ I2C_模拟  │  │ USART1       │    │
│  │ PB6(SCL) │  │ PB10(SCL)│  │ PC10(SCL) │  │ PA9(TX)      │    │
│  │ PB7(SDA) │  │ PB11(SDA)│  │ PC11(SDA) │  │ PA10(RX)     │    │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └──────┬───────┘    │
│       │             │             │               │             │
│  ┌────▼─────┐  ┌────▼─────┐  ┌────▼─────┐  ┌─────▼─────────┐  │
│  │ MPU6050  │  │ TOF400C  │  │ TOF400C  │  │ M800AK 4G模组 │  │
│  │ 六轴姿态  │  │ 后方测距  │  │ 侧方测距  │  │ 定位+MQTT上云 │  │
│  └──────────┘  └──────────┘  └──────────┘  └───────────────┘  │
│                                                                  │
│  ┌──────────┐  ┌──────────────┐  ┌──────────┐  ┌──────────┐    │
│  │ USART2  │  │ TIM1_CH1/CH2 │  │ GPIO PA4 │  │ GPIO     │    │
│  │ PA2(TX) │  │ PE9, PE11    │  │ 推挽+下拉 │  │          │    │
│  │ PA3(RX) │  │ PWM+DMA      │  │          │  │          │    │
│  └────┬─────┘  └──────┬───────┘  └────┬─────┘  └──────────┘    │
│       │               │               │                         │
│  ┌────▼─────┐  ┌──────▼───────┐  ┌────▼─────┐                  │
│  │ ASR01    │  │ WS2812B×2   │  │ 震动马达  │                  │
│  │ 离线语音  │  │ 左/右灯带    │  │ 触觉反馈  │                  │
│  └──────────┘  └──────────────┘  └──────────┘                  │
└─────────────────────────────────────────────────────────────────┘

为什么要用 I2C 而不是其他协议?

讲到这里要深挖 I2C 的物理层:

  • I2C 只需要 2 根线:SCL(时钟线)+ SDA(数据线)
  • 总线拓扑:一条总线上可以挂多个设备,每个设备有唯一的 7 位地址
  • 主从架构:MCU 永远是 Master,传感器是 Slave
  • 开漏输出 + 上拉电阻:这是 I2C 最关键的电特性
I2C 总线的物理连接:
                          VCC(3.3V)
                             │
                         ┌───┴───┐
                         │  4.7kΩ │ 上拉电阻(必须!)
                         │        │
                         └───┬───┘
                             │
    ┌────────┬───────────────┼───────────────┬────────┐
    │        │               │               │        │
  SCL      SDA             SCL             SDA     ...
    │        │               │               │        │
  Master  (MCU)         Slave1          Slave2
  STM32              MPU6050(0x68)   TOF400C(0x29)

为什么需要上拉电阻?
- I2C的SCL和SDA都是开漏(Open-Drain)输出
- 开漏只能拉低,不能拉高
- 所以需要外部上拉电阻把总线拉到高电平
- 阻值通常4.7kΩ,太小会增大功耗,太大会使上升沿变慢

为什么 TOF400C 的两路测距用了不同的 I2C?

同一I2C总线上的设备地址必须唯一:
  MPU6050 默认地址: 0x68 (AD0接GND)
  VL53L1X 默认地址: 0x29 (TOF400C的核心芯片)

问题:两颗TOF400C都是0x29,挂在同一条I2C总线上会冲突!

解决方案:
  后方TOF400C → I2C2硬件外设 (PB10/PB11)
  侧方TOF400C → 软件模拟I2C (PC10/PC11)

  两条物理独立的I2C总线,地址互不干扰

FreeRTOS 的四任务架构:

优先级从高到低:

Task1: 环境感知任务 (最高优先级)
  ├─ 采集周期: MPU6050 @100Hz, TOF400C @25Hz
  ├─ 时间同步: 零阶保持插值法,25Hz→100Hz对齐
  ├─ 姿态解算: DMP引擎输出四元数→欧拉角
  └─ 判定逻辑: 跌倒检测(倾角>45° && 加速度>8g && 持续200ms)
                盲区预警(距离<1.5m && 持续3帧)
              
Task2: 无线通信任务 (次高优先级)
  ├─ M800AK 4G模组: UART1 @115200bps
  ├─ 网络心跳: 定期AT指令检测驻网状态
  ├─ MQTT发布: 传感器数据打包JSON上传
  └─ 紧急上报: 跌倒时获取GPS坐标,发送求救报文

Task3: 语音识别任务 (普通优先级)
  ├─ ASR01: UART2 @9600bps
  ├─ 中断接收: 串口空闲中断 + DMA
  └─ 指令解析: 0x01=左转, 0x02=右转, 0x05=取消

Task4: 声光控制任务 (普通优先级)
  ├─ WS2812B: TIM1_CH1(PE9) + TIM1_CH2(PE11)
  ├─ PWM+DMA: ARR=209, 800KHz, 自动搬运色彩数据
  └─ 震动马达: PA4 GPIO推挽输出

进程间通信机制:

为什么用互斥锁 + 全局结构体?

全局状态结构体:
typedef struct {
    uint8_t fall_flag;          // 跌倒标志
    uint8_t rear_distance_flag; // 后方来车标志
    uint8_t side_distance_flag; // 侧方来车标志
    uint8_t turn_cmd;          // 转向指令 (0/1/2)
    float rear_distance;       // 后方距离(mm)
    float side_distance;       // 侧方距离(mm)
    float roll, pitch, yaw;    // 欧拉角
} SystemState;

SystemState g_state;  // 全局共享内存
SemaphoreHandle_t xMutex;  // 互斥锁

写入流程(环境感知任务):
  xSemaphoreTake(xMutex, portMAX_DELAY);
  g_state.rear_distance = tof_data;
  g_state.fall_flag = check_fall();
  xSemaphoreGive(xMutex);

读取流程(声光控制任务):
  xSemaphoreTake(xMutex, portMAX_DELAY);
  uint8_t local_fall = g_state.fall_flag;
  xSemaphoreGive(xMutex);
  if(local_fall) { 执行声光告警 }

为什么不用消息队列?
- 状态数据是持续更新的(距离值每秒几十次刷新)
- 消费方只需要"最新值",不需要"历史值"
- 如果消息队列堆积未处理的历史数据,会浪费内存
- 但缺点是互斥锁持有期间所有其他任务阻塞 → 未来可优化

1.2 STM32F413 大赛架构(Keil5 C语言 + UniKnect 扩展板)

硬件拓扑框图(与 407 架构的关键差异):

┌─────────────────────────────────────────────────────────────────┐
│                 NUCLEO-F413ZH 开发板                              │
│                 STM32F413ZHT6                                    │
│                 ARM Cortex-M4 @ 100MHz ⬅ 注意主频变化            │
│                 Flash: 1.5MB  RAM: 320KB                         │
│                                                                  │
│  ┌──────────────────────────┐                                   │
│  │  ST-LINK/V2-1 调试器      │                                   │
│  │  ├─ SWD 烧录/调试        │                                   │
│  │  └─ 虚拟串口(REPL用)     │                                   │
│  └──────────────────────────┘                                   │
│                                                                  │
│  ┌──────────────────────────────────────────────────────┐       │
│  │         Arduino UNO 接口 (与 UniKnect 垂直堆叠)        │       │
│  │                                                      │       │
│  │  UART1_TX (PA9)  ←────→ EC200U 主串口 RX            │       │
│  │  UART1_RX (PA10) ←────→ EC200U 主串口 TX            │       │
│  │  I2C1_SCL (PB6)  ←────→ 板载传感器 (S502→ARDU)     │       │
│  │  I2C1_SDA (PB7)  ←────→ 板载传感器                 │       │
│  │  ADC_IN (PA0)    ←────→ 光敏电阻 GL5528            │       │
│  │  GPIO PB0        ─────→ EC200U PWRKEY 控制          │       │
│  │  GPIO PB1        ─────→ EC200U RESET 控制           │       │
│  │  5V / 3.3V / GND ←────→ 供电                        │       │
│  └──────────────────────────────────────────────────────┘       │
│                                                                  │
│  外部扩展 (通过 NUCLEO 引出的其他 144 引脚)                       │
│  ┌──────────┐  ┌──────────┐  ┌──────────────┐  ┌──────────┐    │
│  │ MPU6050  │  │ TOF400C×2│  │ WS2812B×2   │  │ ASR-PRO  │    │
│  │ I2C2     │  │ I2C3+模拟│  │ TIM1 PWM    │  │ UART2    │    │
│  └──────────┘  └──────────┘  └──────────────┘  └──────────┘    │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                 UniKnect Gen1-PRO 扩展板 (叠在下方)               │
│                                                                  │
│  ┌──────────────────────────────────────────────────────┐       │
│  │  EC200U 4G Cat.1 模组                                 │       │
│  │  ├─ LTE 通信 (上行5Mbps/下行10Mbps)                   │       │
│  │  ├─ GNSS 定位 (GPS+北斗双模)                          │       │
│  │  ├─ TTS 语音合成 (板载扬声器J402输出)                  │       │
│  │  ├─ 录音功能 (板载咪头MIC1输入)                        │       │
│  │  └─ USB虚拟串口 (J401, 调试/AT指令)                   │       │
│  └──────────────────────────────────────────────────────┘       │
│                                                                  │
│  ┌──────────────────────────────────────────────────────┐       │
│  │  板载传感器 + 切换开关                                  │       │
│  │  ├─ LIS2DH12TR 三轴加速度计                           │       │
│  │  ├─ AH20-F 温湿度传感器                               │       │
│  │  ├─ GL5528 光敏电阻 (通过ADC)                          │       │
│  │  └─ S502 开关: ARDU↔MCU / QUEC↔EC200U直连            │       │
│  └──────────────────────────────────────────────────────┘       │
│                                                                  │
│  ┌──────────────────────────────────────────────────────┐       │
│  │  SIM卡 + 天线                                          │       │
│  │  ├─ eSIM (内置贴片物联网卡) + NanoSIM卡座             │       │
│  │  ├─ S501 开关: eSIM ↔ 插拔SIM卡                      │       │
│  │  ├─ J101 主天线接口 (SMA外螺内孔)                     │       │
│  │  ├─ J102 GNSS天线接口                                │       │
│  │  └─ J103 WiFi Scan天线接口                            │       │
│  └──────────────────────────────────────────────────────┘       │
│                                                                  │
│  ┌──────────────────────────────────────────────────────┐       │
│  │  调试接口                                              │       │
│  │  ├─ J403 USB-C: CP2105双串口(Enhanced+Standard)       │       │
│  │  │   ├─ Enhanced COM: EC200U DEBUG日志输出             │       │
│  │  │   └─ Standard COM: MCU↔EC200U 串口数据监测         │       │
│  │  ├─ J401 USB-C: 模组USB口(AT命令/固件升级)            │       │
│  │  └─ DC5V电源输入                                      │       │
│  └──────────────────────────────────────────────────────┘       │
└─────────────────────────────────────────────────────────────────┘

两代架构的核心差异对照表:

维度 STM32F407 毕业设计 STM32F413 大赛方案 迁移策略
MCU F407VET6, 168MHz, 512KB Flash F413ZHT6, 100MHz, 1.5MB Flash 引脚部分兼容,HAL库API一致
4G模组 银尔达 M800AK (UART AT指令) 移远 EC200U (UART AT指令) 协议兼容,AT指令集略有差异
IMU MPU6050 (I2C, 0x68) 板载LIS2DH12TR + 可外接MPU6050 I2C驱动逻辑不变
测距 TOF400C×2 (硬件I2C+模拟I2C) TOF400C×2 (同上) 完全复用
语音 ASR01 (UART, 十六进制码) ASR-PRO (UART, 十六进制码) 协议不变,天问Block配置复用
灯带 WS2812B×2 (TIM1 PWM+DMA) WS2812B×2 (TIM1 PWM+DMA) 需重新计算ARR值(主频变了)
云平台 OneNET MQTT (M800AK) OneNET MQTT (EC200U) 物模型、三元组、Topic完全复用
音频输出 ASR01自带喇叭 EC200U TTS → 板载扬声器 ASR-PRO识别,EC200U发声
定位 M800AK内置GPS EC200U内置GNSS AT指令不同,逻辑相同

第二课时:STM32CubeMX 配置 STM32F413ZH(100MHz)

2.1 创建工程

打开 STM32CubeMX
  → File → New Project
  → 搜索 STM32F413ZH
  → 双击选择 LQFP144 封装 (这是NUCLEO-F413ZH的封装)
  → Start Project

2.2 时钟树配置:如何从外部晶振到 100MHz 系统时钟

NUCLEO-F413ZH 的外部晶振说明:

板载晶振:
  HSE (高速外部时钟): 8MHz 无源晶振 (连接 PH0/PH1)
  LSE (低速外部时钟): 32.768KHz 无源晶振 (连接 PC14/PC15)

F413 和 F407 的时钟树差异:
  F407: 8MHz → PLL × 336 / 2 / 2 = 168MHz (最大主频)
  F413: 8MHz → PLL × 100 / 2 / 2 = 100MHz (最大主频!)

注意:F413 最高主频就是 100MHz,不是 168MHz!
      这和 F407 不一样,CubeMX 不会让你配置超过 100MHz

CubeMX 时钟树配置步骤(详细操作):

第一步:Pinout & Configuration 标签页
  → System Core → RCC
  → High Speed Clock (HSE): Crystal/Ceramic Resonator
  → Low Speed Clock (LSE): Crystal/Ceramic Resonator

第二步:Clock Configuration 标签页

  时钟树配置值(从左到右填写):

  ┌─────────────────────────────────────────────────────┐
  │ HSE Input Frequency:    8 MHz                       │
  │                                                     │
  │ PLL Source Mux:         HSE                         │
  │ PLLM:                   /8    (8MHz÷8=1MHz 进PLL)   │
  │ PLLN:                   ×100  (1MHz×100=100MHz VCO) │
  │ PLLP:                   /2    (100MHz÷2=50MHz)      │
  │ PLLQ:                   不用管                       │
  │ PLLR:                   不用管                       │
  │                                                     │
  │ System Clock Mux:       PLLCLK                      │
  │ AHB Prescaler:          /1    (HCLK=100MHz)         │
  │                                                     │
  │ ┌──────┐   /8   ┌────┐  ×100  ┌────┐  /2   ┌─────┐│
  │ │8MHz  │───→───│PLLM│───→───│PLLN│──→───│PLLP ││
  │ │ HSE  │       └────┘       └────┘      └──┬──┘│
  │ └──────┘                                   │    │
  │            ┌───────────────────────────────┘    │
  │            ▼                                    │
  │      PLLCLK = 50MHz                            │
  │            │                                    │
  │      ┌─────▼─────┐                              │
  │      │  System    │  → SYSCLK = 50MHz? ❌       │
  │      │  Clock Mux │                              │
  │      └───────────┘                              │
  └─────────────────────────────────────────────────┘

  等等!50MHz 不是 100MHz?

  注意区分:
  - PLLP 输出的是 PLLCLK,这个 = 50MHz
  - 但 STM32F413 的 System Clock 可以选择不同的 PLL 输出
  - 实际上 F413 的 PLL 可以输出 100MHz 直接给 SYSCLK

  正确的 F413 时钟配置应该是:

  PLLM = 8
  PLLN = 200    ← 注意!是 200 不是 100
  PLLP = 2      → VCO=8÷8×200=200MHz, PLLCLK=200÷2=100MHz

  或者另一种常用配置:
  PLLM = 4
  PLLN = 100    → VCO=8÷4×100=200MHz
  PLLP = 2      → PLLCLK=100MHz

F413 时钟树 CubeMX 实际截图对应的值:

在 Clock Configuration 页面的输入框依次填写:

  1. HSE:             8 MHz
  2. PLL Source Mux:  HSE
  3. PLLM:            4            (8MHz ÷ 4 = 2MHz)
  4. PLLN:            100          (2MHz × 100 = 200MHz VCO)
  5. PLLP:            2            (200MHz ÷ 2 = 100MHz SYSCLK)

  6. System Clock Mux: PLLCLK
  7. AHB Prescaler:    /1          (HCLK = 100MHz)
  8. APB1 Prescaler:   /2          (APB1 = 50MHz, 因为APB1最大50MHz)
  9. APB2 Prescaler:   /1          (APB2 = 100MHz, APB2可跑满100MHz)

  10. TIM Prescaler:  APB1×2=100MHz, APB2×1=100MHz
      (当APB分频≠1时,定时器时钟=APB×2,这是STM32的时钟倍增机制)

2.3 FreeRTOS 配置

Pinout & Configuration → Middleware → FREERTOS

Interface:        CMSIS_V2
Configuration:
  → Tasks and Queues 标签:
    创建四个任务:
  
    1. Task_Environment (环境感知)
       Priority: osPriorityHigh
       Stack Size: 512 Words
     
    2. Task_Communication (无线通信)
       Priority: osPriorityAboveNormal
       Stack Size: 1024 Words
     
    3. Task_Voice (语音识别)
       Priority: osPriorityNormal
       Stack Size: 512 Words
     
    4. Task_LightSound (声光控制)
       Priority: osPriorityNormal
       Stack Size: 256 Words

  → Timers and Semaphores 标签:
    创建互斥锁:
    - Mutex_SystemState: 保护全局状态结构体
  
    创建二值信号量:
    - Sem_UART2_Rx: 语音模块串口接收完成信号

2.4 I2C 配置

Pinout & Configuration → Connectivity → I2C1

I2C1:
  Mode: I2C
  I2C Speed Mode: Fast Mode (400KHz)
  Clock Speed: 400000 Hz

  引脚映射:
  I2C1_SCL → PB6
  I2C1_SDA → PB7

I2C2 (如果接MPU6050或第二路TOF):
  Mode: I2C
  I2C Speed Mode: Fast Mode

  引脚映射:
  I2C2_SCL → PB10
  I2C2_SDA → PB11

I2C 通信原理深挖(讲给学弟学妹的):

I2C 通信的完整时序:

1. START 条件:
   SCL 高电平时,SDA 从高→低的跳变
         SDA ─┐
   SCL ─────┘────── 这是主机发出的"我要开始说话了"

2. 7位地址 + R/W位:
   主机发送 8 位:7位地址 + 1位方向
   例如 MPU6050 (地址 0x68):
    写操作: 0x68 << 1 | 0 = 0xD0
    读操作: 0x68 << 1 | 1 = 0xD1
 
   每个从机在第9个SCL时钟拉低SDA,表示"ACK,我在!"

3. 数据传输:
   每次8位,第9位是ACK
   主机发: 寄存器地址 (0x3B = ACCEL_XOUT_H)
   从机回: ACK
   主机发: 重复START
   主机发: 读地址 (0xD1)
   从机回: 数据字节1, 主机ACK
   从机回: 数据字节2, 主机NACK  ← 主机说"够了"

4. STOP 条件:
   SCL 高电平时,SDA 从低→高的跳变
         SDA ─┐
   SCL ─────┘────── 总线释放

为什么I2C最多挂127个设备?
- 地址只有7位: 2^7 = 128 个地址
- 减去 0x00(广播)和一些保留地址,实际可用约112个
- 但受限于总线电容(最大400pF),实际挂不了那么多

2.5 USART 配置

Pinout & Configuration → Connectivity → USART1
  Mode: Asynchronous
  Baud Rate: 115200 (与EC200U通信)
  Word Length: 8 Bits
  Parity: None
  Stop Bits: 1
  引脚: TX=PA9, RX=PA10

USART2 (接ASR-PRO语音模块):
  Mode: Asynchronous
  Baud Rate: 9600 (ASR01/ASR-PRO 默认)
  Word Length: 8 Bits
  Parity: None
  Stop Bits: 1
  引脚: TX=PA2, RX=PA3

USART3 (备用调试串口):
  Mode: Asynchronous
  Baud Rate: 115200

USART 名称含义深挖:

USART vs UART 的区别?

UART: Universal Asynchronous Receiver/Transmitter
      通用异步收发器
      - 只能异步通信(没有时钟线)
      - 双方约定相同的波特率
    
USART: Universal Synchronous/Asynchronous Receiver/Transmitter
       通用同步/异步收发器
       - 既可以异步(和UART一样)
       - 也可以同步(有时钟线CK输出)
       - STM32的串口都叫USART,但通常我们只用异步模式

为什么 STM32F413 的手册上写的是 USART?
- STM32 的所有串口硬件都支持同步模式
- 同步模式需要额外的 CK 引脚(时钟线)
- 但我们项目中不需要,所以只配置 TX 和 RX
- CubeMX 中选择 "Asynchronous" 模式即可

异步通信的关键参数:
  波特率(Baud Rate): 每秒传输的码元数
    9600bps  = 每秒9600个bit = 约960字节/秒(含起始位和停止位)
    115200bps = 每秒115200个bit = 约11.5KB/秒
  
  帧格式:
    [起始位(1bit)] [数据位(8bit)] [校验位(0/1bit)] [停止位(1bit)]
    总共10位传输1字节,所以实际有效速率 = 波特率 ÷ 10
  
  例如115200bps → 实际约11.5KB/s

全双工 vs 半双工:

  全双工(Full Duplex):
    TX和RX是两条独立的物理线
    MCU可以同时发送和接收
    例: USART的PA9(TX)和PA10(RX)同时工作
  
  ┌─────┐  TX ────────────→ RX  ┌─────┐
  │ MCU │                        │模组 │
  │     │  RX ←──────────── TX  │     │
  └─────┘                        └─────┘

  半双工(Half Duplex):
    只有一条数据线,发送和接收不能同时进行
    例: RS485、I2C的SDA(虽然是I2C不是UART)
  
  ┌─────┐        数据线        ┌─────┐
  │ MCU │ ←────────────────→ │模组 │
  │     │   同一时刻只能单向    │     │
  └─────┘                      └─────┘

UART 通信没有 CLK 线,如何保证时序正确?

  同步通信(如SPI): 
    CLK线由主机发出,双方按照时钟边沿采样
    ┌─┐ ┌─┐ ┌─┐ ┌─┐
    │1│0│1│1│0│0│1│0│  ← 用CLK来"同步节拍"
  
  异步通信(如UART): 
    没有CLK线,双方"约定"波特率
    - 接收方用自己内部的16倍或8倍过采样时钟
    - 检测到起始位的下降沿后,开始按自己的时钟采样
    - 如果双方波特率误差太大(>3%),就会出错
  
    过采样原理:
    接收方以 16×波特率 的频率采样
    每个bit持续期间采样第8/9/10次,取多数值
    这样即使有微小误差也能正确接收

2.6 PWM 配置(WS2812B 的关键)

为什么 WS2812B 对时序要求极其严格?

WS2812B 的通信协议是单线归零码(NRZ),800KHz:

逻辑 "0": ┌──┐┌──────────────────────┐
          │  ││                      │
          └──┘└──────────────────────┘
          ←0.4μs→←──────0.85μs───────→
          高电平   低电平
        
逻辑 "1": ┌──────┐┌───────┐
          │      ││       │
          └──────┘└───────┘
          ←0.8μs→←0.45μs→
          高电平   低电平

关键参数:
  周期 T = 1/800KHz = 1.25μs
  逻辑0高电平: 0.4μs → 占空比 = 0.4/1.25 = 32%
  逻辑1高电平: 0.8μs → 占空比 = 0.8/1.25 = 64%

为什么不用普通GPIO翻转?
  - STM32在100MHz主频下,一条指令约10ns
  - 0.4μs = 400ns = 40条指令
  - 用延迟函数很难精确控制,而且中断会打断时序
  - LED灯会随机闪烁、颜色错乱

所以必须用硬件PWM+DMA!

CubeMX 配置 PWM(STM32F413 @ 100MHz):

Pinout & Configuration → Timers → TIM1

TIM1 配置:
  Clock Source: Internal Clock
  Channel1: PWM Generation CH1 (接左侧WS2812B → PE9)
  Channel2: PWM Generation CH2 (接右侧WS2812B → PE11)

  Configuration → Parameter Settings:
    Prescaler:         0        (不分频,输入100MHz)
    Counter Mode:      Up
    Counter Period:    124      (ARR = 124)
  
    计算过程:
      PWM频率 = 定时器时钟 ÷ (Prescaler+1) ÷ (ARR+1)
      800KHz   = 100MHz    ÷ 1            ÷ (ARR+1)
      ARR+1    = 100MHz ÷ 800KHz = 125
      ARR      = 124
  
    逻辑0比较值: 124 × 32% ≈ 40
    逻辑1比较值: 124 × 64% ≈ 80

  DMA Settings:
    点击 Add → 选择 TIM1_CH1
    Direction: Memory to Peripheral
    Mode: Circular
    Data Width: Half Word (16位,一次传输一个颜色分量)
  
    TIM1_CH2 同样配置 DMA

WS2812B 的色彩数据格式:

每颗灯珠需要24位数据(G-R-B顺序):
  G7 G6 G5 G4 G3 G2 G1 G0 | R7 R6 R5 R4 R3 R2 R1 R0 | B7 B6 B5 B4 B3 B2 B1 B0

例如,黄色(R=255, G=255, B=0)的数据:
  G: 0xFF = 11111111
  R: 0xFF = 11111111
  B: 0x00 = 00000000
  组合: 11111111 11111111 00000000 = 0xFF, 0xFF, 0x00

复位码:
  发送完所有灯珠数据后,数据线保持低电平 > 50μs
  灯珠内部锁存数据,新颜色生效

第三课时:编译原理与烧录技术

3.1 从源码到固件:编译的四个阶段

main.c  →  .o  →  .elf/.axf  →  .hex/.bin

阶段1: 预处理 (Preprocessing)
  编译器: armcc -E
  输入:  .c / .h 文件
  输出:  .i 文件 (预处理后的源码)

  做了什么:
  - #include 头文件展开(把头文件内容复制过来)
  - #define 宏替换
  - #ifdef / #ifndef 条件编译
  - 删除注释

  示例:
  原始代码:
    #define LED_ON()  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET)
    LED_ON();

  预处理后:
    HAL_GPIO_WritePin(GPIOB, ((uint16_t)0x0000), ((uint32_t)0x0001));

阶段2: 编译 (Compilation)
  编译器: armcc
  输入:  .i 文件
  输出:  .s 文件 (汇编代码)

  做了什么:
  - 词法分析、语法分析、语义分析
  - 生成中间代码
  - 优化
  - 生成目标架构的汇编代码

  示例: C → ARM汇编
    int a = b + 5;
    ↓
    LDR  R0, [SP, #4]   ; 从栈加载b
    ADD  R0, R0, #5     ; R0 = b + 5
    STR  R0, [SP, #0]   ; 存到a的位置

阶段3: 汇编 (Assembly)
  汇编器: armasm
  输入:  .s 文件
  输出:  .o 文件 (目标文件,机器码)

  做了什么:
  - 把汇编指令翻译成二进制机器码
  - 生成符号表(记录函数名、变量名的地址占位符)

  .o 文件内容:
    包含: 机器码 + 重定位信息 + 符号表
    不能直接运行!因为地址还没最终确定

阶段4: 链接 (Linking)
  链接器: armlink
  输入:  多个 .o 文件 + 库文件(.lib) + 链接脚本(.sct)
  输出:  .elf / .axf 文件 (可执行文件)

  做了什么:
  - 把所有 .o 文件合并
  - 解决符号引用(把函数调用指向实际地址)
  - 按照链接脚本分配内存地址
  - 添加启动代码、中断向量表

  .elf/.axf 文件:
    ELF = Executable and Linkable Format
    AXF = ARM eXecutable Format
    包含: 代码 + 数据 + 调试信息 + 符号表

最终转换:
  fromelf.exe:
    .axf → .hex (Intel HEX格式,含地址信息)
    .axf → .bin (纯二进制镜像,无地址信息)

各文件格式的区别:

文件格式 内容 大小 用途
.o 单个源文件的机器码+符号表 编译中间产物
.s ARM汇编源码 文本 调试、优化时查看
.axf / .elf 完整可执行文件(含调试信息) 最大 Keil仿真调试用
.hex Intel HEX格式(ASCII文本,含地址) 中等 烧录(通用性最好)
.bin 纯二进制镜像(无地址) 最小 烧录(需指定起始地址)

c语言和lua、python的区别

3.2 烧录是什么?为什么叫"烧录"?

"烧录"的由来:
  - 早期ROM(只读存储器)用熔丝技术
  - 用高压电流"烧断"特定熔丝,写入数据
  - 这个过程不可逆,像"烧"进去一样

  现代Flash的写入原理:
  - 浮栅晶体管(Floating Gate MOSFET)
  - 用较高的电压(10-12V)给浮栅注入/擦除电子
  - 电子被"困"在浮栅中,改变阈值电压
  - 0和1通过浮栅电荷量来区分
  - 可以反复擦写,但还是习惯叫"烧录"

3.3 三种烧录/调试接口

SWD (Serial Wire Debug) ← ST-Link 使用
  ┌──────────────────────────────────────┐
  │ 只需要2根信号线!                     │
  │                                      │
  │ SWDIO  ←→ 双向数据线                 │
  │ SWCLK  →   时钟线(Master发出)        │
  │ GND    ──  共地                     │
  │ VCC    →   可选(目标板自供电就不接)   │
  │                                      │
  │ 优点: 引脚少,抗干扰强,速率快        │
  │ 缺点: 需要专用调试器                 │
  │                                      │
  │ NUCLEO-F413ZH 板载 ST-LINK/V2-1      │
  │ 自动通过 SWD 连接片上 STM32F413       │
  └──────────────────────────────────────┘

JTAG (Joint Test Action Group) ← J-Link 使用
  ┌──────────────────────────────────────┐
  │ 需要4-5根信号线:                     │
  │                                      │
  │ TDI  →  测试数据输入                  │
  │ TDO  ←  测试数据输出                  │
  │ TCK  →  测试时钟                      │
  │ TMS  →  测试模式选择                  │
  │ TRST →  测试复位(可选)                │
  │                                      │
  │ 优点: 标准接口,功能强大              │
  │ 缺点: 引脚多,占用PCB面积             │
  └──────────────────────────────────────┘

ISP (In-System Programming) ← 串口烧录
  ┌──────────────────────────────────────┐
  │ 通过芯片内置的Bootloader烧录:        │
  │                                      │
  │ 需要: UART1 (TX/RX) + BOOT0引脚      │
  │                                      │
  │ 步骤:                                │
  │ 1. BOOT0 接高电平(拉高到3.3V)         │
  │ 2. 复位芯片                          │
  │ 3. 芯片进入System Memory的Bootloader  │
  │ 4. 通过UART1下载程序到Flash           │
  │ 5. BOOT0 接低电平,复位,运行新程序    │
  │                                      │
  │ 优点: 不需要专用调试器,串口即可      │
  │ 缺点: 不能在线调试,速度慢            │
  └──────────────────────────────────────┘

为什么 NUCLEO 板上有两个芯片?(F413 和 F103)

NUCLEO-F413ZH 板上的两个芯片:

1. STM32F413ZHT6 (目标芯片,144引脚大芯片)
   - 你的程序运行在这里
   - 通过 SWD 连接到 ST-LINK

2. STM32F103CBT6 (ST-LINK/V2-1 调试器芯片)
   - 出厂预烧了 ST-LINK 固件
   - 负责: USB↔SWD 协议转换
   - 提供虚拟串口 (VCP) 功能
   - 用户LED (LD1) 由它控制
 
工作流程:
  PC USB ──→ F103 (ST-LINK) ──SWD──→ F413 (你的程序)
              │
              └── 虚拟串口 ──UART──→ F413 (printf输出)

第四课时:串口调试与数据显示

4.1 SSCOM/XCOM 使用技巧

为什么 0x01 显示为方框?

ASCII 码表的关键区间:

可打印字符:  0x20(空格) ~ 0x7E(~)
控制字符:    0x00 ~ 0x1F

0x01 是 SOH (Start of Heading) 控制字符
在 ASCII 表中是不可打印的!

SSCOM 中显示:
  HEX模式:   显示 "01"
  字符串模式: 显示 "□"(方框) 或者什么都不显示

所以调试时:
  ASR-PRO 返回 0x01 → SSCOM需要切换到 "HEX显示" 模式
  或者在你的代码里把接收到的 0x01 转成人类可读的字符串

SSCOM 同时监测多个串口的技巧:

方法1: 复制多份 SSCOM.exe 到不同文件夹
  C:\Tools\SSCOM_COM3\SSCOM.exe  → 打开 COM3 (ASR-PRO)
  C:\Tools\SSCOM_COM5\SSCOM.exe  → 打开 COM5 (EC200U监测)

方法2: 用 XCOM 替代
  XCOM 支持多实例,直接打开两次即可

方法3: 用 MobaXterm
  多个标签页,每个打开一个串口

第五课时:工程思维培养——混合音频架构设计

5.1 问题分析:为什么不能简单"借用"EC200U 的麦克风?

EC200U 的 MIC 输入电路(简化):
                         VCC
                          │
                     ┌────┴────┐
                     │  偏置电阻 │
                     │  2.2kΩ  │
                     └────┬────┘
                          │
              ┌───────────┼───────────┐
              │           │           │
         ┌────▼────┐     ├──→ EC200U_MIC_P
         │  咪头    │     │
         │ (MIC1)  │     ├──→ EC200U_MIC_N (或GND)
         └─────────┘     │
              │           │
             GND    ┌────▼────┐
                    │ 隔直电容 │
                    │  100nF   │
                    └────┬────┘
                         │
                     EC200U 内部ADC

关键问题:
1. 咪头是模拟信号,不能直接"分线"给两个芯片
2. EC200U 内部有偏置电压,会干扰 ASR-PRO 的 MIC 输入
3. ASR-PRO 如果也加偏置,两个偏置电压打架,咪头没法正常工作
4. 即使断开连接,ASR-PRO 核心板自己的 MIC 引脚也没有偏置电路,
   需要自己搭运放电路,比买一个开发板还贵

5.2 最终方案:一盔两 MIC,一喇叭

┌─────────────────────────────────────────────────────────────┐
│                    智能骑行头盔 音频架构                       │
│                                                             │
│  ┌──────────────────────┐    ┌──────────────────────┐      │
│  │  ASR-PRO 开发板       │    │  UniKnect 扩展板      │      │
│  │  (带板载MIC咪头)      │    │                      │      │
│  │                      │    │  MIC1 ──→ EC200U     │      │
│  │  MIC ──→ ASR-PRO芯片  │    │  板载咪头    (TTS+录音)│      │
│  │  语音识别输入         │    │                      │      │
│  │                      │    │  J402 ←── EC200U     │      │
│  │  (SPK不接喇叭!)      │    │  扬声器接口  (唯一喇叭)│      │
│  │                      │    │              │        │      │
│  │  UART ──→ STM32     │    │  UART ←──→ STM32    │      │
│  └──────────────────────┘    └──────────┬───────────┘      │
│                                         │                   │
│                                    ┌────▼────┐              │
│                                    │ J402接口 │              │
│                                    │ 接8Ω喇叭 │              │
│                                    │ (唯一喇叭)│              │
│                                    └─────────┘              │
└─────────────────────────────────────────────────────────────┘

两个MIC的职责:
  ASR-PRO的MIC → 离线语音识别 (骑行者说话→识别指令)
  EC200U的MIC  → 录音功能 (紧急情况时双向通话用)

一个喇叭的职责:
  EC200U J402 → TTS播报 + 通话音频
  (ASR-PRO不接喇叭,识别结果通过UART告诉STM32,
   STM32通过EC200U的TTS驱动统一喇叭发声)

5.3 软件联动流程

完整交互序列:骑行者说"左转左转"

时间轴 →

t0: 骑行者说出"左转左转"
    │
    ▼
t1: ASR-PRO 麦克风拾音
    │
    ▼
t2: ASR-PRO 离线识别引擎匹配"左转左转"
    │
    ▼  (约100ms)
t3: ASR-PRO 通过 UART TX 发出 0x01
    │
    ▼
t4: STM32 USART2 中断接收 0x01
    │
    ├─────────────────────────────┐
    │                             │
    ▼                             ▼
t5: TIM1 PWM+DMA               USART1 发送AT指令
    左侧灯带黄色闪烁            AT+QTTS=2,"正在左转,请注意安全"
    (500ms单次,持续5秒)
    │                             │
    │                             ▼
    │                         t6: EC200U 收到AT指令
    │                             调用内部TTS引擎
    │                             │
    │                             ▼
    │                         t7: EC200U DAC → 功放 → J402
    │                             喇叭播放"正在左转请注意安全"
    │                             │
    ▼                             ▼
t8: 5秒后灯带熄灭              TTS播放完毕

总延迟:t1→t7 ≈ 200ms,骑行者几乎感觉不到

5.4 为什么这个方案是"工程思维"的体现?

工程思维的核心:不重复造轮子,利用现有资源的最优组合

资源盘点:
  ✅ EC200U 自带 TTS 引擎 (移远已经开发好,稳定可靠)
  ✅ EC200U 自带音频功放 (无需外接)
  ✅ EC200U 有 Audio API (支持TTS/录音/播放)
  ✅ ASR-PRO 有最好的离线语音识别能力
  ✅ UniKnect 有 J402 扬声器接口
  ✅ UniKnect 有 MIC1 咪头

最优组合决策:
  1. 语音输入: ASR-PRO 的 MIC (专做识别,精度高)
  2. 语音输出: EC200U TTS → J402喇叭 (统一出声,音质好)
  3. ASR-PRO 不接喇叭 (避免两个声源互相干扰)
  4. EC200U MIC 预留 (未来紧急通话用)

成本分析:
  ASR-PRO 开发板: 约35元
  ASR-PRO 核心板: 约25元
  差异: 10元 = 一套完整的MIC+偏置电路+USB下载电路

  如果买核心板再自己搭音频电路:
    - MIC咪头: 2元
    - 运放芯片: 3元
    - 电阻电容: 2元
    - 洞洞板+焊接时间: 无价
    总计: 远超过10元 + 至少半天时间

教训:
  硬件设计中,"省10元"往往会导致"多花两天"
  工程思维就是要算"总拥有成本"而不是只看BOM价格

第六课时:嘉立创EDA实操要点

6.1 核心操作流程

1. 创建工程 → 选择"专业版"
2. 原理图:
   - 与立创商城联动: 搜索框输入型号 → 自动找到元器件
   - 网络编号: 同名网络自动连通 (VCC_3.3V, GND, I2C1_SCL等)
   - Shift分隔: 画线时按住 Shift → 可以转角分段走线
   - 自定义元件: 库 → 新建元件 → 画封装 → 关联符号

3. 转PCB:
   - 设计 → 转PCB
   - 布局: 先放主控,再放模块,最后电源
   - 布线: 信号线 10mil, 电源线 20-30mil
   - 铺铜: 顶层和底层都铺 GND 铜皮

第七课时:云平台配置

7.1 MQTT 协议简介

MQTT = Message Queuing Telemetry Transport
      消息队列遥测传输协议

核心概念:发布/订阅模式 (Pub/Sub)

┌─────────┐     发布 Topic: "helmet/fall"     ┌─────────┐
│  头盔端  │ ─────────────────────────────────→ │  Broker  │
│(Publisher)│  Payload: {"fall":1,"lat":34.3,   │ (OneNET) │
└─────────┘             "lon":108.9}            └────┬────┘
                                                     │
                                          订阅 Topic: "helmet/fall"
                                                     │
                                                     ▼
                                              ┌──────────┐
                                              │  微信小程序│
                                              │(Subscriber)│
                                              └──────────┘

MQTT 的 QoS (服务质量):
  QoS 0: 至多一次 (发完不管)
  QoS 1: 至少一次 (确认收到,可能重复)
  QoS 2: 恰好一次 (四次握手,保证不重不漏)

本项目用 QoS 1:跌倒报警必须确保送达,重复了前端可去重

7.2 OneNET 物模型配置

物模型 JSON 定义 (示例):

{
  "properties": [
    {
      "identifier": "rear_distance",
      "name": "后方距离",
      "dataType": "float",
      "unit": "mm",
      "accessMode": "r"       // 只读,设备上报
    },
    {
      "identifier": "fall_alarm",
      "name": "跌倒警报",
      "dataType": "bool",
      "accessMode": "r"       // 只读,设备上报
    },
    {
      "identifier": "turn_cmd",
      "name": "转向命令",
      "dataType": "int",
      "accessMode": "rw"      // 读写,小程序可下发
    }
  ]
}

三元组:设备的唯一身份
  ProductKey:  产品标识 (同一类设备共享)
  DeviceName:  设备名称 (每台设备唯一)
  DeviceSecret: 设备密钥 (鉴权用)

设备连接时需要:
  MQTT ClientID = ProductKey + DeviceName
  MQTT Username = ProductKey
  MQTT Password = HMAC_SHA1(DeviceSecret, 时间戳等)

第八课时:Git 仓库协作

8.1 仓库平台选择

根据你"不用梯子"的要求,推荐 Gitee(码云)

特性 Gitee GitHub(无梯子) GitCode
国内访问速度 ⭐⭐⭐⭐⭐ 快 ❌ 极慢/不可用 ⭐⭐⭐⭐ 快
免费私有仓库 ✅ 5人内免费 ✅ 但无法访问
上传速度 不可用
团队协作 ✅ Issues/PR/Wiki ❌ 无法用

8.2 仓库结构建议

smart-helmet/
├── docs/                    # 文档
│   ├── 论文_王浩丞.pdf
│   ├── 硬件设计/
│   │   ├── 电路图.pdf
│   │   └── 引脚分配表.xlsx
│   └── 会议纪要/
├── firmware_f407/           # 毕业设计代码(FreeRTOS C)
│   ├── Core/
│   ├── Drivers/
│   └── README.md
├── firmware_f413/           # 大赛移植代码(C)
│   ├── Core/
│   ├── Drivers/
│   └── README.md
├── hardware/                # 嘉立创EDA工程
│   └── smart_helmet.epro
├── tools/                   # 工具配置
│   └── asr_project.hd       # 天问Block工程
└── README.md                # 项目总览和快速开始

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐