2.4G无线遥控车(无要全求开源)
本文介绍了一个基于STM32和ESP8266的嵌入式无线遥控小车项目。系统分为遥控器和小车两部分,均采用四层板设计,通过UDP透传实现无线通信。遥控器以STM32F103C8T6为主控,配备IP5306电源管理、OLED显示界面,实现了电量监测、低功耗模式和UI交互功能。小车部分采用STM32F103RET6,集成DRV8870电机驱动、编码器测速和PID控制算法,实现四轮闭环控制。项目详细阐述了
前言
刚入坑嵌入式时做的第一个项目与2023年12月份完成,利用STM32为主控,使用ESP8266为WIFI芯片,遥控器和小车控制器使用UDP透传,电路板均采用四层板设计,全面开源,由于是第一个,略微粗糙。(无需点赞,等其要求,直接免费开源所有文件)
百度网盘:
链接:https://pan.baidu.com/s/1ZGycBddl1UqXlp-1A5_QPA?pwd=1234 提取码:1234
摘要
本文介绍了一个基于STM32和ESP8266的嵌入式无线遥控小车项目。系统分为遥控器和小车两部分,均采用四层板设计,通过UDP透传实现无线通信。遥控器以STM32F103C8T6为主控,配备IP5306电源管理、OLED显示界面,实现了电量监测、低功耗模式和UI交互功能。小车部分采用STM32F103RET6,集成DRV8870电机驱动、编码器测速和PID控制算法,实现四轮闭环控制。项目详细阐述了硬件设计、软件架构、通信协议及控制算法,包括电量滤波算法、无线数据收发、电机PID调速等关键代码实现。整个系统已完全开源,可作为嵌入式开发的参考案例。
设计总框图

一、遥控器部分
1.1、硬件部分介绍
- 主控芯片选择:STM32F103C8T6作为核心控制器,介绍其性能及外设资源。
- WiFi模块选型:ESP8266-07的特性,使用UDP透传。
- 电源管理:遥控器部分使用IP5306电源管理芯片,3.7V锂电池供电,Type-C充电。
- 设计图如下:

1.2、硬件原理图



1.3、软件部分

