副板固件实现(STM32F103C8 + stm32f1xx-hal)
副板(Blue Pill,STM32F103C8)负责接收主板(ESP32-S3)通过 I2C 发来的电机控制指令,驱动一个步进电机(调节靠背角度)和两个直流电机(驱动轮子)。
硬件接线
| 功能 | 引脚 | 说明 |
|---|---|---|
| I2C1 SCL | PB6 | 接 ESP32-S3 ,需 1kΩ 上拉至 3.3V |
| I2C1 SDA | PB7 | 接 ESP32-S3 ,需 1kΩ 上拉至 3.3V |
| 步进电机 IN1~IN4 | PA0~PA3 | ULN2003 驱动板 |
| 左轮 AIN1 / AIN2 | PB14 / PB15 | TB6612 Motor A |
| 右轮 BIN1 / BIN2 | PB13 / PB12 | TB6612 Motor B |
| 状态 LED | PC13 | 板载 LED(低电平亮) |
上拉电阻
I2C 总线必须有上拉电阻。没有 4.7kΩ 时用 1kΩ 也可以,100kHz 标准模式下完全正常,功耗略高(3.3mA/line)。
通信协议
副板 I2C 地址:0x42
ESP32-S3 侧向此地址发送指令帧。
帧格式(6 字节):
S T flag cs E Dflag:电机控制字节,bit[7:6] 步进、bit[5:4] 左轮、bit[3:2] 右轮,每组10=正转、01=反转、00/11=停止cs:校验字节,cs = b'S' ^ b'T' ^ flag- 无回响:副板校验失败时停止所有电机,I2C 层 ACK/NACK 已足够确认在线状态
flag 位定义:
| bit[7:6] | bit[5:4] | bit[3:2] | bit[1:0] |
|---|---|---|---|
| 步进电机 | 左轮 | 右轮 | 保留(置0) |
每组编码:10 = 正转,01 = 反转,00/11 = 停止
ESP32-S3 侧发送示例:
fn frame(flag: u8) -> [u8; 6] {
[b'S', b'T', flag, b'S' ^ b'T' ^ flag, b'E', b'D']
}
// 步进正转 + 左右轮前进
let flag: u8 = (0b10 << 6) | (0b10 << 4) | (0b10 << 2);
i2c.write(0x42u8, &frame(flag)).unwrap();固件架构
src/
├── main.rs # 初始化 + 主循环
├── protocol.rs # I2C slave 接收 + 协议解析
└── motor/
├── mod.rs # Motors 聚合 + MotorCommand
├── uln2003.rs # 步进电机(4相全步)
└── tb6612.rs # 直流电机 H 桥主循环:
loop {
match protocol.next_command() {
Ok(cmd) => motors.apply(&cmd),
Err(()) => motors.stop_all(), // 校验失败立即停机
}
}I2C Slave 实现
HAL 只提供 master 模式,slave 直接操作 PAC 寄存器。
初始化(PCLK1 = 32 MHz):
rcc.apb1enr().modify(|_, w| w.i2c1en().set_bit());
rcc.apb1rstr().modify(|_, w| w.i2c1rst().set_bit());
rcc.apb1rstr().modify(|_, w| w.i2c1rst().clear_bit());
i2c.cr2().write(|w| unsafe { w.freq().bits(32) });
i2c.oar1().write(|w| unsafe { w.bits((0x42u16) << 1) }); // OAR1[7:1] = 0x42
i2c.cr1().write(|w| w.pe().set_bit().ack().set_bit());接收流程:
电机驱动
步进电机(ULN2003 + 28BYJ-48)
4 相全步序列:
| 步 | IN1 | IN2 | IN3 | IN4 |
|---|---|---|---|---|
| 0 | 1 | 1 | 0 | 0 |
| 1 | 0 | 1 | 1 | 0 |
| 2 | 0 | 0 | 1 | 1 |
| 3 | 1 | 0 | 0 | 1 |
Stop 指令直接断电(release())。28BYJ-48 内部蜗轮蜗杆减速比 64:1,断电后靠背不会自行滑动。
直流电机(TB6612)
| 状态 | IN1 | IN2 |
|---|---|---|
| 正转 | H | L |
| 反转 | L | H |
| 滑行 | L | L |
Stop 指令调用 coast()(两路低电平,自由滑行)。
踩坑记录
HAL Serial 的 USART1 时钟问题(已弃用 UART 方案)
原方案使用 UART,stm32f1xx-hal 0.11 的 Serial::new() 通过 bit-band 操作使能 USART1 时钟,在部分 Blue Pill 上实测 APB2ENR bit14 不会被置位(OpenOCD 读出 APB2ENR = 0x401C)。TX 完全没有信号输出。后改为直接操作 PAC 寄存器可解决,但主板 USART 资源耗尽,最终整体切换到 I2C。
I2C OAR1 地址位
STM32F1 的 OAR1 寄存器,7-bit 地址存放在 bit[7:1],写入时需左移一位:
i2c.oar1().write(|w| unsafe { w.bits((SLAVE_ADDR as u16) << 1) });STOPF 清除序列
STOPF 标志必须先读 SR1 再写 CR1,否则标志不会清除,下一帧的 ADDR 检测会卡死。
repeated START 与 STOPF 的冲突
write_read 在写完后发 repeated START 而非 STOP,STM32 slave 等待 STOPF 会永远卡住。解决方案:去掉回响,改为单向 write 事务,协议更简单,也不存在此问题。
