STM32 智能小车代码说明文档

本文档根据当前 k.txt 生成,说明程序的整体结构、主要模块、数据流和关键函数。内容面向第一次接触嵌入式和单片机项目的读者,会先解释必要概念,再说明代码如何控制小车。

源文件:k.txt 主控:STM32 联网:ESP8266 + Blynk 显示:SSD1306 OLED 功能:遥控、避障、密码、定位

目录

1. 项目概览

这份程序控制一辆基于 STM32 的智能小车。小车可以通过手机端 Blynk 远程控制,也可以通过本地按键控制;同时,它还包含 OLED 显示、密码验证、继电器触发、超声波避障、定位数据读取与上传等功能。

远程控制

ESP8266 连接 WiFi 后接入 Blynk,手机端通过 V1 到 V5 控制小车运动和避障模式。

本地控制

两个模拟按键输入口分别负责密码输入和方向控制。按键通过不同电压范围区分不同功能。

密码与继电器

默认密码为 1324。密码正确时,继电器引脚短时间动作,可用于开锁或触发外部设备。

避障与定位

超声波模块检测前方障碍物,定位模块通过串口返回坐标信息,程序再把坐标上传到 Blynk。

程序的核心思路

程序上电后先执行 setup(),初始化串口、OLED、WiFi、Blynk 和各个引脚。之后一直执行 loop(),不断处理定位数据、扫描按键、刷新 OLED、运行 Blynk 网络服务,并在避障模式开启时执行自动避障。

可以把这套程序理解成一个“小型控制系统”:STM32 不断观察外部输入,例如手机按钮、实体按键、超声波距离和定位模块返回值;然后根据这些输入改变输出,例如电机转动、OLED 显示、蜂鸣器提示、继电器动作和 Blynk 数据上传。

2. 嵌入式基础概念

如果之前没有接触过嵌入式,可以先把嵌入式系统理解成“运行在具体硬件上的程序”。普通电脑程序主要处理文件、窗口、网络请求;嵌入式程序更关注硬件本身,例如某个引脚是高电平还是低电平、电机是否转动、传感器有没有返回数据。

单片机 / STM32

STM32 是一类单片机,也就是小型控制芯片。它内部有 CPU、内存、定时器、串口、ADC 等外设资源,可以直接控制引脚,也可以和其他模块通信。

引脚 / GPIO

引脚是单片机和外部世界连接的“接口”。GPIO 是通用输入输出口,可以被配置成输入读取按键,也可以被配置成输出控制蜂鸣器、继电器或电机驱动板。

高低电平

HIGHLOW 表示引脚输出的两种电平状态。简单理解就是开关的开和关,但具体高电平触发还是低电平触发,要看外接模块电路。

PWM

PWM 是一种快速开关信号。电机速度控制常用 PWM:不是输出一个真正连续变化的电压,而是用很快的开关比例来模拟“强一点”或“弱一点”的驱动力。

ADC

ADC 用来把模拟电压转换成数字值。代码中的 analogRead() 就是在读取电压大小,然后用数值范围判断按下的是哪个键。

串口 UART

串口是一种常见通信方式,一次传一串字符或字节。本项目里 ESP8266 使用 Serial3,定位模块使用 Serial2,电脑调试使用 Serial

I2C

I2C 是另一种常见通信总线,通常只需要两根信号线。这里 OLED 屏幕通过 I2C 与 STM32 通信,代码中用 U8g2 库完成底层操作。

主循环

嵌入式程序通常不会执行完就退出,而是在主循环里一直运行。它每一轮都检查输入、更新状态、控制输出,这就是 loop() 的作用。

读这份代码时可以抓住三件事

第一,看输入从哪里来:手机 Blynk、两个按键、超声波模块、定位模块。第二,看程序保存了哪些状态:例如 set_modekey_strmyWiFi_state。第三,看输出控制了什么:电机、OLED、蜂鸣器、继电器和 Blynk 虚拟引脚。只要沿着“输入、状态、输出”这条线看,代码会清楚很多。