1.1.1、低功耗模式:上电自动进入低功耗模式,OLED屏幕显示时间日期等信息
上电进入低功耗模式代码简单不予以展示
1.1.2、电量转换、及其滤波算法
#include "Power_source.h" // 引入电源相关的头文件,包含必要的定义和宏
uint8_t G_power_status_flag = 0; // 全局变量,表示当前是否处于放电状态(0 为充电,1 为放电)
// 将电压值转换为电量百分比
uint8_t toPercentage(float voltage)
{
// 电压-百分比对照表(充电状态下使用)
const float Battery_Level_Percent_Table[11] = {3.100, 3.550, 3.620, 3.700, 3.750, 3.795, 3.840, 3.90, 3.960, 4.040, 4.120};
// 电压-百分比对照表(放电状态下使用)
const float Electric_current_Percent_Table[11] = {2.0, 1.8, 1.6, 1.4, 1.2, 1.0, 0.8, 0.6, 0.45, 0.3, 0.18};
static float smoothedVoltage = 0.00; // 平滑处理后的电压值(滞后滤波或滑动窗口平均)
static uint8_t smoothedPercentage = 0; // 平滑处理后的电量百分比
static uint8_t currentPercentage = 0; // 当前计算得到的百分比
float scale = 0.2; // 滤波系数,默认值为 0.2
static float window[WINDOW_SIZE] = {0}; // 滑动窗口数组,用于放电状态的平均滤波
static int index = 0; // 滑动窗口的当前位置索引
static float sum = 0; // 滑动窗口的总和,用于计算平均值
if (G_power_status_flag == 0) // 当前为充电状态
{
if (voltage < Battery_Level_Percent_Table[0])
{
return 0; // 电压过低,直接返回 0%
}
if (voltage < 3.55)
{
scale = 0.9; // 电压较低时,提高滞后滤波响应速度
}
// 滞后滤波:用于平滑输入电压
smoothedVoltage = (voltage * scale) + (smoothedVoltage * (1 - scale));
// 根据电压查找对应的百分比
for (uint8_t i = 1; i < ARRAY_DIM(Battery_Level_Percent_Table); i++)
{
if (smoothedVoltage < Battery_Level_Percent_Table[i])
{
// 插值计算百分比
currentPercentage = i * 10 - (10UL * ((float)(Battery_Level_Percent_Table[i] - smoothedVoltage)) /
(float)(Battery_Level_Percent_Table[i] - Battery_Level_Percent_Table[i - 1]));
// 平滑输出百分比,防止抖动
smoothedPercentage = (smoothedPercentage * 3 + currentPercentage) / 4;
return smoothedPercentage;
}
}
return 100; // 电压高于所有阈值,返回 100%
}
else if (G_power_status_flag == 1) // 当前为放电状态
{
scale = 0.2; // 放电时使用较慢的响应速率
smoothedVoltage = (voltage * scale) + (smoothedVoltage * (1 - scale)); // 滞后滤波
sum = sum - window[index] + smoothedVoltage; // 更新滑动窗口的总和
window[index] = smoothedVoltage; // 将当前值写入窗口
index = (index + 1) % WINDOW_SIZE; // 滑动窗口索引递增(循环)
smoothedVoltage = sum / WINDOW_SIZE; // 计算窗口平均值
if ((voltage - smoothedVoltage) < 0.15) // 判断是否电压趋于稳定
{
int8_t i = ARRAY_DIM(Electric_current_Percent_Table); // 从高到低搜索对应百分比
for (i = ARRAY_DIM(Electric_current_Percent_Table); i >= 0; i--)
{
if (smoothedVoltage < Electric_current_Percent_Table[i - 1])
{
// 插值计算百分比
currentPercentage = i * 10 - ((10UL * (float)(smoothedVoltage - Electric_current_Percent_Table[i])) -
(float)(Electric_current_Percent_Table[i - 1] - Electric_current_Percent_Table[i]));
// 平滑处理输出百分比
smoothedPercentage = (smoothedPercentage + currentPercentage) / 2;
return smoothedPercentage;
}
}
if (i <= 0) // 电压过低
{
return 0;
}
return 100; // 电压高于所有放电参考值
}
}
return 0; // 默认返回
}
// 电池电量处理函数,返回包含电量百分比和图标等级的数组指针
uint8_t *Battery_display(void)
{
float Voltage_value = 0, Current_value = 0;
uint8_t percent = 0;
static uint8_t Electrical_data[2]; // 返回数组:[0] 为百分比,[1] 为电池图标等级(0~20)
// 计算电流(单位:V),根据ADC值转换
Current_value = (((float)AD_Value.Current / MAX_AD * R_3V3_V) - R_2V5_V) / coefficient;
// 计算电压(单位:V),根据ADC值转换
Voltage_value = (float)AD_Value.Voltage / MAX_AD * R_3V3_V * R_scale_R;
if (Current_value > 0.2) // 有明显放电电流
{
percent = toPercentage(Current_value); // 基于电流模式计算百分比
Electrical_data[0] = percent;
Electrical_data[1] = percent / 5; // 电池图标等级,每 5% 提升一级
G_power_status_flag = 1; // 切换到放电状态
}
else
{
percent = toPercentage(Voltage_value); // 基于电压模式计算百分比
Electrical_data[0] = percent;
Electrical_data[1] = percent / 5;
G_power_status_flag = 0; // 切换到充电状态
}
return Electrical_data; // 返回指向电量数据的指针
}
1.1.3、充电动画UI、电量显示UI
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "DMA.h"
#include "Power_source.h"
#include "Car_data_handle.h"
#include "USART.h"
#include "MyRTC.h"
#include "OLED_Font.h"
uint8_t UI_flag = 0;
extern uint16_t Data_speed_new;
void OLED_Remote_control_UI()
{
static uint8_t flag = 0;
if (flag == 0)
{
OLED_ShowString_ch(2, 1, 2, 6);
OLED_ShowString(2, 5, "(V):");
OLED_ShowString_ch(3, 1, 2, 4);
OLED_ShowString(3, 5, "(I):");
OLED_ShowString_ch(4, 1, 2, 8);
OLED_ShowString(4, 5, "(W):");
flag = 1;
}
float Voltage = (float)AD_Value.Voltage / 4095 * 6.60;
float Current = (((float)AD_Value.Current / 4095 * 3.300) - 2.545) / 0.4;
OLED_SmallShowNum(2, 9, Voltage, 1, 2);
OLED_SmallShowNum(3, 9, Current, 1, 3);
OLED_SmallShowNum(4, 9, Voltage * Current, 1, 2);
}
void OLED_Car_UI()
{
static uint8_t flag = 0;
if (flag == 0)//进入此函数部分代码只执行一次
{
OLED_ShowString(2, 1, "(V):");
OLED_ShowString(3, 1, "(I):");
OLED_ShowString(4, 1, "(R):");
OLED_show_ui_array(0, 43, 0, 11, 8);//显示 “ms”
OLED_show_ui_array(0, 50, 0, 12, 8);
flag = 1;
}
OLED_SmallShowNum(2, 5, show_data.voltage, 2, 2);//显示小车电压
OLED_SmallShowNum(3, 5, show_data.current, 1, 3);//显示小车电压
OLED_ShowNum(4, 5, show_data.speed-1, 3);//显示小车速度
for (int j = 0; j < 2; j++)
{
OLED_show_ui_array(0, 30 + j * 7, 0, Data_speed_new / OLED_Pow(10, 2 - j - 1) % 10, 8); // 显示无线延迟单位ms
}
}
void Connection_indication_UI(uint8_t number)
{
static uint8_t flag = 0;
static uint8_t TIM_OLED_flag = 0;
static uint8_t speed = 0;
static uint8_t key_flag = 0;
if (number == 0)
{
if(key_flag== 0)//显示时间
{
if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_15) == 0){key_flag=1;}
OLED_ShowNum(2, 6,T.year, 4);
OLED_ShowNum(2, 11, T.mon, 2);
OLED_ShowNum(2, 14,T.day, 2);
OLED_ShowNum(3, 6, T.hour, 2);
OLED_ShowNum(3, 9,T.min, 2);
OLED_ShowNum(3, 12, T.sec, 2);
}
else
{
if(TIM_OLED_flag==0)
{
for(uint8_t i=2;i<6;i++)
{
OLED_SetCursor(i, 0);
for (uint8_t j = 0; j < 128; j++)
{
OLED_WriteData(0x00);
}
}
TIM_OLED_flag=1;
}
OLED_ShowString_ch(3, 3, 4, 10);
}
}
else//连接进度条显示
{
if (flag == 0)
{
OLED_show_ui_array(4, 9, 5, 0, 108);
OLED_SetCursor(5, 32);
for (int i = 0; i < 64; i++)
{
OLED_WriteData(0x00);
}
flag = 1;
}
if (speed < number * 10)
{
speed++;
OLED_show_ui(4, 13, speed, 0xbD);
}
if (speed >= 100)
{
OLED_SetCursor(4, 0);//设置OLED写入位置
for (int i = 0; i < 128; i++)
{
OLED_WriteData(0x00);//对此位置清零
}
UI_flag = 1;//此函数结束执行标志
}
}
}
void OLED_Power_UI(uint8_t Line, uint8_t Column, uint8_t status)//电量UI显示界面
{
int8_t j, k = 2;//确定OLED上显示1位数、两位数、三位数,“避免在三位数下降至两位数后第三位无法清除”
static uint8_t flag = 0;
static uint8_t flag2 = 0;
uint8_t Current_value = 0;
static uint8_t *frequency = NULL;
static uint8_t sum = 0;
static uint8_t delay = 0;
static uint8_t Interlock_flag = 0;
frequency = Battery_display();//指针接收电量转换函数%比地址,获取电量百分比
if (frequency[0] < 10)
{
k -= 1;
OLED_show_ui(0, sum + 8, 4, 0x00);
}
else if (frequency[0] >= 100)
{
k += 1;
}
else
{
k = 2;
OLED_show_ui(0, sum + 8, 4, 0x00);
}
for (j = 0; j < k; j++)
{
OLED_show_ui_array(Line - 1, j * 6 + (Column - 1) * 8 + 28, 0, frequency[0] / OLED_Pow(10, k - j - 1) % 10, 8); // 显示电量
}
//存储现阶段OLED写入位置
sum = j * 6 + (Column - 1) * 8 + 28;
OLED_show_ui_array(Line - 1, j * 6 + (Column - 1) * 8 + 28, 0, 10, 8); // 显示电量“%”
if (flag == 0)//此部分代码只执行一次
{
OLED_show_ui_array(0, 8, 3, 0, 14);//信号图标
OLED_show_ui_array(0, 0, 4, 0, 9);//信号图标
OLED_show_ui_array(Line - 1, (Column - 1) * 8, 1, 0, 27); // 显示电量框
OLED_ShowString(2, 1, "Date:XXXX-XX-");//此处为开机实时时钟显示
OLED_ShowString(3, 1, "Time:XX:XX:");
flag = 1;
}
OLED_show_ui((Line - 1), (Column - 1) * 8 + 5, frequency[1], 0xbd); // 填充电量框
if (Interlock_flag == 0)
{
OLED_show_ui((Line - 1), (Column - 1) * 8 + 5 + frequency[1], 20 - frequency[1], 0x81);// 清除填充电量框填充
}
if (status == 1)
{
if (flag2 == 0)//此部分代码只执行一次
{
OLED_show_ui_array((Line - 1), (Column - 2) * 8 - 2, 2, 0, 8); // 显示 雷电标识
flag2 = 1;
}
//存储电量百分比,确认填充比
Current_value = frequency[1] + (Column - 1) * 8 + 5;
if (delay < (20 - frequency[1]))
{
OLED_SetCursor((Line - 1), Current_value + delay);
OLED_WriteData(0xbd);//对电量框进行填充
delay++;//缓慢依次写入,此函数为定时器分时执行函数执行时间为100ms
Interlock_flag = 1;//清除电量填充标志位,为1则不清除
}
else
{
delay = 0;
Interlock_flag = 0;//清除电量填充标志位,为 0则清除
}
}
else
{
flag2 = 0;
OLED_show_ui((Line - 1), (Column - 2) * 8 - 2, 7, 0x00);//清除雷电标识
}
}
1.1.4、无线收发功能:单片机通过ESP为中介,将AD值,等一系列控制信号发送给小车部分,同时接收并处理小车回传的电流,电压,运行速度,通讯延时,等数据。
#ifndef __USART_H
#define __USART_H
#include <stdio.h>
#include "stm32f10x.h"
#define size 50
void usart_init(void);
// 为发送数据结构体
typedef struct
{
uint8_t start_flag; // 数据帧头,在接收方自动剔除,不接收该数据位,只作为接收起始位
uint8_t WIFI_status; // 唤醒;
uint8_t brake; // 紧急刹车 ;
uint8_t mode; // 循迹遥控模式选择;
uint8_t led; // 开启灯光;
uint8_t car_start; // 小车启动;
uint8_t Data_speed_flag;
signed char F_B_rocker_1; // 左摇杆
signed char L_R_rocker_1; // 左摇杆
signed char F_B_rocker_2; // 右摇杆
signed char L_R_rocker_2; // 右摇杆
uint8_t end_flag;
} sending_cardata;
// 为接收数据结构体
#pragma pack(1) // 配置为一字节对齐
typedef struct
{
// uint8_t start_flag; //数据帧头,在接收方自动剔除,不接收该数据位,只作为接收起始位
uint8_t speed; // 小车速度 ;
uint8_t mode; // 循迹遥控模式选择; //小车电流小车部分;
uint8_t WIFI_status;
uint8_t Data_speed_flag; // 通讯延时标志位
uint16_t voltage; // 小车电池电压;
uint16_t Electric_current; // 小车电流
// uint8_t end_flag; //数据帧尾,在接收方自动剔除,不接收该数据位,只作为接收结束位 //小车电流;
} receiving_car_data;
void send_byte(uint8_t byte); // 发送一个字节
void send_charstring(char *string); // 发送字符串
void send_num(uint8_t length, uint32_t num); // 发送数字()
void send_array(uint8_t length, uint16_t *array); // 发送数组
void send_pack(void); // eps初始化接收缓存
void esp_start(char *send_string, char *receive_string, uint16_t time_ms); // esp发送数据
void esp_init(void); // esp初始化
void send_struct(uint8_t length, sending_cardata *send); // 发送结构体数据
extern uint8_t init_flag;
extern receiving_car_data receiving;
extern char rxdata_packet[size];
extern uint8_t Current_progress;
#endif
#include "stm32f10x.h"
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include "Delay.h"
#include "USART.h"
char rxdata_packet[size]; // 初始化数据接收缓存区
receiving_car_data receiving; // 接收数据结构体
uint8_t init_flag = 0; // 初始化标志位
uint8_t stop_flag = 0; // 等待停止标志位
uint8_t Current_progress = 0; // 进度记录
void usart_init(void)
{
Current_progress++;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 初始化GPIO端口
GPIO_InitTypeDef GPIO_InitStucture;
GPIO_InitStucture.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStucture.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStucture.GPIO_Pin = GPIO_Pin_9;
GPIO_Init(GPIOA, &GPIO_InitStucture);
GPIO_InitStucture.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStucture.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStucture.GPIO_Pin = GPIO_Pin_10;
GPIO_Init(GPIOA, &GPIO_InitStucture);
// 初始化串口
USART_InitTypeDef USART_InitSture;
USART_InitSture.USART_BaudRate = 115200;
USART_InitSture.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitSture.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_InitSture.USART_Parity = USART_Parity_No;
USART_InitSture.USART_StopBits = USART_StopBits_1;
USART_InitSture.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_InitSture);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
// 开启中断
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_Init(&NVIC_InitStructure);
USART_Cmd(USART1, ENABLE);
}
// 发送一个字节
void send_byte(uint8_t byte)
{
USART_SendData(USART1, byte);
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET)
;
}
// 发送字符串
void send_charstring(char *string)
{
uint8_t i;
for (i = 0; string[i] != 0; i++)
{
send_byte(string[i]);
}
}
// 发送数字
void send_num(uint8_t length, uint32_t num)
{
uint8_t i, j;
uint32_t product = 1;
for (i = 0; i < length - 1; i++)
{
product *= 10;
}
for (j = 0; j < length; j++)
{
send_byte(num / product % 10 + 0x30);
product = product / 10;
}
}
// 发送数组
void send_array(uint8_t length, uint16_t *array)
{
uint8_t j;
for (j = 0; j < length; j++)
{
send_byte(array[j]);
}
}
// 发送结构体
void send_struct(uint8_t length, sending_cardata *send) // lengh:结构体数据长度,定义结构体指针
{
uint8_t j;
uint8_t *p = (uint8_t *)&send->start_flag; // 指向结构体首地址,将地址存在指针变量P中
for (j = 0; j < length; j++)
{
send_byte(p[j]); // 依次发送结构体数据
}
}
// 串口中断接收
void USART1_IRQHandler(void)
{
static uint8_t rxstate = 0; // 接收个数计次,为清除数组
static uint8_t i = 0; // 用于地址偏移
uint8_t *p = (uint8_t *)&receiving; // 取出结构体地址放在指针P中
static uint8_t mark = 0; // 结构体接收数据时,是否接收标志
if (init_flag == 0) // 初始化接收
{
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
uint8_t Rxdata = USART_ReceiveData(USART1);
if (i == 0)
{
for (int j = 0; j < rxstate; j++) // 数组清除 (刷新数组)
{
rxdata_packet[j] = 0;
}
rxstate = 0; // 重新计次
}
if (i < size - 5) // 防数组越界
{
if (Rxdata != '\r' && Rxdata != '\n') // 剔除\r\n
{
rxdata_packet[i] = Rxdata; // 开始接收数据 ;
i++;
}
else if (Rxdata == '\n') // 检测到数据发完,开启下轮数据接收
{
i = 0; // 复位,接收下一组数据
}
if (Rxdata == 'C') // 如果收到‘C’停止
{
stop_flag = 1;
}
}
else
{
i = 0;
}
rxstate++; // 记录一轮数据个数
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
if (init_flag == 1) // 初始化完成,开始接收数据
{
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
uint8_t Rxdata = USART_ReceiveData(USART1);
if (i < sizeof(receiving)) // 限制结构体越界
{
if ((mark == 0) && (Rxdata == 0xFF)) // 标志位不存储该数据
{
mark = 1; // 开始接收数据
}
else if ((mark == 1) && (Rxdata != 0xFE))
{
p[i++] = Rxdata; // 开始接收数据
}
else if ((mark == 1) && (Rxdata == 0xFE)) // 结束位
{
i = 0; // 停止接收数据
mark = 0; // 停止接收数据
}
}
else
{
i = 0;
mark = 0;
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
}
void esp_start(char *send_string, char *receive_string, uint16_t time_ms)
{
uint8_t flag = 0; // 循环结束标志位
do
{
send_charstring(send_string); // 发送字符串
Delay_ms(time_ms);
if (strcmp(rxdata_packet, receive_string) == 0) // " rxdata_packet"为ESP回传字符串,“receive_string” 为自定义判断字符串
{
flag = 1; // 则退出循环发送
Current_progress++;
}
else if (strcmp(rxdata_packet, "ERROR") == 0) // 两字符串相等时结果为0
{
strcpy(rxdata_packet, "EEORE"); // 返回错误
break;
}
else
{
flag = 0; // 模式还未开启,继续发送指令
}
} while (flag == 0); // 循环结束标志位
}
void init_complete(void)
{
uint8_t flag = 0;
Delay_ms(200);
do
{
send_charstring("BBBBBBBBB"); // 互相发送数据,透传完成后检验
Delay_ms(200);
if (rxdata_packet[4] == 'C')
{
send_charstring("BBBBBBBBB\r\n");
Delay_ms(200);
break;
}
} while (stop_flag == 0);
flag = 1;
if (flag == 1) // 透传判断
{
Delay_ms(200);
send_charstring("@@@@@@@@@\r\n");
strcpy(rxdata_packet, "complete"); // 透传完成
init_flag = 1; // 开始接收数据,可以发送数据
}
else
{ // 透传失败,返回失败
strcpy(rxdata_packet, "failure");
}
Current_progress++;
Delay_ms(100);
}
void station_completion(void) // 连接WiFi初始化
{
Delay_s(1);
do
{
if (stop_flag == 0)
{
send_charstring("AT+CIPSEND=10\r\n");
Delay_ms(1000);
send_charstring("bbb");
Delay_s(2);
}
} while (stop_flag == 0);
stop_flag = 0;
Current_progress++;
}
void esp_init(void)
{
// 连接WIFI方
usart_init(); // 初始化串口
Delay_ms(300);
esp_start("AT+RESTORE\r\n", "OK", 500); // 对ESP恢复出厂数据
esp_start("AT+CWAUTOCONN=0\r\n", "OK", 100); // 设置开机不自动连接AP
esp_start("AT+SYSSTORE=0\r\n", "OK", 100); // 设置以下设定不保存至FLASH
esp_start("AT+CWMODE=1\r\n", "OK", 100); // 设置ESP为station模式
esp_start("AT+CWJAP=\"AAA\",\"1234567890\"\r\n", "OK", 3000); // '/' 表示转义字符 AT+CWAUTOCONN=<enable>连接AP名,AP密码
esp_start("AT+CIPSTART=\"UDP\",\"192.168.4.1\",9090,8080,0\r\n", "OK", 2000); // 建立UDP透传(为UPD透传,远端IP地址,远端端口号,本地端口号)
station_completion(); // 等待EPS AP发来数据
esp_start("AT+CIPMODE=1\r\n", "OK", 200); // 打开透传
send_charstring("AT+CIPSEND\r\n"); // 打开透传
Current_progress++;
init_complete(); // 等待ESP AP发来数据,EPS连接初始化完毕
}
1.1.5、小车回传电流电压数据解析
void Get__Voltage_Current_value(void)
{
show_data.voltage = (float)(receiving.voltage >> 8) + (float)(receiving.voltage & 0xff) / Voltage_Zoom;
show_data.current= (float)(receiving.Electric_current >> 12) + (float)(receiving.Electric_current & 0xfff) / Current_Zoom;
show_data.speed = receiving.speed;
}
二、小车部分
2.1、硬件部分介绍
- 主控芯片选择:STM32F103RET6作为核心控制器。
- WiFi模块选型:ESP8266-07的特性,使用UDP透传。
- 电源管理:使用 DC-DC 降压芯片降压到5V(RT8279GSP),12V锂电池供电
- 电机驱动:使用TI的 DRV8870DDAR 电机驱动芯片,驱动四电机配合编码器完成速度闭环控制。
2.2、硬件原理图



2.3、软件部分

2.2.1、编码器测速
使用TIM2、TIM3、TIM4、TIM5四个定时器的编码器模式,记录编码器的脉冲值,使用TIM6定时器定时或取脉冲值得到速度,从而实现电机运动控制闭环。
#ifndef __ENCODER_H
#define __ENCODER_H
/********************使用部分********************/
#define motor_left_forward_TIM TIM2
#define motor_left_backward_TIM TIM3
#define motor_right_forward_TIM TIM4
#define motor_right_backward_TIM TIM5
void Encoder_Init(void);
typedef struct
{
unsigned int CR1;
unsigned int CR2;
unsigned int SMCR;
unsigned int DIER;
unsigned int SR;
unsigned int EGR;
unsigned int CCMR1;
unsigned int CCMR2;
unsigned int CCER;
unsigned int CNT;
unsigned int PSC;
unsigned int ARR;
unsigned int null1;
unsigned int CCR1;
unsigned int CCR2;
unsigned int CCR3;
unsigned int CCR4;
unsigned int null2;
unsigned int DCR;
unsigned int DMAR;
} _GenTimStr;
#endif
#include "stm32f10x.h"
#include "Encoder.h"
void Encoder_Init(void)
{
RCC->APB2ENR |= (1 << 0) |(1 << 2) | (1 << 3) ;
RCC->APB1ENR |= (1 << 0) |(1 << 1) |(1 << 2) | (1 << 3);
AFIO->MAPR |=(0x02<<24)|(0x01<<8);
GPIOA->CRH &= (~(0x0f<<28));
GPIOA->CRL &= (~((0xff<<0)|(0xff<<18)));
GPIOB->CRL &= (~((0x0f<<12)|(0xff<<24)));
GPIOA->CRL |= (1 << 2) |(1 << 6) | (1 << 26)|(1 << 30);
GPIOB->CRL |= (1 << 14) |(1 << 30) | (1 << 26);
GPIOA->CRH |= (1 << 30);
motor_left_forward_TIM->CR1 |= (0x00 << 0);
motor_left_forward_TIM->CCMR1 = 0xF1F1;
motor_left_forward_TIM->CCER |=(0x01 << 0)| (0x1<< 4);
motor_left_forward_TIM->DIER |= (uint16_t)0x0001;
motor_left_forward_TIM->SMCR |= (0x03 << 0);
motor_left_forward_TIM->ARR = 65535;
motor_left_forward_TIM->PSC = 0;
motor_left_forward_TIM->CNT = 0;
motor_left_forward_TIM->CR1 |= (0x01 << 0);
*((_GenTimStr *)motor_left_backward_TIM) = *((_GenTimStr *)motor_left_forward_TIM) ;
*((_GenTimStr *)motor_right_forward_TIM) = *((_GenTimStr *)motor_left_forward_TIM);
*((_GenTimStr *)motor_right_backward_TIM) = *((_GenTimStr *)motor_left_forward_TIM);
motor_left_forward_TIM->CR1 |=(0x00 << 0);
motor_left_backward_TIM->CR1 |=(0x00 << 0);
motor_left_forward_TIM->CCER |=(0x01 << 1);
motor_left_backward_TIM->CCER|=(0x01 << 1);
motor_left_forward_TIM->CR1 |= (0x01 << 0);
motor_left_backward_TIM->CR1 |= (0x01 << 0);
}
2.2.2、PID
通过PID控制算法来实现小车的四轮定速,增加闭环控制系统的响应速度,控制精度,以及控制稳定性。
#define Kp 60 //比例、积分、微分系数
#define Ki 0.3
#define Kd 30
typedef struct
{
int16_t target_val; //目标值
int16_t err; //偏差值
int16_t err_last; //上一个偏差值
float integral; //积分值
float output_val; //输出值
} PID_InitDefStruct;
void Velocity_PID(int TargetVelocity,int CurrentVelocity,PID_InitDefStruct *p);
void Clear(PID_InitDefStruct * p);
void Velocity_PID(int TargetVelocity, int CurrentVelocity, PID_InitDefStruct *p)
{
p->err = TargetVelocity - CurrentVelocity;
/*积分项*/
p->integral += p->err;
/*p算法实现*/
p->output_val = Kp * p->err + Ki * p->integral + Kd * (p->err - p->err_last);
/*误差传递*/
p->err_last = p->err;
}
void Clear(PID_InitDefStruct *p)
{
p->err = 0;
p->err_last = 0;
p->integral = 0;
p->output_val = 0;
p->target_val = 0;
}
2.2.3、无线收发功能
单片机通过ESP为中介,接受遥控器发来的指令数据配合其他模块来完成各功能,同时将小车的电流,电压,运行速度,通讯延时,数据等一系列信号发送给遥控器部分。
#include "stm32f10x.h"
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include "Delay.h"
#include "USART.h"
char rxdata_packet[size]; // 初始化数据接收缓存区
receiving_car_data receiving; // 接收数据结构体
uint8_t init_flag = 0; // 初始化标志位
uint8_t stop_flag = 0; // 等待停止标志位
void usart_init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 初始化GPIO端口
GPIO_InitTypeDef GPIO_InitStucture;
GPIO_InitStucture.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStucture.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStucture.GPIO_Pin = GPIO_Pin_2;
GPIO_Init(GPIOA, &GPIO_InitStucture);
GPIO_InitStucture.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStucture.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStucture.GPIO_Pin = GPIO_Pin_3;
GPIO_Init(GPIOA, &GPIO_InitStucture);
// 初始化串口
USART_InitTypeDef USART_InitSture;
USART_InitSture.USART_BaudRate = 115200;
USART_InitSture.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitSture.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_InitSture.USART_Parity = USART_Parity_No;
USART_InitSture.USART_StopBits = USART_StopBits_1;
USART_InitSture.USART_WordLength = USART_WordLength_8b;
USART_Init(Esp_Usart, &USART_InitSture);
USART_ITConfig(Esp_Usart, USART_IT_RXNE, ENABLE);
// 开启中断
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
NVIC_Init(&NVIC_InitStructure);
USART_Cmd(Esp_Usart, ENABLE);
}
// 发送一个字节
void send_byte(uint8_t byte)
{
USART_SendData(Esp_Usart, byte);
while (USART_GetFlagStatus(Esp_Usart, USART_FLAG_TXE) == RESET)
;
}
// 发送字符串
void send_charstring(char *string)
{
uint8_t i;
for (i = 0; string[i] != 0; i++)
{
send_byte(string[i]);
}
}
// 发送数字
void send_num(uint8_t length, uint32_t num)
{
uint8_t i, j;
uint32_t product = 1;
for (i = 0; i < length - 1; i++)
{
product *= 10;
}
for (j = 0; j < length; j++)
{
send_byte(num / product % 10 + 0x30);
product = product / 10;
}
}
// 发送数组
void send_array(uint8_t length, uint16_t *array)
{
uint8_t j;
for (j = 0; j < length; j++)
{
send_byte(array[j]);
}
}
// 发送结构体
void send_struct(uint8_t length, sending_cardata *send)
{
uint8_t j;
uint8_t *p = (uint8_t *)&send->start_flag;
for (j = 0; j < length; j++)
{
send_byte(p[j]);//依次发送结构体数据
}
}
void USART2_IRQHandler(void)
{
static uint8_t rxstate = 0; // 接收个数计次,为清除数组
static uint8_t i = 0; // 用于地址偏移
uint8_t *p = (uint8_t *)&receiving; // 取出结构体地址放在指针P中
static uint8_t mark = 0; // 结构体接收数据时,是否接收标志
if (init_flag == 0) // 初始化接收
{
if (USART_GetITStatus(Esp_Usart, USART_IT_RXNE) == SET)
{
uint8_t Rxdata = USART_ReceiveData(Esp_Usart);
if (i == 0)
{
for (int j = 0; j < rxstate; j++) // 数组清除 (刷新数组)
{
rxdata_packet[j] = 0;
}
rxstate = 0; // 重新计次
}
if (Rxdata == 'B') // 如果收到‘B’停止
{
stop_flag = 1;
}
if (i < size ) // 防数组越界
{
if ((Rxdata != '\r' && Rxdata != '\n')) // 剔除\r\n
{
rxdata_packet[i++] = Rxdata; // 开始接收数据 ;
}
else if (Rxdata == '\n') // 检测到数据发完,开启下轮数据接收
{
i = 0; // 复位,接收下一组数据
}
}
else
{
i = 0;
}
rxstate++; // 记录一轮数据个数
USART_ClearITPendingBit(Esp_Usart, USART_IT_RXNE);
}
}
if (init_flag == 1) // 初始化完成,开始接收数据
{
if (USART_GetITStatus(Esp_Usart, USART_IT_RXNE) == SET)
{
uint8_t Rxdata = Esp_Usart->DR;
if (i < sizeof(receiving)) // 防结构体越界
{
if ((mark == 0) && (Rxdata == 0xFF)) // 标志位
{
mark = 1; // 开始接收数据
}
else if ((mark == 1) && (Rxdata != 0xFE))
{
p[i++] = Rxdata; // 开始接收数据
}
else if ((mark == 1) && (Rxdata == 0xFE)) // 结束位
{
i = 0;
mark = 0; // 停止接收数据
}
}
else
{
i = 0;
mark = 0;
}
USART_ClearITPendingBit(Esp_Usart, USART_IT_RXNE);
}
}
}
void esp_start(char *send_string, char *receive_string, uint16_t time_ms)
{
uint8_t flag = 0;
do
{
send_charstring(send_string); // 发送字符串函数
Delay_ms(time_ms);
if (strcmp(rxdata_packet, receive_string) == 0) // " rxdata_packet"为ESP回传字符串,“receive_string” 为自定义判断字符串
{
flag = 1; // 则退出循环发送
}
else if (strcmp(rxdata_packet, "ERROR") == 0) // 两字符串相等时结果为0
{
strcpy(rxdata_packet, "EEORE"); // 返回错误
}
else
{
flag = 0; // 模式还未开启,继续发送指令
}
} while (flag == 0);
}
void ap_init_complete(void)
{
do
{
send_charstring("CCCCCCC"); // 互相发送数据,透传完成后检验
Delay_ms(200);
if (rxdata_packet[4] == 'B')
{
send_charstring("CCCCCCC");
Delay_ms(200);
break;
}
} while (stop_flag == 0);
init_flag = 1;
send_charstring("CCCCCCC");
Delay_ms(300); // 开始接收数据,可以发送数据
strcpy(rxdata_packet, "complete"); // 透传完成
}
void ap_completion(void)
{
while (USART_GetITStatus(Esp_Usart, USART_IT_RXNE) == RESET)
; // 等待ESP发来数据(验证连接)
}
void esp_init(void)
{
// 建立WIFI方
usart_init();
esp_start("AT+RESTORE\r\n", "OK", 500); // 对ESP恢复出厂数据
esp_start("AT+SYSSTORE=0\r\n", "OK", 100); // 设置以下设定不保存至FLASH
esp_start("AT+CWMODE=2\r\n", "OK", 100); // 设置ESP为AP模式
esp_start("AT+CWSAP=\"AAA\",\"1234567890\",5,3\r\n", "OK", 100); // '/' 表示转义字符 AT+CWAUTOCONN=<enable>建立AP名,AP密码,加密格式
esp_start("AT+CIPSTART=\"UDP\",\"192.168.4.2\",8080,9090,0\r\n", "OK", 2000); // 建立UDP透传(为UPD透传,远端IP地址,远端端口号,本地端口号)
GPIO_SetBits(GPIOC, GPIO_Pin_4); // UDP建立完成,提示station方可以开始建立连接
ap_completion(); // 等待EPS Station发来数据
esp_start("AT+CIPMODE=1\r\n", "OK", 100); // 打开透传
send_charstring("AT+CIPSEND\r\n"); // 打开透传
ap_init_complete(); // 等待ESP Station发来数据,EPS连接初始化完毕
}
void struct_init(sending_cardata *send) // 发送结构体初始化
{
send->start_flag = 0xff;
send->speed = 0;
send->mode = 0;
send->WIFI_status = 0x1;
send->voltage = 0;
send->Electric_current = 0;
send->end_flag = 0xfE;
}
2.2.4、四轮转向控制算法
#include "Car_control.h"
#include "pid.h"
#include "TIME.h"
#include "USART.h"
PID_InitDefStruct F_L_whell; // 左前轮
PID_InitDefStruct B_L_whell; // 左后轮
PID_InitDefStruct F_R_whell; // 右前轮
PID_InitDefStruct B_R_whell; // 左后轮
void PID_Clear()
{
Clear(&F_L_whell);
Clear(&B_L_whell);
Clear(&F_R_whell);
Clear(&B_R_whell);
}
void Car_Stop(uint8_t Wheel_STOP)
{
if (Wheel_STOP == 0) // 熄火
{
TIM1->CCR1 = 0;
TIM1->CCR2 = 0;
TIM1->CCR3 = 0;
TIM1->CCR4 = 0;
TIM8->CCR1 = 0;
TIM8->CCR2 = 0;
TIM8->CCR3 = 0;
TIM8->CCR4 = 0;
}
else // if(Wheel_STOP==1) // 刹车
{
TIM1->CCR1 = 1000;
TIM1->CCR2 = 1000;
TIM1->CCR3 = 1000;
TIM1->CCR4 = 1000;
TIM8->CCR1 = 1000;
TIM8->CCR2 = 1000;
TIM8->CCR3 = 1000;
TIM8->CCR4 = 1000;
}
}
void Car_forward(uint8_t Car_start)
{
uint8_t forward = receiving.F_B_rocker_1;
uint8_t right = receiving.L_R_rocker_2;
uint8_t left = receiving.L_R_rocker_2 * -1;
if (Car_start == 0)
{
PID_Clear();
}
else
{
if ((F_B_1 > 10) && (L_R_2 < 10) && (L_R_2 > -10))
{
Velocity_PID(forward, G_speed1, &F_L_whell);
Velocity_PID(forward, G_speed2, &B_L_whell);
Velocity_PID(forward, G_speed3, &F_R_whell);
Velocity_PID(forward, G_speed4, &B_R_whell);
}
else if (((F_B_1 > 10) || (L_R_2 > 10)) && (L_R_2 > -10))
{
Velocity_PID(forward, G_speed1, &F_L_whell);
Velocity_PID(forward, G_speed2, &B_L_whell);
Velocity_PID(forward - right, G_speed3, &F_R_whell);
Velocity_PID(forward - right, G_speed4, &B_R_whell);
}
else if (((F_B_1 > 10) || (L_R_2 < -10)) && (L_R_2 < 10))
{
Velocity_PID(forward - left, G_speed1, &F_L_whell);
Velocity_PID(forward - left, G_speed2, &B_L_whell);
Velocity_PID(forward, G_speed3, &F_R_whell);
Velocity_PID(forward, G_speed4, &B_R_whell);
}
else
{
PID_Clear();
}
}
TIM1->CCR1 = 0;
TIM1->CCR2 = F_L_whell.output_val;
TIM1->CCR3 = 0;
TIM1->CCR4 = B_L_whell.output_val;
TIM8->CCR1 = F_R_whell.output_val;
TIM8->CCR2 = 0;
TIM8->CCR3 = B_R_whell.output_val;
TIM8->CCR4 = 0;
}
void Car_back(uint8_t Car_start)
{
uint8_t forward = receiving.F_B_rocker_1 * -1;
uint8_t right = receiving.L_R_rocker_2;
uint8_t left = receiving.L_R_rocker_2 * -1;
if (Car_start == 0)
{
PID_Clear();
}
else
{
if ((F_B_1 < -10) && (L_R_2 < 10) && (L_R_2 > -10))
{
Velocity_PID(forward, G_speed1 * -1, &F_L_whell);
Velocity_PID(forward, G_speed2 * -1, &B_L_whell);
Velocity_PID(forward, G_speed3 * -1, &F_R_whell);
Velocity_PID(forward, G_speed4 * -1, &B_R_whell);
}
else if (((F_B_1 < -10) || (L_R_2 > 10)) && (L_R_2 > -10))
{
Velocity_PID(forward, G_speed1 * -1, &F_L_whell);
Velocity_PID(forward, G_speed2 * -1, &B_L_whell);
Velocity_PID(forward - right, G_speed3 * -1, &F_R_whell);
Velocity_PID(forward - right, G_speed4 * -1, &B_R_whell);
}
else if (((F_B_1 < -10) || (L_R_2 < -10)) && (L_R_2 < 10))
{
Velocity_PID(forward - left, G_speed1 * -1, &F_L_whell);
Velocity_PID(forward - left, G_speed2 * -1, &B_L_whell);
Velocity_PID(forward, G_speed3 * -1, &F_R_whell);
Velocity_PID(forward, G_speed4 * -1, &B_R_whell);
}
else
{
PID_Clear();
}
}
TIM1->CCR1 = F_L_whell.output_val;
TIM1->CCR2 = 0;
TIM1->CCR3 = B_L_whell.output_val;
TIM1->CCR4 = 0;
TIM8->CCR1 = 0;
TIM8->CCR2 = F_R_whell.output_val;
TIM8->CCR3 = 0;
TIM8->CCR4 = B_R_whell.output_val;
}
void Car_Left_Turn(uint8_t Car_start)
{
uint8_t left_1 = receiving.L_R_rocker_2 * -1;
if (Car_start == 0)
{
PID_Clear();
}
else
{
if ((F_B_1 < 10) && (F_B_1 > -10) && (L_R_2 < -10))
{
Velocity_PID(left_1, G_speed3, &F_R_whell);
Velocity_PID(left_1, G_speed4, &B_R_whell);
}
else
{
PID_Clear();
}
}
TIM1->CCR1 = 1000;
TIM1->CCR2 = 1000;
TIM1->CCR3 = 1000;
TIM1->CCR4 = 1000;
TIM8->CCR1 = F_R_whell.output_val;
TIM8->CCR3 = B_R_whell.output_val;
}
void Car_Right_Turn(uint8_t Car_start)
{
uint8_t right_1 = receiving.L_R_rocker_2;
if (Car_start == 0)
{
PID_Clear();
}
else
{
if ((F_B_1 < 10) && (F_B_1 > -10) && (L_R_2 > 10))
{
Velocity_PID(right_1, G_speed1, &F_L_whell);
Velocity_PID(right_1, G_speed2, &B_L_whell);
}
else
{
PID_Clear();
}
}
TIM1->CCR2 = F_L_whell.output_val;
TIM1->CCR4 = B_L_whell.output_val;
TIM8->CCR1 = 1000;
TIM8->CCR2 = 1000;
TIM8->CCR3 = 1000;
TIM8->CCR4 = 1000;
}
void Car_Clockwise_Rotate(uint8_t Car_start)
{
uint8_t right_1 = receiving.L_R_rocker_1;
if (Car_start == 0)
{
PID_Clear();
}
else
{
if ((receiving.mode == 1) && (L_R_1 > 10))
{
Velocity_PID(right_1, G_speed1, &F_L_whell);
Velocity_PID(right_1, G_speed2, &B_L_whell);
Velocity_PID(right_1, G_speed3 * -1, &F_R_whell);
Velocity_PID(right_1, G_speed4 * -1, &B_R_whell);
}
else
{
PID_Clear();
}
}
TIM1->CCR2 = F_L_whell.output_val;
TIM1->CCR4 = B_L_whell.output_val;
TIM8->CCR2 = F_R_whell.output_val;
TIM8->CCR4 = B_R_whell.output_val;
}
void Car_Anticlockwise_Rotate(uint8_t Car_start)
{
uint8_t left_1 = receiving.L_R_rocker_1 * -1;
if (Car_start == 0)
{
PID_Clear();
}
else
{
if ((receiving.mode == 1) && (L_R_1 < -10))
{
Velocity_PID(left_1, G_speed1 * -1, &F_L_whell);
Velocity_PID(left_1, G_speed2 * -1, &B_L_whell);
Velocity_PID(left_1, G_speed3, &F_R_whell);
Velocity_PID(left_1, G_speed4, &B_R_whell);
}
else
{
PID_Clear();
}
}
TIM1->CCR1 = F_L_whell.output_val;
TIM1->CCR3 = B_L_whell.output_val;
TIM8->CCR1 = F_R_whell.output_val;
TIM8->CCR3 = B_R_whell.output_val;
}
(注:可根据实际项目需求调整章节深度,例如添加PCB设计、3D打印车体等内容。)
作品视屏展示
STM32_2.4GWIFi无线遥控车—全部免费开源-开源文件在简介下面(stm32,esp8266,差速转向,PID调速,动画UI,PCB,焊接)_哔哩哔哩_bilibili
ESP透传教学视频:
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐

所有评论(0)