一、项目背景与意义
学习 STM32 的过程中,许多人在掌握理论知识后,往往面临缺乏实践项目锻炼的困境。智能循迹避障小车系统作为一个综合性项目,涵盖了 STM32 的 GPIO 控制、传感器数据采集、算法逻辑实现等核心知识点,非常适合作为 STM32 学习后的首个实践项目。本文将分享基于 STM32 的智能循迹避障小车系统设计与实现过程,希望能为正在探索嵌入式开发的同行提供参考和启发。以下大部分都是基于江协的代码修改实现,欢迎大家进行交流学习。
二、系统整体架构
三、硬件设计详解
核心控制器:STM32F103C8T6
STM32F103C8T6是一款高性能、低功耗的32位ARM Cortex- M3处理器,主频可达72MHz,具备强大的处理能力。
内置多种外设接口,如USART、SPI、I2C等,便于与其他模块通信,满足小车系统控制需求。
电机驱动模块:TB6612FNG
TB6612FNG是一款双通道直流电机驱动芯片,可驱动两路电机独立运行。
具有驱动能力强、效率高、控制简单等特点,适用于小车电机驱动。
将TB6612FNG与小车电机相连,通过STM32输出PWM信号控制电机转速。
红外传感器:TCRT5000
TCRT5000是一种反射式红外传感器,通过发射红外光并接收反射光来检测物体表面的反射率差异。
当小车行驶在黑白路径上时,传感器可依据反射光强度判断路径位置,实现精准循迹。
采用基于传感器阵列的数字信号处理算法,实时分析路径信息,确定小车行驶方向。
当检测到路径偏移时,通过调整电机转速与转向,使小车快速回归正确路径,确保循迹精度与稳定性。
蓝牙串口模块: HC-05
HC- 05是一款主从机一体的蓝牙串口透传模块,支持蓝牙2.0协议,可实现设备之间的无线数据通信。在本项目中,通过HC- 05将手机端与小车控制系统连接,实现远程遥控功能。将HC- 05的串口引脚与STM32的USART接口相连,即可完成硬件连接。在手机端使用蓝牙调试器,通过蓝牙协议与HC- 05通信,发送控制指令。STM32接收手机端发送的指令,解析并执行相应操作,如前进、后退、转弯等。实现小车的远程操控功能,拓展小车的应用场景与操作便利性。
超声波传感器: HC-SR04
HC- SR04利用超声波在空气中的传播特性,通过发射超声波并接收反射波,测量超声波往返时间,计算障碍物距离。
将HC- SR04的信号引脚与STM32的GPIO口相连,通过控制信号引脚触发超声波发射,接收回波信号并计算距离。
设定安全距离阈值,当检测到障碍物距离小于阈值时,触发避障算法。
通过调整小车行驶方向与速度,避开障碍物,选择最优路径继续行驶,保障小车安全运行。
其他模块:OLED显示屏、DC3V-6V直流减速电机、SG90 9g舵机、12v锂电池组
其他模块这里就不再详细介绍,使用一块OLED显示屏会对我们调试程序有非常大的帮助,直流电机配合电机驱动芯片控制小车运动,舵机转动配合超声波模块实现避障。
3.1 核心控制单元
-
STM32最小系统设计要点
-
PWM 输出:利用 TIM(定时器)外设产生 PWM 信号控制电机转速,通常选择具有互补输出功能的定时器通道,确保电机驱动的稳定性。
-
ADC 采集:将循迹模块的红外传感器输出连接至 STM32 的 ADC 引脚,配置为连续转换模式,通过 DMA(直接内存访问)减少 CPU 负载,快速获取传感器模拟信号。
-
UART 通信:预留 USART1 作为调试串口,连接至 USB 转 TTL 模块,方便烧录程序与打印调试信息;USART2 用于蓝牙模块通信,通过配置波特率实现无线数据传输。
3.2 运动控制模块
-
将 STM32 的 GPIO 引脚与 TB6612FNG 的 IN1-IN4(电机转向控制)和 PWMA、PWMB(电机速度控制)连接,通过高低电平组合实现电机正转
-
PWM 调速通过调节占空比实现,公式为:Vout=Vmax×Tton,其中ton为高电平时间,T为 PWM 周期。在 STM32 中,通过配置 TIM 外设的 ARR(自动重装载值)和 CCR(捕获 / 比较寄存器)调整占空比,例如设置 ARR=100,CCR=50 时,占空比为 50%。
3.3 感知系统设计
循迹模块
TCRT5000 阵列布局方案
TCRT5000 是红外反射式传感器,通过发射红外光并检测反射信号判断地面颜色。推荐采用 5 路传感器阵列布局:中间 1 路用于直行检测,两侧各 2 路用于弯道和偏移修正。传感器间距建议为 2cm,确保能覆盖 1cm 宽的黑色循迹线。(代码里使用的是四路传感器)
红外反射式检测原理
传感器通过发射红外光并接收反射光来检测物体表面的反射率差异。当小车行驶在黑白路径上时,传感器可依据反射光强度判断路径位置,同时返回一个数字信号,在软件设计里通过数字信号的判断实现精准循迹。
避障模块
HC- SR04利用超声波在空气中的传播特性,通过发射超声波并接收反射波,测量超声波往返时间,计算障碍物距离。我们只需要设定安全距离阈值,当检测到障碍物距离小于阈值时,触发避障算法。通过调整小车行驶方向与速度,避开障碍物,选择最优路径继续行驶,保障小车安全运行。
蓝牙通信模块
HC-05 主从模式配置
HC-05 默认工作在从模式,通过 AT 指令可切换至主模式:
- 将 HC-05 与 STM32 的 USART2 连接,设置波特率为 38400bps(AT 指令默认波特率)。
- 发送AT指令测试通信,返回OK表示连接正常。
- 切换主模式:发送AT+ROLE=1,重启后生效。
将HC- 05的串口引脚与STM32的USART接口相连,即可完成硬件连接。在手机端使用蓝牙调试器,通过蓝牙协议与HC- 05通信,发送控制指令。STM32接收手机端发送的指令,解析并执行相应操作,如前进、后退、转弯等。实现小车的远程操控功能,拓展小车的应用场景与操作便利性。
四、软件设计
主函数 main
处理OLED显示和模式切换
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "Timer.h"
#include "TB6612.h"
#include "OLED.h"
#include "Key.h"
#include "TCRT5000.h"
#include "OLED.h"
#include "USART.h"
#include "ADC.h"
#include "HC_SR04.h"
uint16_t ADValue; //ADC采样值
float Voltage; //电压值
void Mode_Change(void)
{
switch(Mode)
{
case 0:
TIM_SetCompare1(TIM3,72); //控制舵机朝前
AIN1 = 0; AIN2 = 0; //停止
BIN1 = 0; BIN2 = 0;
OLED_ShowString(4, 1, "Stop ");
break;
case 1:
SR04_Follow(); //超声波跟随
break;
case 2:
Usart_Ctrl(); //蓝牙遥控
break;
case 3:
SR04_bizhang(); //超声波避障
break;
case 4:
HW_Tracking(); //红外循迹
break;
}
}
int main(void)
{
LED_Init(); //LED初始化
TB6612_Init(); //电机初始化
Exti_Init(); //外部中断初始化
TCRT5000_Init(); //红外对管初始化
OLED_Init(); //OLED显示初始化
Serial_Init(); //串口初始化
AD_Init(); //ADC初始化
SR04_Init(); //超声波模块初始化
IC_Init(); //定时器2通道2输入捕获初始化
TIM1_PWM_Init(1999, 359); //定时器1 PWM 输出初始化(电机)
TIM3_PWM_Init(999, 1439); //定时器3 PWM 输出初始化(舵机) 72M / 1000 / 1440 = 50
OLED_ShowString(1,1,"Mode: "); //显示模式
OLED_ShowString(2,1,"Voltage:0.00V"); //显示电压值
OLED_ShowString(3,1,"Distance:"); //显示距离
while(1)
{
OLED_ShowNum(1,6,Mode,1);
Usart_Ctrl();
Mode_Change();
//ADC电压转换
ADValue = AD_GetValue(); //获取ADC采样数据
Voltage = (float)ADValue / 4095 * 3.3; //转换电压值
OLED_ShowNum(2,9,Voltage,1); //电压值整数部分
OLED_ShowNum(2,11,(uint16_t)(Voltage * 100) %100,2); //电压值小数部分
//超声波测距
float dist = Get_Distance();
if(dist < 100) OLED_ShowNum(3,10,dist,2);
else OLED_ShowNum(3,10,dist,3);
Delay_ms(100); // 每次测量间隔至少60ms
}
}
延时函数 Delay.c
延时函数可以直接调用江协的代码
#include "stm32f10x.h"
/**
* @brief 微秒级延时
* @param xus 延时时长,范围:0~233015
* @retval 无
*/
void Delay_us(uint32_t xus)
{
SysTick->LOAD = 72 * xus; //设置定时器重装值
SysTick->VAL = 0x00; //清空当前计数值
SysTick->CTRL = 0x00000005; //设置时钟源为HCLK,启动定时器
while(!(SysTick->CTRL & 0x00010000)); //等待计数到0
SysTick->CTRL = 0x00000004; //关闭定时器
}
/**
* @brief 毫秒级延时
* @param xms 延时时长,范围:0~4294967295
* @retval 无
*/
void Delay_ms(uint32_t xms)
{
while(xms--)
{
Delay_us(1000);
}
}
/**
* @brief 秒级延时
* @param xs 延时时长,范围:0~4294967295
* @retval 无
*/
void Delay_s(uint32_t xs)
{
while(xs--)
{
Delay_ms(1000);
}
}
显示模块 OLED.c
直接调用江协的代码再改一下GPIO(根据你的原理图进行修改)
#include "stm32f10x.h"
#include "OLED_Font.h"
/*引脚配置*/
#define OLED_W_SCL(x) GPIO_WriteBit(GPIOC, GPIO_Pin_14, (BitAction)(x))
#define OLED_W_SDA(x) GPIO_WriteBit(GPIOC, GPIO_Pin_15, (BitAction)(x))
/*引脚初始化*/
void OLED_I2C_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_Init(GPIOC, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15;
GPIO_Init(GPIOC, &GPIO_InitStructure);
OLED_W_SCL(1);
OLED_W_SDA(1);
}
/**
* @brief I2C开始
* @param 无
* @retval 无
*/
void OLED_I2C_Start(void)
{
OLED_W_SDA(1);
OLED_W_SCL(1);
OLED_W_SDA(0);
OLED_W_SCL(0);
}
/**
* @brief I2C停止
* @param 无
* @retval 无
*/
void OLED_I2C_Stop(void)
{
OLED_W_SDA(0);
OLED_W_SCL(1);
OLED_W_SDA(1);
}
/**
* @brief I2C发送一个字节
* @param Byte 要发送的一个字节
* @retval 无
*/
void OLED_I2C_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i++)
{
OLED_W_SDA(!!(Byte & (0x80 >> i)));
OLED_W_SCL(1);
OLED_W_SCL(0);
}
OLED_W_SCL(1); //额外的一个时钟,不处理应答信号
OLED_W_SCL(0);
}
/**
* @brief OLED写命令
* @param Command 要写入的命令
* @retval 无
*/
void OLED_WriteCommand(uint8_t Command)
{
OLED_I2C_Start();
OLED_I2C_SendByte(0x78); //从机地址
OLED_I2C_SendByte(0x00); //写命令
OLED_I2C_SendByte(Command);
OLED_I2C_Stop();
}
/**
* @brief OLED写数据
* @param Data 要写入的数据
* @retval 无
*/
void OLED_WriteData(uint8_t Data)
{
OLED_I2C_Start();
OLED_I2C_SendByte(0x78); //从机地址
OLED_I2C_SendByte(0x40); //写数据
OLED_I2C_SendByte(Data);
OLED_I2C_Stop();
}
/**
* @brief OLED设置光标位置
* @param Y 以左上角为原点,向下方向的坐标,范围:0~7
* @param X 以左上角为原点,向右方向的坐标,范围:0~127
* @retval 无
*/
void OLED_SetCursor(uint8_t Y, uint8_t X)
{
OLED_WriteCommand(0xB0 | Y); //设置Y位置
OLED_WriteCommand(0x10 | ((X & 0xF0) >> 4)); //设置X位置高4位
OLED_WriteCommand(0x00 | (X & 0x0F)); //设置X位置低4位
}
/**
* @brief OLED清屏
* @param 无
* @retval 无
*/
void OLED_Clear(void)
{
uint8_t i, j;
for (j = 0; j < 8; j++)
{
OLED_SetCursor(j, 0);
for(i = 0; i < 128; i++)
{
OLED_WriteData(0x00);
}
}
}
/**
* @brief OLED显示一个字符
* @param Line 行位置,范围:1~4
* @param Column 列位置,范围:1~16
* @param Char 要显示的一个字符,范围:ASCII可见字符
* @retval 无
*/
void OLED_ShowChar(uint8_t Line, uint8_t Column, char Char)
{
uint8_t i;
OLED_SetCursor((Line - 1) * 2, (Column - 1) * 8); //设置光标位置在上半部分
for (i = 0; i < 8; i++)
{
OLED_WriteData(OLED_F8x16[Char - ' '][i]); //显示上半部分内容
}
OLED_SetCursor((Line - 1) * 2 + 1, (Column - 1) * 8); //设置光标位置在下半部分
for (i = 0; i < 8; i++)
{
OLED_WriteData(OLED_F8x16[Char - ' '][i + 8]); //显示下半部分内容
}
}
/**
* @brief OLED显示字符串
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param String 要显示的字符串,范围:ASCII可见字符
* @retval 无
*/
void OLED_ShowString(uint8_t Line, uint8_t Column, char *String)
{
uint8_t i;
for (i = 0; String[i] != '\0'; i++)
{
OLED_ShowChar(Line, Column + i, String[i]);
}
}
/**
* @brief OLED次方函数
* @retval 返回值等于X的Y次方
*/
uint32_t OLED_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1;
while (Y--)
{
Result *= X;
}
return Result;
}
/**
* @brief OLED显示数字(十进制,正数)
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~4294967295
* @param Length 要显示数字的长度,范围:1~10
* @retval 无
*/
void OLED_ShowNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i++)
{
OLED_ShowChar(Line, Column + i, Number / OLED_Pow(10, Length - i - 1) % 10 + '0');
}
}
/**
* @brief OLED显示数字(十进制,带符号数)
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:-2147483648~2147483647
* @param Length 要显示数字的长度,范围:1~10
* @retval 无
*/
void OLED_ShowSignedNum(uint8_t Line, uint8_t Column, int32_t Number, uint8_t Length)
{
uint8_t i;
uint32_t Number1;
if (Number >= 0)
{
OLED_ShowChar(Line, Column, '+');
Number1 = Number;
}
else
{
OLED_ShowChar(Line, Column, '-');
Number1 = -Number;
}
for (i = 0; i < Length; i++)
{
OLED_ShowChar(Line, Column + i + 1, Number1 / OLED_Pow(10, Length - i - 1) % 10 + '0');
}
}
/**
* @brief OLED显示数字(十六进制,正数)
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~0xFFFFFFFF
* @param Length 要显示数字的长度,范围:1~8
* @retval 无
*/
void OLED_ShowHexNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
uint8_t i, SingleNumber;
for (i = 0; i < Length; i++)
{
SingleNumber = Number / OLED_Pow(16, Length - i - 1) % 16;
if (SingleNumber < 10)
{
OLED_ShowChar(Line, Column + i, SingleNumber + '0');
}
else
{
OLED_ShowChar(Line, Column + i, SingleNumber - 10 + 'A');
}
}
}
/**
* @brief OLED显示数字(二进制,正数)
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~1111 1111 1111 1111
* @param Length 要显示数字的长度,范围:1~16
* @retval 无
*/
void OLED_ShowBinNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i++)
{
OLED_ShowChar(Line, Column + i, Number / OLED_Pow(2, Length - i - 1) % 2 + '0');
}
}
/**
* @brief OLED初始化
* @param 无
* @retval 无
*/
void OLED_Init(void)
{
uint32_t i, j;
for (i = 0; i < 1000; i++) //上电延时
{
for (j = 0; j < 1000; j++);
}
OLED_I2C_Init(); //端口初始化
OLED_WriteCommand(0xAE); //关闭显示
OLED_WriteCommand(0xD5); //设置显示时钟分频比/振荡器频率
OLED_WriteCommand(0x80);
OLED_WriteCommand(0xA8); //设置多路复用率
OLED_WriteCommand(0x3F);
OLED_WriteCommand(0xD3); //设置显示偏移
OLED_WriteCommand(0x00);
OLED_WriteCommand(0x40); //设置显示开始行
OLED_WriteCommand(0xA1); //设置左右方向,0xA1正常 0xA0左右反置
OLED_WriteCommand(0xC8); //设置上下方向,0xC8正常 0xC0上下反置
OLED_WriteCommand(0xDA); //设置COM引脚硬件配置
OLED_WriteCommand(0x12);
OLED_WriteCommand(0x81); //设置对比度控制
OLED_WriteCommand(0xCF);
OLED_WriteCommand(0xD9); //设置预充电周期
OLED_WriteCommand(0xF1);
OLED_WriteCommand(0xDB); //设置VCOMH取消选择级别
OLED_WriteCommand(0x30);
OLED_WriteCommand(0xA4); //设置整个显示打开/关闭
OLED_WriteCommand(0xA6); //设置正常/倒转显示
OLED_WriteCommand(0x8D); //设置充电泵
OLED_WriteCommand(0x14);
OLED_WriteCommand(0xAF); //开启显示
OLED_Clear(); //OLED清屏
}
定时器模块 Timer.c
产生PWM波控制电机和舵机转动
#include "stm32f10x.h"
// 定时器1 PWM 初始化(电机转动)
// 参数 ARR: 自动重装载值,用于设置PWM周期
// 参数 PSC: 预分频器值,用于设置定时器时钟频率
void TIM1_PWM_Init(uint16_t ARR, uint16_t PSC)
{
// 使能TIM1的时钟,TIM1是高级定时器,用于产生PWM信号
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
// 使能GPIOA的时钟,因为PWM信号将通过GPIOA的引脚输出
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
// 配置TIM1使用内部时钟源
TIM_InternalClockConfig(TIM1);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
TIM_TimeBaseInitStruct.TIM_ClockDivision = 0;
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStruct.TIM_Period = ARR;
TIM_TimeBaseInitStruct.TIM_Prescaler = PSC;
TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseInitStruct);
// 定义定时器输出比较初始化结构体
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure);
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0;
// 初始化TIM1的通道1输出比较参数
TIM_OC1Init(TIM1,&TIM_OCInitStructure);
// 使能TIM1通道1的预装载寄存器
TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable);
TIM_OCStructInit(&TIM_OCInitStructure);
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0;
// 初始化TIM1的通道4输出比较参数
TIM_OC4Init(TIM1,&TIM_OCInitStructure);
// 使能TIM1通道4的预装载寄存器
TIM_OC4PreloadConfig(TIM1, TIM_OCPreload_Enable);
// 使能TIM1定时器
TIM_Cmd(TIM1, ENABLE);
// 使能TIM1的PWM输出(高级定时器需开启)
TIM_CtrlPWMOutputs(TIM1,ENABLE);
}
// 定时器3 PWM 初始化(舵机转向)
void TIM3_PWM_Init(uint16_t ARR, uint16_t PSC)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
TIM_InternalClockConfig(TIM3);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
TIM_TimeBaseInitStruct.TIM_ClockDivision = 0;
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStruct.TIM_Period = ARR;
TIM_TimeBaseInitStruct.TIM_Prescaler = PSC;
TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStruct);
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure);
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0;
TIM_OC1Init(TIM3, &TIM_OCInitStructure);
TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable);
TIM_Cmd(TIM3, ENABLE);
}
电机驱动模块 TB6612.c
驱动电机前进、后退、转弯
#include "stm32f10x.h"
#include "TB6612.h"
#include "Timer.h"
//LED初始化
void LED_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure);
GPIO_ResetBits(GPIOC,GPIO_Pin_13); //PC13置零,点亮LED
}
//电机初始化
void TB6612_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_12 | GPIO_Pin_1 | GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_SetBits(GPIOB, GPIO_Pin_13 | GPIO_Pin_12 | GPIO_Pin_1 | GPIO_Pin_0);
}
void Forward(void) //小车前行
{
AIN1 = 0;
AIN2 = 1;
BIN1 = 0;
BIN2 = 1;
TIM_SetCompare1(TIM1,1500);
TIM_SetCompare4(TIM1,1500);
}
void Backward(void) //小车后退
{
AIN1 = 1;
AIN2 = 0;
BIN1 = 1;
BIN2 = 0;
TIM_SetCompare1(TIM1,1500);
TIM_SetCompare4(TIM1,1500);
}
void TurnRight(void) //小车右转
{
AIN1 = 0;
AIN2 = 1;
BIN1 = 1;
BIN2 = 0;
TIM_SetCompare1(TIM1,1500);
TIM_SetCompare4(TIM1,1500);
}
void TurnLeft(void) //小车左转
{
AIN1 = 1;
AIN2 = 0;
BIN1 = 0;
BIN2 = 1;
TIM_SetCompare1(TIM1,1500);
TIM_SetCompare4(TIM1,1500);
}
按键模块 Key.c
按键控制模式切换
#include "stm32f10x.h"
#include "Delay.h"
#include "Key.h"
#include "TB6612.h"
#include "USART.h"
void Key_Init(void) //按键初始化
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12; //PA12 (Key2)
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; //PA7 (Key1)
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //下拉输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
void Exti_Init(void)
{
Key_Init();
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource12);// 将GPIOB的第12个引脚与EXTI Line12关联,以便可以作为外部中断线使用。
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line12;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;// 设置中断触发方式为下降沿触发
EXTI_Init(&EXTI_InitStructure);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource7);
EXTI_InitStructure.EXTI_Line = EXTI_Line7;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
EXTI_Init(&EXTI_InitStructure);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //分组
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI9_5_IRQn; //使能外部中断通道7
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;//抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x03; //子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断
NVIC_Init(&NVIC_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn; //使能外部中断通道12
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
void EXTI9_5_IRQHandler(void)
{
if(Key1 == 1) //按下Key1 (下拉输入)
{
Mode ++;
if(Mode == 5) Mode = 1;
LED = ~LED;
}
EXTI_ClearITPendingBit(EXTI_Line7); //清除EXTI_Line7上的中断标志位
}
void EXTI15_10_IRQHandler(void)
{
if(Key2 == 0) //按下Key2 (上拉输入)
{
Mode = 0;
LED = ~LED;
}
EXTI_ClearITPendingBit(EXTI_Line12);
}
ADC电压转换模块 ADC.c
ADC转换电池电压,不影响主体功能
#include "stm32f10x.h" // Device header
void AD_Init(void) //ADC初始化
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; //选择GPIOA的引脚4作为模拟输入引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
ADC_RegularChannelConfig(ADC1,ADC_Channel_4,1,ADC_SampleTime_55Cycles5);
ADC_InitTypeDef ADC_InitStructure;
// 将ADC的数据对齐方式设置为右对齐
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
// 设置ADC的外部触发转换模式为无
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
// 将ADC的工作模式设置为独立模式
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
// 禁用ADC的连续转换模式
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
// 禁用ADC的扫描转换模式,即只对单个通道进行转换
ADC_InitStructure.ADC_ScanConvMode = DISABLE;
// 设置ADC规则组中要转换的通道数量为1个
ADC_InitStructure.ADC_NbrOfChannel = 1;
ADC_Init(ADC1,&ADC_InitStructure);
ADC_Cmd(ADC1,ENABLE); //使能
ADC_ResetCalibration(ADC1); //校准
while(ADC_GetResetCalibrationStatus(ADC1) == SET);
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1) == SET);
}
uint16_t AD_GetValue(void) //获取指定ADC通道转换值的函数
{
ADC_SoftwareStartConvCmd(ADC1,ENABLE);
while(ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC) == RESET);
return ADC_GetConversionValue(ADC1);
}
红外循迹模块 TCRT5000
根据红外传感器返回值调整小车运动状态
#include "stm32f10x.h"
#include "TCRT5000.h"
#include "Delay.h"
#include "TB6612.h"
void TCRT5000_Init(void) //红外对管初始化
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB,ENABLE);
//重映射配置关闭JTAG-DP 启用SW-DP从而可以使用PA15 PB3 PB4
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //下拉输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_4 | GPIO_Pin_3;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
void HW_Tracking(void) //红外循迹
{
//前进
if(HW_1 == 0 && HW_2 == 0 && HW_3 == 0 && HW_4 == 0)
{
Forward();
Delay_ms(50);
}
//右转
if(HW_1 == 0 && HW_2 == 1 && HW_3 == 0 && HW_4 == 0)
{
TurnRight();
Delay_ms(150);
}
if(HW_1 == 1 && HW_2 == 0 && HW_3 == 0 && HW_4 == 0)
{
TurnRight();
Delay_ms(250);
}
if(HW_1 == 1 && HW_2 == 1 && HW_3 == 0 && HW_4 == 0)
{
TurnRight();
Delay_ms(300);
}
//左转
if(HW_1 == 0 && HW_2 == 0 && HW_3 == 1 && HW_4 == 0)
{
TurnLeft();
Delay_ms(150);
}
if(HW_1 == 0 && HW_2 == 0 && HW_3 == 0 && HW_4 == 1)
{
TurnLeft();
Delay_ms(250);
}
if(HW_1 == 0 && HW_2 == 0 && HW_3 == 1 && HW_4 == 1)
{
TurnLeft();
Delay_ms(300);
}
}
蓝牙遥控模块 USART.c
通过连接蓝牙可以使用手机直接控制小车状态和模式选择
#include "stm32f10x.h"
#include <stdio.h>
#include <stdarg.h>
#include "Delay.h"
#include "TB6612.h"
#include "OLED.h"
uint8_t Mode = 0; //模式选择(1:超声波跟随;2:蓝牙遥控;3:超声波避障;4:红外循迹)
/*
uint8_t Serial_RxData; // 存储串口接收到的单个字节数据
uint8_t Serial_RxFlag; // 标记串口是否接收到新数据的标志位
uint8_t temp;
*/
// 环形缓冲区配置
// 定义环形缓冲区的大小为64字节
#define RX_BUFFER_SIZE 64
// 定义一个8位整数数组,作为环形缓冲区,用于存储串口接收到的数据
volatile uint8_t Serial_RxBuffer[RX_BUFFER_SIZE];
// 定义一个16位整数,作为环形缓冲区的头指针,初始值为0
volatile uint16_t Serial_RxHead = 0;
// 定义一个16位整数,作为环形缓冲区的尾指针,初始值为0
volatile uint16_t Serial_RxTail = 0;
void Serial_Init(void) //串口1函数初始化
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 ; //TX PB10
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11 ; //RX PB11
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 115200; //波特率
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //收发模式
USART_InitStructure.USART_Parity = USART_Parity_No; //无校验位
USART_InitStructure.USART_StopBits = USART_StopBits_1; //1停止位
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //8位数据
USART_Init(USART3, &USART_InitStructure);
USART_ITConfig(USART3, USART_IT_RXNE, ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
USART_Cmd(USART3, ENABLE);
}
void Serial_SendByte(uint8_t Byte) // 发送单个字节数据的函数
{
USART_SendData(USART3, Byte);
while(USART_GetFlagStatus(USART3, USART_FLAG_TXE) == RESET);
}
void USART3_IRQHandler(void) //串口中断服务函数
{
if(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET)
{
uint8_t data = USART_ReceiveData(USART3);
uint16_t next_head = (Serial_RxHead + 1) % RX_BUFFER_SIZE;
// 缓冲区未满时存储数据
if(next_head != Serial_RxTail)
{
Serial_RxBuffer[Serial_RxHead] = data;
Serial_RxHead = next_head;
}
USART_ClearITPendingBit(USART3, USART_IT_RXNE);
}
}
void Usart_Ctrl(void) //蓝牙控制小车运动
{
while(Serial_RxTail != Serial_RxHead) // 当环形缓冲区中有数据时
{
uint8_t command = Serial_RxBuffer[Serial_RxTail]; // 从环形缓冲区中取出一个字节数据作为命令
Serial_RxTail = (Serial_RxTail + 1) % RX_BUFFER_SIZE; // 更新尾指针的位置
LED = ~LED; // 状态指示
switch(command)
{
case 'A':
if(Mode == 2)
{
Forward(); //前进
OLED_ShowString(4, 1, "Forward ");
}
break;
case 'B':
if(Mode == 2)
{
Backward(); //后退
OLED_ShowString(4, 1, "Backward ");
}
break;
case 'C':
if(Mode == 2)
{
TurnRight();//右转
OLED_ShowString(4, 1, "TurnRight");
}
break;
case 'D':
if(Mode == 2)
{
TurnLeft(); //左转
OLED_ShowString(4, 1, "TurnLeft ");
}
break;
case 'E':
if(Mode == 2)
{
AIN1 = 0; AIN2 = 0; //停止
BIN1 = 0; BIN2 = 0;
OLED_ShowString(4, 1, "Stop ");
}
break;
case 'F':
Mode = 1;
break;
case 'G':
Mode = 2;
break;
case 'H':
Mode = 3;
break;
case 'I':
Mode = 4;
break;
case 'J':
Mode = 0;
break;
}
}
}
/*
void Serial_SendArray(uint8_t *Array, uint16_t Length) // 发送字节数组的函数
{
uint16_t i;
for(i=0; i<Length; i++)
{
Serial_SendByte(Array[i]);
}
}
void Serial_SendString(char *String) // 发送字符串的函数
{
uint8_t i;
for(i=0; String[i] != '\0'; i++)
{
Serial_SendByte(String[i]);
}
}
uint32_t Serial_Pow(uint32_t X, uint32_t Y) // 计算X的Y次幂的函数
{
uint32_t Result = 1;
while(Y--)
{
Result *= X;
}
return Result;
}
void Serial_SendNumber(uint32_t Number, uint8_t Length) // 发送指定长度的数字的函数
{
uint8_t i;
for(i=0; i<Length; i++)
{
Serial_SendByte(Number / Serial_Pow(10, Length - i -1) % 10 + '0');
}
}
int fputc(int ch, FILE *f) // 重定向标准输出函数,将输出重定向到串口
{
Serial_SendByte(ch);
return ch;
}
void Serial_Printf(char *format, ...) // 实现类似printf功能的函数,支持格式化输出
{
char String[100];
va_list arg;
va_start(arg, format);
vsprintf(String, format, arg);
va_end(arg);
Serial_SendString(String);
}
uint8_t Serial_GetRxFlag(void) // 获取串口接收标志位的函数
{
if(Serial_RxFlag != 0)
{
temp = Serial_RxFlag;
Serial_RxFlag = 0;
return temp;
}
return 0;
}
uint8_t Serial_GetRxData(void) // 获取串口接收到的数据的函数
{
return Serial_RxData;
}
*/
超声波避障模块 HC_SR04
实现超声波测距,再通过测距进行避障
#include "stm32f10x.h"
#include "Delay.h"
#include "TB6612.h"
volatile uint32_t rise_time = 0; //存储超声波信号上升沿的捕获时间
volatile uint32_t fall_time = 0; //存储超声波信号下降沿的捕获时间
volatile uint32_t overflow_count = 0; //记录定时器溢出的次数
volatile uint8_t capture_stage = 0; // 0:等待上升沿,1:等待下降沿
volatile float distance_cm = 0.0; //存储计算得到的超声波测量距离
void SR04_Init(void) //超声波模块初始化
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_ResetBits(GPIOA,GPIO_Pin_0);
}
void IC_Init(void) //定时器2通道2输入捕获配置
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; // TIM2_CH2 对应 PA1
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStruct.TIM_Period = 65536 - 1; //自动重装载寄存器(ARR)
TIM_TimeBaseInitStruct.TIM_Prescaler = 72 - 1; //预分频器(PSC)
TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStruct);
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_2; // 指定使用 TIM2 的通道 2 进行输入捕获
TIM_ICInitStructure.TIM_ICFilter = 0x0; //输入捕获滤波器的值为 0
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; //设置捕获边沿为上升沿
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //每个有效边沿都进行捕获
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; //捕获信号直接来自通道 1 的输入
TIM_ICInit(TIM2,&TIM_ICInitStructure);
TIM_SelectInputTrigger(TIM2,TIM_TS_TI2FP2); // 选择 TI2FP2 作为触发源
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; // TIM2 中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
TIM_ITConfig(TIM2,TIM_IT_Update|TIM_IT_CC2,ENABLE);
TIM_Cmd(TIM2,ENABLE);
}
void SR04_Trigger(void)
{
GPIO_SetBits(GPIOA, GPIO_Pin_0); // 发送10us高电平脉冲
Delay_us(12);
GPIO_ResetBits(GPIOA, GPIO_Pin_0);
}
float Get_Distance(void) // 触发超声波模块发射超声波信号的函数
{
SR04_Trigger(); // 发送触发信号
Delay_ms(60); // 等待测量完成
return distance_cm; // 返回计算结果
}
void TIM2_IRQHandler(void) //定时器2的中断服务函数
{
// 处理溢出中断
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
overflow_count++;
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
// 处理捕获中断
if (TIM_GetITStatus(TIM2, TIM_IT_CC2) != RESET)
{
if (capture_stage == 0)
{
// 上升沿捕获
rise_time = TIM_GetCapture2(TIM2);
overflow_count = 0;
// 切换为下降沿捕获
TIM_OC2PolarityConfig(TIM2, TIM_ICPolarity_Falling);
capture_stage = 1;
}
else
{
// 下降沿捕获
fall_time = TIM_GetCapture2(TIM2);
// 计算时间差(单位:微秒)
uint32_t time_diff = (overflow_count * 65536) + (fall_time - rise_time);
// 计算距离(声速340m/s = 0.0343cm/μs,来回距离需除以2)
distance_cm = (time_diff * 0.0343) / 2;
// 切换回上升沿准备下次测量
TIM_OC2PolarityConfig(TIM2, TIM_ICPolarity_Rising);
capture_stage = 0;
}
TIM_ClearITPendingBit(TIM2, TIM_IT_CC2);
}
}
void SR04_Follow(void) //超声波跟随
{
if(Get_Distance() > 30)
{
Forward();
Delay_ms(50);
}
else if(Get_Distance() < 15)
{
Backward();
Delay_ms(50);
}
else
{
AIN1 = 0;
AIN2 = 0;
BIN1 = 0;
BIN2 = 0;
}
}
void SR04_bizhang(void) //超声波避障
{
Delay_ms(500);
TIM_SetCompare1(TIM3,72); //控制舵机朝前
Delay_ms(200);
if(Get_Distance() > 25) //前方无障碍
{
Forward(); //向前运动
Delay_ms(500);
}
else //前方有障碍
{
TIM_SetCompare1(TIM3,42); //控制舵机朝右30°
Delay_ms(200);
if(Get_Distance() > 25) //右前方无障碍
{
TurnRight(); //右转
Delay_ms(700);
}
else //右前方有障碍
{
TIM_SetCompare1(TIM3,102); //控制舵机朝左30°
Delay_ms(200);
if(Get_Distance() > 25) //左前方无障碍
{
TurnLeft(); //左转
Delay_ms(700);
}
else //左前方有障碍
{
Backward(); //后退
Delay_ms(700);
TurnRight(); //右转
Delay_ms(700);
}
}
}
}
五、总结与展望
通过完成基于 STM32 的智能循迹避障小车项目,不仅能巩固理论知识,更能提升将知识转化为实际应用的能力。在这个过程中,从硬件设计、程序编写到系统调试,每一个环节都是对个人技术能力的锻炼。如果你已经学完 STM32,不妨尝试从小车项目入手,迈出嵌入式开发实践的第一步。相信在解决各种实际问题的过程中,你会对 STM32 有更深刻的理解和掌握。