3. 系统组成

这个项目由多个模块共同完成。STM32 是主控,负责做判断和控制;ESP8266 负责联网;OLED 负责显示;超声波模块负责测距;电机驱动模块负责让车轮运动;定位模块通过串口提供位置数据。

对没有嵌入式经验的读者来说,可以把 STM32 看成整辆小车的“大脑”,但它本身不能直接完成所有事情。它需要通过不同接口和外部模块配合:有的模块负责提供信息,例如按键、超声波、定位模块;有的模块负责执行动作,例如电机驱动、蜂鸣器、继电器和 OLED 显示屏。

手机端通过 Blynk 发送虚拟引脚命令。
ESP8266连接 WiFi,并通过 Serial3 与 STM32 通信。
STM32读取按键和传感器,执行控制逻辑。
电机驱动接收方向电平和 PWM 信号,驱动左右电机。
OLED通过 I2C 显示状态和密码输入信息。
定位模块通过 Serial2 返回经纬度相关字符串。

主要数据流

手机 Blynk 控件
  -> ESP8266
  -> STM32 BLYNK_WRITE 回调
  -> 电机控制函数或模式变量 set_mode

按键输入
  -> analogRead 读取电压
  -> 判断按键编号
  -> 输入密码或直接控制电机

定位模块
  -> Serial2 返回字符串
  -> 程序按逗号截取坐标字段
  -> Blynk.virtualWrite 上传到 V11/V12

这几条数据流说明了项目的基本逻辑:外部输入先进入 STM32,STM32 根据代码做判断,再控制外部设备。远程按钮、本地按键和传感器数据虽然来源不同,但最后都会变成程序里的函数调用或变量变化。

4. 引脚与外设 源代码第 1 到 10 行,第 425 到 426 行

代码开头使用 #define 给硬件引脚起了易读的名字,后续控制外设时直接使用这些名字。

引脚名里的 PA6PB0 代表 STM32 的端口和编号。字母 A、B 表示不同端口组,后面的数字表示该端口组里的具体引脚。代码中把这些硬件编号重新命名为 left_dirkey_1 这样的业务名称,是为了让读代码的人一眼知道它连接了什么。

输入引脚和输出引脚

输入引脚用于“读外部状态”,例如按键是否按下、超声波有没有回波。输出引脚用于“控制外部设备”,例如让蜂鸣器响、让继电器动作、让电机驱动板改变方向。代码在 setup() 中用 pinMode() 指定每个引脚的工作方向。

名称 引脚 连接对象 作用
left_dir PA6 左电机方向端 输出高低电平,控制左电机方向。
left_speed PA0 左电机速度端 输出 PWM 信号,控制左电机速度。
right_speed PA1 右电机速度端 输出 PWM 信号,控制右电机速度。
right_dir PA7 右电机方向端 输出高低电平,控制右电机方向。
key_1 PA4 密码输入按键 通过 ADC 数值判断输入的是 1、2、3、4。
key_2 PA5 本地方向按键 通过 ADC 数值判断前进、后退、左转、右转。
MYBEEP PB5 蜂鸣器 Blynk 断开或连接时用于状态提示。
rely_pin PB0 继电器 密码正确时短暂触发。
PB8 PB8 超声波 Trig 向超声波模块发送触发脉冲。
PB9 PB9 超声波 Echo 接收超声波回响信号。
PWM 说明:analogWrite() 输出的是 PWM,占空比越高不一定在所有驱动板上都表示越快,具体还要看电机驱动模块的接线和有效电平。

5. 依赖库与对象 源代码第 11 到 14 行,第 22 行,第 36 行

嵌入式开发经常会使用现成库。库的作用是把复杂的底层通信细节封装起来,让代码可以用较简单的函数操作模块。例如 OLED 实际需要按照显示芯片协议发送很多字节,但使用 U8g2 后,程序只需要设置字体、坐标并调用 print()

