前言

        刚入坑嵌入式时做的第一个项目与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透传教学视频:

两esp8266双向透传(不使用PC建立服务器)_哔哩哔哩_bilibili

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