代码 用途
ESP8266_Lib.h 让 STM32 可以通过串口控制 ESP8266 模块。
BlynkSimpleShieldEsp8266.h 把 ESP8266 和 Blynk 平台连接起来,支持手机远程控制。
U8g2lib.h 驱动 SSD1306 OLED,负责文字显示和页面刷新。
Wire.h I2C 通信库,OLED 使用 I2C 总线通信。

ESP8266 对象

ESP8266 wifi(&Serial3);

这行表示 ESP8266 模块接在 Serial3 上。程序后面通过这个对象配置 WiFi 模式,并交给 Blynk 使用。

这里的 &Serial3 可以理解为告诉库:“请通过 STM32 的第三个串口去找 ESP8266”。如果硬件接线换到了别的串口,这里也需要对应修改。

OLED 对象

U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

这行创建了一个 SSD1306、128x64 分辨率、硬件 I2C 的 OLED 显示对象。所有 OLED 显示都通过 u8g2 完成。

128x64 表示屏幕有 128 列、64 行像素。代码里用 setCursor(x, y) 指定文字从哪个位置开始显示,其中 x 是横向位置,y 是纵向位置。

未使用的 playlist

第 16 行定义了一个 13 字节数组 playlist,但当前程序没有使用它。它可能是某个串口模块的指令帧,也可能是之前功能留下的代码。

6. 全局变量 源代码第 18 到 35 行

全局变量用于保存整个程序都会用到的状态。比如 WiFi 是否连接、当前控制模式、输入中的密码、超声波距离、定位字符串等。

普通程序也会有变量,但嵌入式程序里的变量经常代表“设备当前状态”。例如 set_mode 不是一个普通数字,它决定小车现在是手动控制还是自动避障;key_str 不是随便保存字符串,它记录用户已经输入了几位密码。

变量 含义 主要用途
authssidpass Blynk 授权信息和 WiFi 配置。 传给 Blynk.begin() 建立连接。文档中不展开具体值。
myWiFi_state 记录 Blynk 是否连接成功。 决定是否运行 Blynk.run(),以及是否上传数据。
vpin_value Blynk 虚拟引脚传来的数值。 远程控制按钮按下或松开时使用。
set_mode 当前模式。1 表示普通控制,2 表示避障模式。 由 Blynk V5 或本地按键改变,loop() 根据它决定是否避障。
csb 超声波测得的距离,单位近似为厘米。 判断前方是否有障碍物。
pass_str 正确密码,初始化为 1324 和用户输入的 key_str 比较。
key_str 用户当前输入的密码。 每按一次密码键就追加一个数字,达到 4 位后进行判断。
time_ms 上一次请求定位数据的时间。 配合 millis() 实现每 10 秒请求一次定位。
read_strread_char 串口读取定位数据时使用。 把 Serial2 收到的字符拼成完整字符串。
log_strlat_str 解析出来的两个坐标字段。 打印到串口,并上传到 Blynk V11、V12。
安全建议:源码中包含 WiFi 密码和 Blynk 授权信息。对外展示或分享代码时,建议替换成占位符,避免泄露真实连接信息。

7. Blynk 远程控制 源代码第 38 到 94 行,第 103 到 116 行

Blynk 使用虚拟引脚和手机端控件通信。手机上某个按钮状态变化后,对应的 BLYNK_WRITE(Vx) 函数会被调用。

这里的“虚拟引脚”不是 STM32 板子上的真实引脚,而是 Blynk 平台提供的一种软件通道。手机端控件绑定 V1、V2 这样的编号,单片机程序也监听同样的编号,二者就能通过网络交换数据。

虚拟引脚 功能 收到非 0 值 收到 0
V1 前进 调用 forward() 调用 stop_motor()
V2 后退 调用 back() 停止电机
V3 左转 调用 Turn_left() 停止电机
V4 右转 调用 Turn_right() 停止电机
V5 模式切换 set_mode = 2,开启避障模式 set_mode = 1,回到普通模式
V11、V12 定位数据显示 程序将解析出的两个坐标字段上传到这两个虚拟引脚。

控制回调示例

BLYNK_WRITE(V1) {
  vpin_value = param.asInt();
  if (vpin_value) {
    forward();
    set_mode = 1;
  } else {
    stop_motor();
    set_mode = 1;
  }
}

以上代码表示:V1 按钮按下时小车前进,松开时停止。无论按下还是松开,都会把模式设回普通控制模式。

param.asInt() 的作用是把 Blynk 传来的值转成整数。对于按钮控件来说,通常按下是 1,松开是 0,所以代码用 if (vpin_value) 判断按钮当前是否处于按下状态。

联网状态提示

BLYNK_CONNECTED() 中把蜂鸣器引脚写 LOW,BLYNK_DISCONNECTED() 中把蜂鸣器引脚写 HIGH。实际表现取决于蜂鸣器模块是高电平触发还是低电平触发。

需要注意的是,Blynk 的回调依赖 Blynk.run() 持续运行。如果主循环长时间被 delay()while 阻塞,远程控制响应就可能变慢。

8. 电机控制 源代码第 96 到 155 行

左右两个电机都由方向引脚和速度引脚控制。方向引脚使用 digitalWrite() 输出高低电平,速度引脚使用 analogWrite() 输出 PWM。

STM32 的引脚一般不能直接带动电机,因为电机需要的电流比单片机引脚能提供的大得多。所以中间通常会有电机驱动模块。STM32 只负责给驱动模块发送控制信号,真正给电机供电和换向的是驱动模块。

小车转向依赖左右轮的速度和方向差。两边轮子都向前转,小车前进;两边都向后转,小车后退;一边向前、一边向后,小车就会原地或近似原地转向。

函数 动作 实现方式
forward() 前进 左右方向引脚写 HIGH,速度引脚写 255 - MOTOR_SPEED
back() 后退 左右方向引脚写 LOW,速度引脚写 MOTOR_SPEED
Turn_left() 左转 左右电机方向相反,让小车向左旋转或转弯。
Turn_right() 右转 左右电机方向相反,让小车向右旋转或转弯。
stop_motor() 停止 两个速度引脚都写 0。
Turn_left_90()Turn_right_90() 定时转向 先停止,再转动约 1.2 秒,再停止,用固定时间近似 90 度。
关于速度值:代码里 MOTOR_SPEED 为 255,所以 255 - MOTOR_SPEED 等于 0。这个写法通常与具体电机驱动板的控制方式有关,实际方向和速度需要结合硬件验证。

9. 超声波避障 源代码第 157 到 177 行,第 324 到 335 行

超声波测距模块的工作方式类似“发声再听回声”。模块发出一束超声波,声波碰到障碍物后反射回来。程序测量从发出到收到回波经过了多久,再根据声速换算距离。

距离检测

float checkdistance_PB8_PB9() {
  digitalWrite(PB8, LOW);
  delayMicroseconds(2);
  digitalWrite(PB8, HIGH);
  delayMicroseconds(10);
  digitalWrite(PB8, LOW);
  float distance = pulseIn(PB9, HIGH) / 58.00;
  delay(10);
  return distance;
}

PB8 负责给超声波模块发送触发脉冲,PB9 负责读取回波。pulseIn(PB9, HIGH) 测量回波高电平持续时间,除以 58 后得到近似厘米数。

代码中先让 PB8 保持低电平,再输出 10 微秒高电平,这是很多超声波模块要求的触发方式。PB9 上高电平持续时间越长,说明声音来回走过的距离越远,也就是障碍物越远。

避障判断

void obstacles_mode() {
  csb = checkdistance_PB8_PB9();
  Serial.println(csb);
  if (20 > csb) {
    Avoid_objects();
  } else {
    forward();
  }
}

当前距离小于 20 cm 时执行绕障动作,否则继续前进。

绕障动作

Avoid_objects() 使用固定动作绕开障碍:右转、前进、左转、前进、左转、前进、右转。它不判断左右两侧是否还有障碍物,因此属于比较简单的避障策略。

这种避障方式容易理解,也容易实现,但它没有真正“看见”周围环境。它只是在检测到前方太近时执行一套预设动作,所以如果障碍物形状复杂,或者地面摩擦导致转向角度不准,实际路线可能和预想不同。

10. 按键密码与本地控制 源代码第 179 到 322 行

key_procedure() 负责处理两个模拟按键输入。key_1 用于输入密码,key_2 用于直接控制小车运动。

这部分代码比较长,是因为它既要识别不同按键,又要处理按键抖动,还要等待按键松开。实体按键不是理想开关,按下和松开的瞬间电压会短暂抖动,因此代码需要多次确认。

按键识别方式

按键不是通过多个数字引脚区分,而是通过一个模拟引脚读取不同电压值。程序根据 ADC 数值范围判断按下的是哪个键。

这种做法常见于“电阻分压按键”。多个按键接在同一个模拟输入上,不同按键按下时形成不同分压,单片机读到的 ADC 数值就不同。好处是节省引脚,代价是需要设置合理的电压判断范围。

ADC 范围 key_1 功能 key_2 功能
> 3900 输入数字 1 前进
2800 到 3800 输入数字 2 后退
2100 到 2700 输入数字 3 右转
1000 到 2000 输入数字 4 左转

密码流程

每识别到一次有效按键,程序就把对应数字追加到 key_str。当 key_str 长度达到 4 位时,程序把它与 pass_str 比较。

按键消抖

每次检测到按键后,代码会延时 20 ms 再确认一次,并在 while 中等待按键松开。这样可以避免按键抖动导致一次按压被识别成多次。

本地方向控制

key_2 的逻辑是按住就运动,松开就停止。在等待松开的过程中,如果 WiFi 已连接,代码仍会调用 Blynk.run(),避免远程通信长时间没有被处理。

11. OLED 显示 源代码第 337 到 358 行,第 385 到 396 行,第 457 到 460 行

OLED 使用 U8g2 库显示文字。程序启动时显示 initialize...,运行时主要显示密码输入页面。

OLED 可以看作小车的人机界面。没有屏幕时,用户只能通过串口或蜂鸣器猜测设备状态;有了 OLED 后,程序可以直接显示初始化状态、密码输入位数、密码正确或错误等信息。

初始化显示

setup() 中先设置 OLED I2C 地址,再调用 u8g2.begin() 初始化显示屏,然后显示一行 initialize...

密码页面

page1() 显示两行提示文字,并根据已经输入的密码位数显示星号。例如输入 1 位显示 *,输入 2 位显示 **。这样可以提示用户当前输入长度,同时不暴露真实密码。

屏幕坐标从左上角开始计算,横向是 x,纵向是 y。代码中的 setCursor(0, 16) 表示文字从左侧第 0 列、纵向第 16 像素附近开始显示。

u8g2.firstPage();
do {
  page1();
} while (u8g2.nextPage());

这是 U8g2 常见的页面刷新方式,所有绘图代码放在 firstPage()nextPage() 之间。

这类刷新方式的特点是:每次刷新都重新画一遍需要显示的内容,而不是只改某一个字符。因此 page1() 中要把提示文字和星号都写完整。

12. 定位数据处理 源代码第 431 到 454 行

程序每 10 秒通过 Serial2 向定位模块发送一次命令:

定位模块和 STM32 之间使用串口通信。串口通信可以简单理解为双方按约定速度发送字符:STM32 发送命令,定位模块返回一段文本,程序再从这段文本里取出需要的字段。

Serial2.println("config,get,lbsloc");

如果 Serial2 收到数据,程序会逐字节读取并拼接成 read_str。随后使用逗号位置截取两个字段:

int firstComma = read_str.indexOf(',');
int secondComma = read_str.indexOf(',', firstComma + 1);
int thirdComma = read_str.indexOf(',', secondComma + 1);
int fourthComma = read_str.indexOf(',', thirdComma + 1);
log_str = read_str.substring(thirdComma + 1, fourthComma);
lat_str = read_str.substring(fourthComma + 1);

从命名来看,log_str 可能想表示经度字段,lat_str 表示纬度字段。解析完成后,程序把这两个值打印到调试串口,并在 WiFi 正常连接时上传到 Blynk 的 V11 和 V12。

这段解析逻辑依赖返回字符串格式稳定。如果模块返回内容少了逗号、顺序改变,或者混入错误提示,当前代码仍会尝试截取字符串,所以后续维护时最好增加格式检查。

理解方式:定位模块返回的是一段逗号分隔字符串。程序不是解析完整协议,而是直接取第 3 个逗号后、第 4 个逗号后的内容作为坐标字段。

13. 程序运行流程 源代码第 369 到 467 行

Arduino 风格程序通常由两个核心函数组成:setup()loop()setup() 只在上电或复位后执行一次,用来做初始化;loop() 会一直重复执行,用来持续响应外部变化。

setup 初始化

setup()
  1. 打开 Serial、Serial2、Serial3
  2. 设置默认变量和默认密码
  3. 初始化 OLED 并显示 initialize...
  4. 配置 ESP8266,连接 Blynk
  5. 设置电机、按键、蜂鸣器、继电器、超声波引脚
  6. 如果 Blynk 已连接,清空 V1 到 V5、V11、V12 的显示状态

loop 主循环

loop()
  1. 每 10 秒请求一次定位数据
  2. 如果 Serial2 有数据,解析坐标并上传
  3. 处理本地按键和密码逻辑
  4. 刷新 OLED 密码输入页面
  5. 调用 Blynk.run() 处理远程通信
  6. 如果 set_mode == 2,执行超声波避障

这类程序的特点是不会运行一次就结束,而是在 loop() 中反复检查各个模块的状态。远程控制、按键输入、定位数据和避障逻辑都通过主循环持续工作。

主循环的顺序会影响响应速度。例如程序正在执行绕障动作或等待按键松开时,其他功能也要等当前代码让出执行机会后才能继续处理。因此代码中有些延时函数会在等待期间调用 Blynk.run(),这是为了尽量保持远程连接不掉线。

初次阅读这类程序时,不需要把每一行都记住。更重要的是理解每一轮循环会检查哪些输入、更新哪些变量、触发哪些输出。按照这个顺序看,整份代码的运行路径会更清晰。

14. 当前代码注意事项

下面是根据当前源码整理出的注意点,主要用于后续维护、交付或继续开发时参考。

位置 说明 建议
第 18 到 20 行 源码中直接写入了 Blynk 授权信息和 WiFi 信息。 对外分享前建议替换为占位符,或改为单独配置文件。
第 189、201、212、223、228、243、442 行附近 部分中文注释或串口提示在源码中显示为乱码。 把源码统一保存为 UTF-8,并把提示文字改回正常中文。
第 235 行、第 343 行 OLED 打印字符串疑似受乱码影响,字符串没有正常结束。 建议改成合法字符串,例如 u8g2.print("Password is correct");u8g2.print("password");
第 163 行 pulseIn(PB9, HIGH) 没有设置超时时间。 如果没有收到回波,程序可能等待较久。可以增加超时参数。
第 431 到 448 行 解析定位字符串前没有检查逗号是否存在。 建议判断 indexOf() 是否返回 -1,避免异常数据导致截取错误。
整体 部分变量当前未使用,例如 playlistrun_stateweight_value_gmy_count 如果后续没有对应功能,可以删除,减少阅读成本。
整体 转弯和绕障主要依赖固定延时,属于开环控制。 如果需要更稳定的运动效果,可以加入编码器、陀螺仪或更完整的避障策略。

建议的最小修正

// OLED 成功提示
u8g2.print("Password is correct");

// OLED 密码输入提示
u8g2.print("password");

// 超声波读取增加超时示例
float distance = pulseIn(PB9, HIGH, 30000) / 58.00;