基于STM32+FreeRTOS的四轴机械臂

目录

代码:

注释写的较少,但本文出现的代码都有注释,所以请直接在本文里看注释

项目概述:

[一 准备阶段(都是些废话)](#一 准备阶段(都是些废话))

[二 裸机测试功能](#二 裸机测试功能)

1.摇杆控制

接线:

CubeMX配置:

代码

2.蓝牙控制

接线:

CubeMX配置

代码:

3.示教器控制

4.记录动作信息

5.执行记录的动作

注:

[三 FreeRTOS上完成项目](#三 FreeRTOS上完成项目)

[1.加入IIC的OLED屏显 和 动作执行(数组版)](#1.加入IIC的OLED屏显 和 动作执行(数组版))

CubeMX配置:

代码:

2.链表的增删遍历实现动作记忆和执行

3.SPI扩容

功能测试

w25q128芯片分析与功能实现


代码:

注释写的较少,但本文出现的代码都有注释,所以请直接在本文里看注释

链接:https://pan.baidu.com/s/14GJF8ZCnkNkKkz5R0uJwOA?pwd=1111

提取码:1111

--来自百度网盘超级会员V4的分享

项目概述:

基于STM32的FreeRTOS四轴机械臂

基于FreeRTOS实时操作系统,主控为 STM32F103C8T6 ,机械臂为四轴分别被四个Mg90s舵机控制。本项目实现了 3 种控制方法,分别为 摇杆控制 、 串口蓝牙控制 和 示教器控制。可以进行动作录制和执行。

采用8路ADC采集摇杆和示教器的模拟量并由DMA搬运数据,USART串口实时收发信息,IIC驱动OLED屏幕实时显示信息。并且实现了动作录制和执行功能,动作记忆可以由二维数组或者链表实现存储。通过SPI驱动W25Q128模块进行动作记忆扩容,即可以录制上百组动作。

一 准备阶段(都是些废话)

首先你需要一台四轴机械臂,才能开始这个项目。可以自己建模3D打印,也可以直接某宝购买了一套成品套件,来做功能实现。而你的机械臂会配备四个电机,本文采用的是舵机,型号无所谓,控制起来是一样的,注意需要是180度的角度型舵机,而不是360度的速度型舵机。

然后是单片机及开发环境,使用STM32F103C8T6。开发环境为STM32cubeM和Keil5。

如果你没有STM32开发经验:首先你至少要有一点C语言基础,最基本的代码要能读懂什么意思;然后最好有过其它单片机开发经验,比如C51、ESP8266等等,或者直接学习一下STM32开发。板子随便买一个此型号的开发板就行,买最小系统板+面包板也可以。STM32cubeMX+Keil5,可以自行百度搜索并下载安装,我建议在B站找一个STM32HAL库的教程,跟着安装,且最好教程芯片型号与你使用的要一致。按照教程走一遍。确认开发板和开发环境可用之后,简单学习一下HAL库开发。然后可以继续下面的步骤。)

STM32cubeM和Keil5的教程推荐:

【中科大RM电控合集】手把手Keil+STM32CubeMX+VsCode环境配置_哔哩哔哩_bilibili

其它硬件准备:

|------------------------------------|
| HC系列蓝牙串口模块,实测HC-05和HC-08都可以 |
| 摇杆模块,买两个即可。 |
| 四个旋钮电位器,质量别太差。 |
| IIC协议OLED屏幕 |
| SPI协议W25Q128模块 |
| 按钮模块若干,我用了四个,有板载的按钮也可以,尽量买带电容的防抖按钮 |
| 舵机拓展板,可有可无,面包板也能用。 |
| 各式杜邦线若干。 |

二 裸机测试功能

1.摇杆控制

首先是摇杆控制STM32,需要4路1ADC+DMA采集摇杆输出的模拟量。根据这个数据来控制舵机角度。蓝牙串口把ADC信息和舵机角度打印出来。蓝牙直接用HC官方的HC蓝牙串口助手就行。

接线:

摇杆4个输出模拟量的引脚连接stm32的A0,A1,A2,A3,VCC这里接5V。

舵机A,夹爪 CH4_B11;adc4_A3

舵机B,上下 CH3_B10;adc3_A2

舵机C,前后 CH2_B3;adc2_A1

舵机D,底座 CH1_A15;adc1_A0

蓝牙TX对板子RX A10,

蓝牙RX对板子TX A9。

CubeMX配置:

基本配置(后面每个工程都是这一套)

ADC1:4路

DMA:搬运ADC数据的

PWM输出:选用799*1799,这样可以把舵机有效的 0.5~2.5ms / 20ms 这个区间分成180段,对应0~180度。

usart,9600波特率给蓝牙模块用。

然后 generate code 即可

代码:

注:只有这种注释之间是用户自己写业务代码的地方,写其它地方再重生成功能会被清除。

cpp 复制代码
/* USER CODE BEGIN */

    。。。。 。。。。

/* USER CODE END */

main.c

关键控制代码在于check的四个函数,首先限制舵机的角度范围避免损坏,再根据采集的摇杆信息值判断每个舵机的角度是增加还是减小。

注释比较清楚,直接看代码就行。

cpp 复制代码
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "stdio.h"
/* USER CODE END Includes */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */
uint16_t adc_dma[4];//DMA搬运的ADC采集值

uint8_t angle[4] = {90,90,90,90};//舵机角度

uint8_t cnt = 0;//计数用,定时串口打印信息

/* USER CODE END PV */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
//覆写printf,用于串口打印数据
int fputc(int ch, FILE *f)
{      
    unsigned char temp[1]={ch};
    HAL_UART_Transmit(&huart1,temp,1,0xffff);  
    return ch;
}

//根据输入的0~180角度获取对应pwm占空比参数
uint8_t Angle(uint8_t pwm_pulse)
{
	return pwm_pulse + 44;
}

//舵机A,夹爪	CH4_B11
void cheack_A()
{
	if(adc_dma[3] > 4000 && angle[3] < 90)
	{
		angle[3]++;
	}
	else if(adc_dma[3] <1000 && angle[3] > 0)
	{
		angle[3]--;
	}
}
//舵机B,上下	CH3_B10
void cheack_B()
{
	if(adc_dma[2] <1000 && angle[2] < 135)
	{
		angle[2]++;
	}
	else if(adc_dma[2] > 4000 && angle[2] > 45)
	{
		angle[2]--;
	}
}
//舵机C,前后	CH2_B3
void cheack_C()
{
	if(adc_dma[1] <1000 && angle[1] < 135)
	{
		angle[1]++;
	}
	else if(adc_dma[1] > 4000 && angle[1] > 45)
	{
		angle[1]--;
	}
}
//舵机D,底座	CH1_A15
void cheack_D()
{
	if(adc_dma[0] <1000 && angle[0] < 180)
	{
		angle[0]++;
	}
	else if(adc_dma[0] > 4000 && angle[0] > 0)
	{
		angle[0]--;
	}
}

/* USER CODE END 0 */


  /* USER CODE BEGIN 2 */
	HAL_ADC_Start_DMA(&hadc1,(uint32_t *)adc_dma,4); //开始ADC和DMA采集
	
	//开启4路PWM
	HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
	HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);
	HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3);
	HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_4);
	
	//延时半秒,系统稳定一下
	HAL_Delay(500);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
		//根据摇杆DMA判断舵机该如何运动
		cheack_A();
		cheack_B();
		cheack_C();
		cheack_D();
		//输出PWM波使舵机运动
		__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_1, Angle(angle[0]));
		__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_2, Angle(angle[1]));
		__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_3, Angle(angle[2]));
		__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_4, Angle(angle[3]));
		
		cnt++;//计数,每循环一次+1
		if(cnt>= 50)//循环50次,每次20ms,即一共1s。每一秒发送一次数据
		{
			printf("Angle = {%d, %d, %d, %d}\r\n",angle[0],angle[1],angle[2],angle[3]);
			cnt = 0;
		}
		HAL_Delay(20);//每20ms循环一次(改成15更流畅)

  }
  /* USER CODE END 3 */

这里要勾选才能使用printf串口打印信息

2.蓝牙控制

这里提前写了一点示教器的业务代码,执行切换模式操作会切换获取摇杆模拟值还是电位器模拟值。

注意:我这里整活儿搞了个ADC通道切换,但实测还是存在一点问题,你们直接使用8通道一起就好。

接线:

先不使用示教器,但是可以先测试一下功能,切换模式和采集一下数据。

把四个旋钮电位器接好,四根线接到ADC 5 6 7 8

CubeMX配置

打开串口中断,中断接收数据。

我这里是ADC再开四个,其它不配置。

你们开启共8个之后把下面个数也4改成8,新的组别5678也改成IN4 5 6 7 四个通道

代码:

都写在main.c里面会太冗长,我这里分文件编程了,不懂可以百度keil怎么添加.c .h文件,实在不行就都放在main.c里吧。。。

adc.c

纯粹整活儿,自定义了一个ADC初始化,把采集1234换成5678来采集电位器信号。直接用八个通道一起采集就行,然后把原来放采集数据的那个数组adc_dma长度也改成8。

cpp 复制代码
/* USER CODE BEGIN 1 */
//写一个切换通道的函数
/* ADC1_Mode2 init function */
void MX_ADC1_Mode2_Init(void)
{
  ADC_ChannelConfTypeDef sConfig = {0};
  /** Common config
  */
  hadc1.Instance = ADC1;
  hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE;
  hadc1.Init.ContinuousConvMode = ENABLE;
  hadc1.Init.DiscontinuousConvMode = DISABLE;
  hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
  hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
  hadc1.Init.NbrOfConversion = 4;
  if (HAL_ADC_Init(&hadc1) != HAL_OK)
  {
    Error_Handler();
  }

  /** Configure Regular Channel
  */
  sConfig.Channel = ADC_CHANNEL_4;
  sConfig.Rank = ADC_REGULAR_RANK_1;
  sConfig.SamplingTime = ADC_SAMPLETIME_1CYCLE_5;
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }

  /** Configure Regular Channel
  */
  sConfig.Channel = ADC_CHANNEL_5;
  sConfig.Rank = ADC_REGULAR_RANK_2;
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }

  /** Configure Regular Channel
  */
  sConfig.Channel = ADC_CHANNEL_6;
  sConfig.Rank = ADC_REGULAR_RANK_3;
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }

  /** Configure Regular Channel
  */
  sConfig.Channel = ADC_CHANNEL_7;
  sConfig.Rank = ADC_REGULAR_RANK_4;
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }
}
/* USER CODE END 1 */

adc.h

cpp 复制代码
/* USER CODE BEGIN Prototypes */
void MX_ADC1_Mode2_Init(void);
/* USER CODE END Prototypes */

usart.c

注:STM32串口接收到的信息都在这里进行处理,千万别忘了最下面一行代码,开启中断。

cpp 复制代码
	//=======中断信息处理=======

        。。。。。。。。        
		
	//==========================
cpp 复制代码
/* USER CODE BEGIN 0 */
#include "stdio.h"
#include "string.h"
#include "PWM.h"

#include "adc.h"
#include "dma.h"

/*机械臂控制模式,默认为1
1:摇杆控制	
2:示教器控制	*/
uint8_t Mode = 1;

/*蓝牙控制机械臂指令:
s		停
l/r 左右
u/d 上下
f/b 前后
o/c 开合*/
uint8_t cmd_BLE = 's';

extern uint16_t adc_dma[4];//DMA搬运的ADC采集值

//覆写printf
int fputc(int ch, FILE *f)
{      
    unsigned char temp[1]={ch};
    HAL_UART_Transmit(&huart1,temp,1,0xffff);  
    return ch;
}

//=====串口(中断)=======
//串口接收缓存(1字节)
uint8_t buf=0;
//定义最大接收字节数 200,可根据需求调整
#define UART1_REC_LEN 200
// 接收缓冲, 串口接收到的数据放在这个数组里,最大UART1_REC_LEN个字节
uint8_t UART1_RX_Buffer[UART1_REC_LEN];
//  接收状态
//  bit15,      接收完成标志
//  bit14,      接收到0x0d
//  bit13~0,    接收到的有效字节数目
uint16_t UART1_RX_STA=0;

// 串口中断:接收完成回调函数,收到一个数据后,在这里处理
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	// 判断中断是由哪个串口触发的
	if(huart->Instance == USART1)
	{
		// 判断接收是否完成(UART1_RX_STA bit15 位是否为1)
		if((UART1_RX_STA & 0x8000) == 0)
		{
			// 如果已经收到了 0x0d (回车),
			if(UART1_RX_STA & 0x4000)
			{
				// 则接着判断是否收到 0x0a (换行)
				if(buf == 0x0a)
				{
					// 如果 0x0a 和 0x0d 都收到,则将 bit15 位置为1
					UART1_RX_STA |= 0x8000;
					
					//=======中断信息处理=======
					//模式切换
					if (!strcmp((const char *)UART1_RX_Buffer, "M1")) 
					{
						Mode = 1;
						HAL_ADC_Stop_DMA(&hadc1);//停止ADC DMA
						MX_ADC1_Init();//初始化ADC1
						HAL_ADC_Start_DMA(&hadc1,(uint32_t *)adc_dma,4); //开启ADC DMA
						printf("摇杆模式\r\n");
					}
					else if(!strcmp((const char *)UART1_RX_Buffer, "M2")) 
					{
						Mode = 2;
						HAL_ADC_Stop_DMA(&hadc1);//停止ADC DMA
						MX_ADC1_Mode2_Init();//自定义初始化ADC1,把1234换成5678采集电位器
						HAL_ADC_Start_DMA(&hadc1,(uint32_t *)adc_dma,4); //开启ADC DMA
						printf("示教器模式\r\n");
					}

					//获取蓝牙控制指令,A打头,后面一个字母就是指令内容
					else if(Mode == 1 && UART1_RX_Buffer[0] == 'A')
					{
						cmd_BLE = UART1_RX_Buffer[1];
					}
					
					else {
						if(UART1_RX_Buffer[0] != '\0')
							printf("指令发送错误:%s\r\n", UART1_RX_Buffer);
					}
					
					//==========================
					memset(UART1_RX_Buffer, 0, strlen((const char *)UART1_RX_Buffer));
			 
						// 重新开始下一次接收
					UART1_RX_STA = 0;
					//==========================
				}
				else
					// 否则认为接收错误,重新开始
					UART1_RX_STA = 0;
			}
			else	// 如果没有收到了 0x0d (回车)
			{
				//则先判断收到的这个字符是否是 0x0d (回车)
				if(buf == 0x0d)
				{
					// 是的话则将 bit14 位置为1
					UART1_RX_STA |= 0x4000;
				}
				else
				{
					// 否则将接收到的数据保存在缓存数组里
					UART1_RX_Buffer[UART1_RX_STA & 0X3FFF] = buf;
					UART1_RX_STA++;
					
					// 如果接收数据大于UART1_REC_LEN(200字节),则重新开始接收
					if(UART1_RX_STA > UART1_REC_LEN - 1)
						UART1_RX_STA = 0;
				}
			}
		}
		// 重新开启中断
		HAL_UART_Receive_IT(&huart1, &buf, 1);
	}
}

/* USER CODE END 0 */


  /* USER CODE BEGIN USART1_Init 2 */
	// 开启接收中断
	HAL_UART_Receive_IT(&huart1, &buf, 1);
  /* USER CODE END USART1_Init 2 */

我这里新建了两个PWM.c和.h文件。

把蓝牙指令控制和摇杆控制放在一起判断了。

cpp 复制代码
#include "PWM.h"
#include "main.h"

extern uint16_t adc_dma[4];//DMA搬运的ADC采集值
extern uint8_t angle[4];//舵机角度
extern uint8_t Mode;
extern uint8_t cmd_BLE;

//根据输入的角度获取对应pwm占空比参数
uint8_t Angle(uint8_t pwm_pulse)
{
	return pwm_pulse + 44;
}

//舵机A,夹爪	CH4_B11
void check_A()
{
	if(Mode == 1)
	{
		if((cmd_BLE == 'c' || adc_dma[3] > 4000) && angle[3] < 90)//合
		{
			angle[3]++;
		}
		else if((cmd_BLE == 'o' || adc_dma[3] <1000) && angle[3] > 0)//开
		{
			angle[3]--;
		}
	}

}

//舵机B,上下	CH3_B10
void check_B()
{
	if(Mode == 1)
	{
		if((cmd_BLE == 'u' || adc_dma[2] <1000) && angle[2] < 135)//上
		{
			angle[2]++;
		}
		else if((cmd_BLE == 'd' || adc_dma[2] > 4000) && angle[2] > 45)//下
		{
			angle[2]--;
		}
	}

}

//舵机C,前后	CH2_B3
void check_C()
{
	if(Mode == 1)
	{
		if((cmd_BLE == 'f' || adc_dma[1] <1000) && angle[1] < 135)//前
		{
			angle[1]++;
		}
		else if((cmd_BLE == 'b' || adc_dma[1] > 4000) && angle[1] > 45)//后
		{
			angle[1]--;
		}
	}

}
//舵机D,底座	CH1_A15
void check_D()
{
	if(Mode == 1)
	{
		if((cmd_BLE == 'l' || adc_dma[0] <1000) && angle[0] < 180)//左
		{
			angle[0]++;
		}
		else if((cmd_BLE == 'r' || adc_dma[0] > 4000) && angle[0] > 0)//右
		{
			angle[0]--;
		}
	}

}
cpp 复制代码
#ifndef __PWM_H__
#define __PWM_H__
 
//根据输入的角度获取对应pwm占空比参数
unsigned char Angle(unsigned char pwm_pulse);

//舵机A,夹爪	CH4_B11
void check_A(void);

//舵机B,上下	CH3_B10
void check_B(void);

//舵机C,前后	CH2_B3
void check_C(void);

//舵机D,底座	CH1_A15
void check_D(void);

#endif

main.c

cpp 复制代码
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "stdio.h"
#include "PWM.h"
/* USER CODE END Includes */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */
uint16_t adc_dma[4];//DMA搬运的ADC采集值

uint8_t angle[4] = {90,90,90,90};//舵机角度

uint8_t cnt = 0;//计数用

/* USER CODE END PV */

  /* USER CODE BEGIN 2 */
	
	printf("Start\r\n");
	
	HAL_ADC_Start_DMA(&hadc1,(uint32_t *)adc_dma,4); 
	
	HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
	HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);
	HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3);
	HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_4);
	
	HAL_Delay(500);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
		//根据摇杆DMA判断舵机该如何运动
		check_A();
		check_B();
		check_C();
		check_D();
		//输出PWM波使舵机运动
		__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_1, Angle(angle[0]));
		__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_2, Angle(angle[1]));
		__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_3, Angle(angle[2]));
		__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_4, Angle(angle[3]));
		
		cnt++;
		if(cnt>= 50)
		{
			printf("Angle = {%d, %d, %d, %d}\r\n",angle[0],angle[1],angle[2],angle[3]);
			cnt = 0;
		}
		HAL_Delay(20);

  }
  /* USER CODE END 3 */

3.示教器控制

把示教器控制的业务代码也写出来,和蓝牙/摇杆控制封装在一个函数里,main里直接调用这个函数就行。

主要是PWM.c添加了一些代码,直接修改上面代码即可。

cpp 复制代码
extern uint16_t adc_dma[4];//DMA搬运的ADC采集值,直接用8通道就改长度8
extern uint8_t angle[4];//舵机角度
extern uint8_t Mode;
extern uint8_t cmd_BLE;


//根据输入的角度获取对应pwm占空比参数
uint8_t Angle(uint8_t pwm_pulse)
{
	return pwm_pulse + 44;
}
//舵机角度如何变化和模式判断的函数
void sg()
{
	if(Mode == 1)//蓝牙/摇杆模式
	{
		check_A();
		check_B();
		check_C();
		check_D();
		
	}
	else if(Mode == 2)//示教器模式
	{
		translate();
	}
	
		//输出PWM波使舵机运动
	__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_1, Angle(angle[0]));
	__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_2, Angle(angle[1]));
	__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_3, Angle(angle[2]));
	__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_4, Angle(angle[3]));
	HAL_Delay(20);
}

void translate()//把采集的模拟值转变为角度。即0~4095变为0~180,除以22.75即可。
{
	angle[3] = (uint8_t)((double)adc_dma[0] / 22.75)/2;
	angle[2] = (uint8_t)((double)adc_dma[1] / 22.75);
	angle[1] = (uint8_t)((double)adc_dma[2] / 22.75) - 10;
	angle[0] = 180 - (uint8_t)((double)adc_dma[3] / 22.75);//电位器装反,改为 180 - 即可
	//直接用8通道就是adc_dma[4~7]
}

PWM.h

cpp 复制代码
#ifndef __PWM_H__
#define __PWM_H__
 
//根据输入的角度获取对应pwm占空比参数
unsigned char Angle(unsigned char pwm_pulse);

//舵机A,夹爪	CH4_B11
void check_A(void);

//舵机B,上下	CH3_B10
void check_B(void);

//舵机C,前后	CH2_B3
void check_C(void);

//舵机D,底座	CH1_A15
void check_D(void);

void sg(void);

void translate(void);

#endif

main.c

cpp 复制代码
  /* USER CODE BEGIN 2 */
	printf("Start\r\n");
	
	HAL_ADC_Start_DMA(&hadc1,(uint32_t *)adc_dma,4); 
	
	HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
	HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);
	HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3);
	HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_4);
	
	HAL_Delay(500);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
		
		sg();//判断舵机该如何运动

		cnt++;
		if(cnt>= 25)
		{
			 printf("Angle = {%d, %d, %d, %d}\r\n",angle[0],angle[1],angle[2],angle[3]);
			 //printf("adc_dma = {%d, %d, %d, %d}\r\n",adc_dma[0],adc_dma[1],adc_dma[2],adc_dma[3]);
			cnt = 0;
		}
  }
  /* USER CODE END 3 */

至此,基本的控制功能代码已经完成了。小白的话完成到这里已经很不错了。

4.记录动作信息

本质上就是保存当前的舵机的四个角度值。

这里暂时先用二维数组来做。

被添加的代码:

蓝牙指令A后的 m g D ,对应我们这里 记录当前角度、获取所有记录的角度、删除所有记录。

cpp 复制代码
#include "stdio.h"
#include "string.h"

uint8_t memory[10][4];//记录用的数组
uint8_t i,j = 0;

void sg()
{
	if(Mode == 1)
	{
		check_A();
		check_B();
		check_C();
		check_D();

	}
	else if(Mode == 2)
	{
		translate();
		
		if(cmd_BLE == 'm' && i<9)
		{
			for(j=0;j<4;j++)
			{
				memory[i][j] = angle[j];
			}
			printf("储存动作\r\n");
			cmd_BLE = 's';
			i++;
		}
		else if(cmd_BLE == 'm' && i>=9)
			printf("动作已满\r\n");
			cmd_BLE = 's';
	}
	if(cmd_BLE == 'g')
	{
		for(i=0;i<10;i++)
		{
			for(j=0;j<4;j++)
			{
				printf("%d ",memory[i][j] + 0x30);
			}
			printf("\r\n");
			if(memory[i][j] == '\0')	break;
		}
		cmd_BLE = 's';
	}
	else if(cmd_BLE == 'D')
	{
		for(i=0;i<10;i++)
		{
			memset(memory[i],'\0',4);
		}
		i = 0;
		printf("已清除动作");
		cmd_BLE = 's';
	}
		//输出PWM波使舵机运动
	__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_1, Angle(angle[0]));
	__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_2, Angle(angle[1]));
	__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_3, Angle(angle[2]));
	__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_4, Angle(angle[3]));
	

	HAL_Delay(20);
}

5.执行记录的动作

这里开始已经转到FreeRTOS上了,没继续在裸机上做。所以没写对应源码,不过可以拿后面FreeRTOS上实现的代码放在这里。没区别一样可以用。需要你们自己来实现和调试。

PWM.c

主要就是下面这两个函数:

location_cnt是数组长度,宏定义出来就行,自己调整长度

cpp 复制代码
uint8_t memory[location_cnt][4];
uint8_t i,j = 0;

uint8_t angle_target[4] = {90,90,90,90};
uint8_t angle_target_flag = 0;


void get_target()//从数组获得位置信息并转换位角度目标值
{
	angle_target_flag = 0;
	
	for(j=0;j<4;j++)
	{
		if(angle[j] == angle_target[j])	angle_target_flag++;
	}
	
	if(angle_target_flag == 4)	i++;
	
	
	for(j=0;j<4;j++)
	{
		if(memory[i][j] == '\0')
		{
			i = 0;
		}
		angle_target[j] = memory[i][j];
	}
}

void reach_target()//角度值像角度目标值靠近,用于简单防抖和执行记忆动作
{
	for(j = 0;j <4;j++)
	{
		if(angle[j] > angle_target[j])
		{
			angle[j]--;
		}
		else if(angle[j] < angle_target[j])
		{
			angle[j]++;
		}
	}
}

void translate()//根据实际情况做了一点角度矫正和限位
{
	angle_target[3] = (uint8_t)((double)adc_dma[4] / 22.75)/2;
	angle_target[2] = (uint8_t)((double)adc_dma[5] / 22.75);
	angle_target[1] = (uint8_t)((double)adc_dma[6] / 22.75) - 10;
	angle_target[0] = 180 - (uint8_t)((double)adc_dma[7] / 22.75);
	
	if(angle_target[1]<45)	angle_target[1]=45;
	else if(angle_target[1]>135)	angle_target[1]=135;
	
	if(angle_target[2]<45)	angle_target[1]=45;
	else if(angle_target[2]>135)	angle_target[1]=135;
}

//是否记录当前位置信息
void if_BLE_cmd()
{
	switch(cmd_BLE)
	{
		case 'm':
			
			if(i < location_cnt)
			{
				for(j=0;j<4;j++)
				{
					memory[i][j] = angle[j];
				}
				printf("储存动作\r\n");
				cmd_BLE = 's';
				i++;
			}
			else
			{
				printf("动作已满\r\n");
				cmd_BLE = 's';
			}
		break;
		
		case 'g':
			
			for(i=0;i < location_cnt;i++)
			{
				for(j=0;j<4;j++)
				{
					printf("%d ",memory[i][j]);
				}
				printf("\r\n");
				if(memory[i][j] == '\0')	break;
			}
			cmd_BLE = 's';
		break;
				
		case 'D':
			
			for(i=0; i < location_cnt ;i++)
			{
				memset(memory[i],'\0',4);
			}
			i = 0;
			printf("已清除动作");
			cmd_BLE = 's';
		break;
	}
}

void check_sg_cmd()//蓝牙和摇杆控制
{
	check_A();
	check_B();
	check_C();
	check_D();
}

usart.c

cpp 复制代码
/*机械臂控制模式,默认为1
1:摇杆控制	
2:示教器控制
3:执行记忆动作
*/
uint8_t Mode = 1;





					//=======中断信息处理=======
					//模式切换
					if (!strcmp((const char *)UART1_RX_Buffer, "M1")) 
					{			
						Mode = 1;

						printf("摇杆模式\r\n");

					}
					else if(!strcmp((const char *)UART1_RX_Buffer, "M2")) 
					{			
						Mode = 2;

						printf("示教模式\r\n");
						
					}
					else if(!strcmp((const char *)UART1_RX_Buffer, "M3")) 
					{			
						Mode = 3;

						printf("执行记忆动作\r\n");
						
					}

freertos.c内相关代码

和main.c的while循环一样理解就行,一样用

cpp 复制代码
  /* Infinite loop */
  for(;;)
  {
		if(Mode == 1)//摇杆和蓝牙控制
		{
			check_sg_cmd();
		}
		else if(Mode == 2)//示教器控制
		{
			translate();
			reach_target();
		}
		else if(Mode == 3)//动作执行
		{
			get_target();
			reach_target();
		}
		
		if_BLE_cmd();//蓝牙指令处理
		
		//输出PWM波使舵机运动
		__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_1, Angle(angle[0]));
		__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_2, Angle(angle[1]));
		__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_3, Angle(angle[2]));
		__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_4, Angle(angle[3]));
		
    osDelay(15);//通过调整此延时可以改变机械臂运行速度
  }

注:

裸机开发到这里就结束了,大部分功能都简单实现出来了。

如果发现舵机运动每秒顿一次,请把每秒串口打印信息关掉就行,这是裸机的劣势之一所在。

三 FreeRTOS上完成项目

1.加入IIC的OLED屏显 和 动作执行(数组版)

下面是移植到FreeRTOS操作系统上运行,没法介绍太详细,建议先系统学一下STM32 HAL开发以及FreeRTOS,再进行。

快速简述,就是把需求分给多个任务去执行,

一个任务负责角度信息处理,

一个任务负责串口发送数据

一个任务负责OLED屏显

且这里开始把adc改成直接测8组了

接线,把OLED的SDA和SCL对应板子接好

CubeMX配置:

打开IIC

设置4个外部中断,把原来的 ADC IN1~7 ==> IN2~9,因为我板载两个按钮位于A0和A1

然后B4 B5我自己外接了两个按钮。中断都是下降沿触发

FreeRTOS配置:使用了V1版本,创建三个任务,优先级都设为普通

打开定时器用于两个外部中断按钮B4 B5的定时器消抖,我这两个没有硬件防抖,如果你们的按钮有电容,就不用。

使用单次定时器

代码:

usart.c

cpp 复制代码
/* USER CODE BEGIN 0 */
#include "stdio.h"
#include "string.h"
#include "PWM.h"

#include "adc.h"
#include "dma.h"


/*机械臂控制模式,默认为1
1:摇杆控制	
2:示教器控制
3:执行记忆动作
*/
uint8_t Mode = 1;

/*蓝牙控制机械臂指令:
s/m	停/储存当前动作
l/r 左右
u/d 上下
f/b 前后
o/c 开合*/
uint8_t cmd_BLE = 's';

extern uint16_t adc_dma[8];//DMA搬运的ADC采集值
extern uint8_t angle[4];

uint8_t k;

//覆写printf
int fputc(int ch, FILE *f)
{      
    unsigned char temp[1]={ch};
    HAL_UART_Transmit(&huart1,temp,1,0xffff);  
    return ch;
}

//=====串口(中断)=======
//串口接收缓存(1字节)
uint8_t buf=0;
//定义最大接收字节数 200,可根据需求调整
#define UART1_REC_LEN 200
// 接收缓冲, 串口接收到的数据放在这个数组里,最大UART1_REC_LEN个字节
uint8_t UART1_RX_Buffer[UART1_REC_LEN];
//  接收状态
//  bit15,      接收完成标志
//  bit14,      接收到0x0d
//  bit13~0,    接收到的有效字节数目
uint16_t UART1_RX_STA=0;

// 串口中断:接收完成回调函数,收到一个数据后,在这里处理
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	// 判断中断是由哪个串口触发的
	if(huart->Instance == USART1)
	{
		// 判断接收是否完成(UART1_RX_STA bit15 位是否为1)
		if((UART1_RX_STA & 0x8000) == 0)
		{
			// 如果已经收到了 0x0d (回车),
			if(UART1_RX_STA & 0x4000)
			{
				// 则接着判断是否收到 0x0a (换行)
				if(buf == 0x0a)
				{
					// 如果 0x0a 和 0x0d 都收到,则将 bit15 位置为1
					UART1_RX_STA |= 0x8000;
					
					//=======中断信息处理=======
					//模式切换
					if (!strcmp((const char *)UART1_RX_Buffer, "M1")) 
					{			
						Mode = 1;

						printf("摇杆模式\r\n");

					}
					else if(!strcmp((const char *)UART1_RX_Buffer, "M2")) 
					{			
						Mode = 2;

						printf("示教模式\r\n");
						
					}
					else if(!strcmp((const char *)UART1_RX_Buffer, "M3")) 
					{			
						Mode = 3;

						printf("执行记忆动作\r\n");
						
					}

					//获取蓝牙控制指令
					else if(UART1_RX_Buffer[0] == 'A')
					{
						cmd_BLE = UART1_RX_Buffer[1];
					}
					
					else {
						if(UART1_RX_Buffer[0] != '\0')
							printf("指令发送错误:%s\r\n", UART1_RX_Buffer);
					}
					
					//==========================
					memset(UART1_RX_Buffer, 0, strlen((const char *)UART1_RX_Buffer));
			 
						// 重新开始下一次接收
					UART1_RX_STA = 0;
					//==========================
				}
				else
					// 否则认为接收错误,重新开始
					UART1_RX_STA = 0;
			}
			else	// 如果没有收到了 0x0d (回车)
			{
				//则先判断收到的这个字符是否是 0x0d (回车)
				if(buf == 0x0d)
				{
					// 是的话则将 bit14 位置为1
					UART1_RX_STA |= 0x4000;
				}
				else
				{
					// 否则将接收到的数据保存在缓存数组里
					UART1_RX_Buffer[UART1_RX_STA & 0X3FFF] = buf;
					UART1_RX_STA++;
					
					// 如果接收数据大于UART1_REC_LEN(200字节),则重新开始接收
					if(UART1_RX_STA > UART1_REC_LEN - 1)
						UART1_RX_STA = 0;
				}
			}
		}
		// 重新开启中断
		HAL_UART_Receive_IT(&huart1, &buf, 1);
	}
}

/* USER CODE END 0 */




  /* USER CODE BEGIN USART1_Init 2 */
	HAL_UART_Receive_IT(&huart1, &buf, 1);
  /* USER CODE END USART1_Init 2 */

PWM.c 解释注释在前面说过

cpp 复制代码
#include "PWM.h"
#include "main.h"
#include "tim.h"
#include "stdio.h"
#include "string.h"



extern uint16_t adc_dma[8];//DMA搬运的ADC采集值
extern uint8_t angle[4];//舵机角度
extern uint8_t Mode;
extern uint8_t cmd_BLE;

uint8_t memory[location_cnt][4];
uint8_t i,j = 0;

uint8_t angle_target[4] = {90,90,90,90};
uint8_t angle_target_flag = 0;

//根据输入的角度获取对应pwm占空比参数
uint8_t Angle(uint8_t pwm_pulse)
{
	return pwm_pulse + 44;
}

void get_target()
{
	angle_target_flag = 0;
	
	for(j=0;j<4;j++)
	{
		if(angle[j] == angle_target[j])	angle_target_flag++;
	}
	
	if(angle_target_flag == 4)	i++;
	
	
	for(j=0;j<4;j++)
	{
		if(memory[i][j] == '\0')
		{
			i = 0;
		}
		angle_target[j] = memory[i][j];
	}
}

void reach_target()
{
	for(j = 0;j <4;j++)
	{
		if(angle[j] > angle_target[j])
		{
			angle[j]--;
		}
		else if(angle[j] < angle_target[j])
		{
			angle[j]++;
		}
	}
}

void translate()//根据实际情况做了一点角度矫正和限位
{
	angle_target[3] = (uint8_t)((double)adc_dma[4] / 22.75)/2;
	angle_target[2] = (uint8_t)((double)adc_dma[5] / 22.75);
	angle_target[1] = (uint8_t)((double)adc_dma[6] / 22.75) - 10;
	angle_target[0] = 180 - (uint8_t)((double)adc_dma[7] / 22.75);
	
	if(angle_target[1]<45)	angle_target[1]=45;
	else if(angle_target[1]>135)	angle_target[1]=135;
	
	if(angle_target[2]<45)	angle_target[1]=45;
	else if(angle_target[2]>135)	angle_target[1]=135;
}

//是否记录当前位置信息
void if_BLE_cmd()
{
	switch(cmd_BLE)
	{
		case 'm':
			
			if(i < location_cnt)
			{
				for(j=0;j<4;j++)
				{
					memory[i][j] = angle[j];
				}
				printf("储存动作\r\n");
				cmd_BLE = 's';
				i++;
			}
			else
			{
				printf("动作已满\r\n");
				cmd_BLE = 's';
			}
		break;
		
		case 'g':
			
			for(i=0;i < location_cnt;i++)
			{
				for(j=0;j<4;j++)
				{
					printf("%d ",memory[i][j]);
				}
				printf("\r\n");
				if(memory[i][j] == '\0')	break;
			}
			cmd_BLE = 's';
		break;
				
		case 'D':
			
			for(i=0; i < location_cnt ;i++)
			{
				memset(memory[i],'\0',4);
			}
			i = 0;
			printf("已清除动作");
			cmd_BLE = 's';
		break;
	}
}

void check_sg_cmd()
{
	check_A();
	check_B();
	check_C();
	check_D();
}

//舵机A,夹爪	CH4_B11-D1;adc4_A3
void check_A()
{
	if(Mode == 1)
	{
		if((cmd_BLE == 'c' || adc_dma[3] > 4000) && angle[3] < 90)//合
		{
			angle[3]++;
		}
		else if((cmd_BLE == 'o' || adc_dma[3] <1000) && angle[3] > 0)//开
		{
			angle[3]--;
		}
	}

}

//舵机B,上下	CH3_B10-D2;adc3_A2
void check_B()
{
	if(Mode == 1)
	{
		if((cmd_BLE == 'u' || adc_dma[2] <1000) && angle[2] < 135)//上
		{
			angle[2]++;
		}
		else if((cmd_BLE == 'd' || adc_dma[2] > 4000) && angle[2] > 45)//下
		{
			angle[2]--;
		}
	}

}

//舵机C,前后	CH2_B3-D3;adc2_A1
void check_C()
{
	if(Mode == 1)
	{
		if((cmd_BLE == 'f' || adc_dma[1] <1000) && angle[1] < 135)//前
		{
			angle[1]++;
		}
		else if((cmd_BLE == 'b' || adc_dma[1] > 4000) && angle[1] > 45)//后
		{
			angle[1]--;
		}
	}

}
//舵机D,底座	CH1_A15-D0;adc1_A0
void check_D()
{
	if(Mode == 1)
	{
		if((cmd_BLE == 'l' || adc_dma[0] <1000) && angle[0] < 180)//左
		{
			angle[0]++;
		}
		else if((cmd_BLE == 'r' || adc_dma[0] > 4000) && angle[0] > 0)//右
		{
			angle[0]--;
		}
	}

}

PWM.h

cpp 复制代码
#ifndef __PWM_H__
#define __PWM_H__

#define location_cnt 20
 
//根据输入的角度获取对应pwm占空比参数
unsigned char Angle(unsigned char pwm_pulse);

//舵机A,夹爪	CH4_B11
void check_A(void);

//舵机B,上下	CH3_B10
void check_B(void);

//舵机C,前后	CH2_B3
void check_C(void);

//舵机D,底座	CH1_A15
void check_D(void);

void check_sg_cmd(void);

void if_BLE_cmd(void);

void translate(void);

void get_target(void);
	
void reach_target(void);

#endif

OLED.c

cpp 复制代码
#include "OLED.h"
#include "i2c.h"
#include "oledfont.h"


void Oled_Write_Cmd(uint8_t OLED_cmd)
{
	HAL_I2C_Mem_Write(&hi2c1, 0x78, 0x00, I2C_MEMADD_SIZE_8BIT, &OLED_cmd, 1, 0xff);
}


void Oled_Write_Data(uint8_t OLED_data)
{
	HAL_I2C_Mem_Write(&hi2c1, 0x78, 0x40, I2C_MEMADD_SIZE_8BIT, &OLED_data, 1, 0xff);
}

//OLED初始化代码,直接复制粘贴
void Oled_Init(void){
	Oled_Write_Cmd(0xAE);//--display off
	Oled_Write_Cmd(0x00);//---set low column address
	Oled_Write_Cmd(0x10);//---set high column address
	Oled_Write_Cmd(0x40);//--set start line address
	Oled_Write_Cmd(0xB0);//--set page address
	Oled_Write_Cmd(0x81); // contract control
	Oled_Write_Cmd(0xFF);//--128
	Oled_Write_Cmd(0xA1);//set segment remap
	Oled_Write_Cmd(0xA6);//--normal / reverse
	Oled_Write_Cmd(0xA8);//--set multiplex ratio(1 to 64)
	Oled_Write_Cmd(0x3F);//--1/32 duty
	Oled_Write_Cmd(0xC8);//Com scan direction
	Oled_Write_Cmd(0xD3);//-set display offset
	Oled_Write_Cmd(0x00);//
	Oled_Write_Cmd(0xD5);//set osc division
	Oled_Write_Cmd(0x80);//
	Oled_Write_Cmd(0xD8);//set area color mode off
	Oled_Write_Cmd(0x05);//
	Oled_Write_Cmd(0xD9);//Set Pre-Charge Period
	Oled_Write_Cmd(0xF1);//
	Oled_Write_Cmd(0xDA);//set com pin configuartion
	Oled_Write_Cmd(0x12);//
	Oled_Write_Cmd(0xDB);//set Vcomh
	Oled_Write_Cmd(0x30);//
	Oled_Write_Cmd(0x8D);//set charge pump enable
	Oled_Write_Cmd(0x14);//
	Oled_Write_Cmd(0xAF);//--turn on oled panel
}
 
void Oled_Clear()
{
	unsigned char i,j; //-128 --- 127
	for(i=0;i<8;i++){
		Oled_Write_Cmd(0xB0 + i);//page0--page7
		//每个page从0列
		Oled_Write_Cmd(0x00);
		Oled_Write_Cmd(0x10);
		//0到127列,依次写入0,每写入数据,列地址自动偏移
		for(j = 0;j<128;j++){
			Oled_Write_Data(0);
		}
	}
}

//在屏幕上显示
//===============================官方提供的代码============================================
void Oled_Show_Char(char row,char col,uint8_t oledChar){ //row*2-2
	unsigned int  i;
	Oled_Write_Cmd(0xb0+(row*2-2));                           //page 0
	Oled_Write_Cmd(0x00+(col&0x0f));                          //low
	Oled_Write_Cmd(0x10+(col>>4));                            //high	
	for(i=((oledChar-32)*16);i<((oledChar-32)*16+8);i++){
		Oled_Write_Data(F8X16[i]);                            //写数据oledTable1
	}
 
	Oled_Write_Cmd(0xb0+(row*2-1));                           //page 1
	Oled_Write_Cmd(0x00+(col&0x0f));                          //low
	Oled_Write_Cmd(0x10+(col>>4));                            //high
	for(i=((oledChar-32)*16+8);i<((oledChar-32)*16+8+8);i++){
		Oled_Write_Data(F8X16[i]);                            //写数据oledTable1
	}		
}
 
 
/******************************************************************************/
// 函数名称:Oled_Show_Char 
// 输入参数:oledChar 
// 输出参数:无 
// 函数功能:OLED显示单个字符
/******************************************************************************/
void Oled_Show_Str(char row,char col,char *str){
	while(*str!=0){
		Oled_Show_Char(row,col,*str);
		str++;
		col += 8;	
	}		
}

OLED.h

cpp 复制代码
#ifndef __OLED_H__
#define __OLED_H__

void Oled_Init(void);
void Oled_Clear(void);
	
void Oled_Show_Str(char row,char col,char *str);
	
#endif

main.c 放了一点初始化配置

cpp 复制代码
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "stdio.h"
/* USER CODE END Includes */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

uint16_t adc_dma[8];//DMA搬运的ADC采集值

uint8_t angle[4] = {90,90,90,90};//舵机角度

/* USER CODE END PV */

  /* USER CODE BEGIN 2 */
	// 开启接收中断
	printf("Start\r\n");//程序开始运行
	HAL_ADC_Start_DMA(&hadc1,(uint32_t *)adc_dma,8); //开启ADC和DMA
	//系统稳定半秒
	HAL_Delay(500);
	
  /* USER CODE END 2 */

freeRTOS.c

uint8_t anti_shake = 0;//定时器按钮消抖标志位

最下面的函数就是外部中断回调函数,anti_shake为0才能通过判断,一旦进入就将anti_shake置为1,避免因为按下的抖动导致按一次触发好几次中断。通过判断之后,启动单次定时器800ms,定时器到时间后触发定时器中断回调函数,在这里再把anti_shake重新置为0。

其中由于我的板载按键0和1本身有硬件消抖,所以不用软件定时器消抖。

cpp 复制代码
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "stdio.h"
#include "PWM.h"
#include "tim.h"
#include "adc.h"
#include "OLED.h"
/* USER CODE END Includes */

/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN Variables */
uint8_t anti_shake = 0;//定时器按钮消抖标志位

extern uint16_t adc_dma[8];//DMA搬运的ADC采集值

extern uint8_t angle[4];//舵机角度

extern uint8_t Mode;

extern uint8_t memory[location_cnt][4];
extern uint8_t i,j;


//角度信息字符串
char speedMes[8];  //IIC发送角度数据的字符串缓冲区
char speedMes1[8];
char speedMes2[8];
char speedMes3[8];
char speedMes4[8];
char speedMes5[8];

/* USER CODE END Variables */


/* USER CODE BEGIN Header_Start_check_angle */
/**
  * @brief  Function implementing the check_angle thread.
  * @param  argument: Not used
  * @retval None
  */
/* USER CODE END Header_Start_check_angle */
void Start_check_angle(void const * argument)
{
  /* USER CODE BEGIN Start_check_angle */
	//开启4路PWM
	HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
	HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);
	HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3);
	HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_4);
  /* Infinite loop */
  for(;;)
  {
		if(Mode == 1)
		{
			check_sg_cmd();
		}
		else if(Mode == 2)
		{
			translate();
			reach_target();
		}
		else if(Mode == 3)
		{
			get_target();
			reach_target();
		}
		
		if_BLE_cmd();
		
		//输出PWM波使舵机运动
		__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_1, Angle(angle[0]));
		__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_2, Angle(angle[1]));
		__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_3, Angle(angle[2]));
		__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_4, Angle(angle[3]));
		
    osDelay(15);//通过调整此延时可以改变机械臂运行速度
  }
  /* USER CODE END Start_check_angle */
}

/* USER CODE BEGIN Header_Start_usart_show */
/**
* @brief Function implementing the usart_show thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_Start_usart_show */
void Start_usart_show(void const * argument)
{
  /* USER CODE BEGIN Start_usart_show */
	
  /* Infinite loop */
  for(;;)
  {
		printf("Angle = {%d, %d, %d, %d}\r\n",angle[0],angle[1],angle[2],angle[3]);
		printf("adc_dma1 = {%d, %d, %d, %d}\r\n",adc_dma[0],adc_dma[1],adc_dma[2],adc_dma[3]);
		printf("adc_dma2 = {%d, %d, %d, %d}\r\n",adc_dma[4],adc_dma[5],adc_dma[6],adc_dma[7]);
		printf("\r\n");
    osDelay(1000);
  }
  /* USER CODE END Start_usart_show */
}

/* USER CODE BEGIN Header_Start_OLED_Task */
/**
* @brief Function implementing the OLED_Task thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_Start_OLED_Task */
void Start_OLED_Task(void const * argument)
{
  /* USER CODE BEGIN Start_OLED_Task */
	Oled_Init();
	Oled_Clear();
  /* Infinite loop */
  for(;;)
  {
		//串口数据的字符串拼装,speed是格子,每个格子1cm
		sprintf(speedMes,"A: %d ",angle[0]);
		sprintf(speedMes1,"B: %d ",angle[1]);
		sprintf(speedMes2,"C: %d ",angle[2]);
		sprintf(speedMes3,"D: %d ",angle[3]);
		sprintf(speedMes4,"Mode %d ",Mode);
		sprintf(speedMes5,"S %d ",i);
		
		Oled_Show_Str(1,5,speedMes);
		Oled_Show_Str(1,69,speedMes1);
		Oled_Show_Str(2,5,speedMes2);
		Oled_Show_Str(2,69,speedMes3);
		
		Oled_Show_Str(4,0,speedMes4);
		Oled_Show_Str(4,64,speedMes5);
		
		
    osDelay(500);
  }
  /* USER CODE END Start_OLED_Task */
}


/* Callback01 function */
void Callback01(void const * argument)
{
  /* USER CODE BEGIN Callback01 */
	anti_shake = 0;
  /* USER CODE END Callback01 */
}


/* Private application code --------------------------------------------------*/
/* USER CODE BEGIN Application */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	switch(GPIO_Pin)
	{
		case GPIO_PIN_0:
			HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_RESET);
			HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_SET);
			Mode = 1;
			printf("摇杆模式\r\n");
		
		break;
		case GPIO_PIN_1:
			HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET);
			HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_RESET);
			Mode = 2;
			printf("示教模式\r\n");
		
		break;
		
		case GPIO_PIN_4:
			if(anti_shake == 0)
			{
				anti_shake = 1;
				
				osTimerStart(myTimer01Handle,800);
				
				if(i<location_cnt)
				{
					for(j=0;j<4;j++)
					{
						memory[i][j] = angle[j];
					}
					printf("储存动作\r\n");
					i++;
				}
				else if(i>=9)
				{
					printf("动作已满\r\n");
				}
			}
			
		case GPIO_PIN_5:
			if(anti_shake == 0)
			{
				anti_shake = 1;
				
				Mode = 3;
				
				HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_RESET);
				HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_RESET);
				
				printf("执行记忆动作\r\n");
				
				osTimerStart(myTimer01Handle,800);
			}
	}
}
/* USER CODE END Application */

2.链表的增删遍历实现动作记忆和执行

创建 Memary_LinkList.c和.h文件,放链表相关代码

cpp 复制代码
#include "Memary_LinkList.h"
#include "main.h"
#include "stdio.h"
#include <stdlib.h>

struct Memary//每个链表节点的结构组成
{
	uint8_t A,B,C,D,cnt;//四个角度和节点的对应编号
	
	struct Memary *next;//下一个链表节点的地址
}*head,*tail,*temp;//声明三个指针,分别指向链表头尾,和一个临时指针

void Memary_Init()//初始化链表
{
	head = (struct Memary *)malloc(sizeof(struct Memary));
	
	head -> cnt = 0;//链表里没记忆信息时编号只有0
	head -> next = NULL;
	tail = head;
	temp = head;
}


void prinrt_List()//打印整个链表的内容
{
	temp = head;
	while(1)
	{
		printf("%d:{%d,%d,%d,%d}\r\n",temp->cnt,temp->A,temp->B,temp->C,temp->D);
		if(temp->next != NULL)	temp = temp->next;
		else	break;
	}
}

void delete_List()//清空整个链表
{
	while(head->next != NULL)
	{
		temp = head;
		head = head->next;
		free(temp);
	}
	temp = head;
	head->cnt = 0;
}

void addNode(uint8_t angle[4])//记忆动作,即新增一个节点
{
	if(head->cnt != 0)//编号cnt从1开始,从头节点开始存数据
	{
		struct Memary *p = (struct Memary *)malloc(sizeof(struct Memary));

		tail->next = p;
		p->cnt = tail->cnt;
		tail = p;
	}
	tail->A = angle[0];
	tail->B = angle[1];
	tail->C = angle[2];
	tail->D = angle[3];
	tail->cnt++;
	
	temp = tail;
}

uint8_t angle_temp[4];//用于提取记忆的角度
uint8_t *p = angle_temp;//用指针传递

uint8_t *getNode()//把记忆信息传出去,自动循环传整个链表
{
	angle_temp[0] = temp->A;
	angle_temp[1] = temp->B;
	angle_temp[2] = temp->C;
	angle_temp[3] = temp->D;
	
	if(temp->next == NULL)
		temp = head;
	else
		temp = temp->next;
	
	return p;
}

uint8_t sizeof_List()//别管名字,看temp在哪用的,
{
	//反应在OLED上就是可以看见当前存储到第几个动作了或者正在执行第一个动作
	return temp->cnt;
}
cpp 复制代码
#ifndef __MEMARY_LINKLIST__
#define __MEMARY_LINKLIST__

#include "main.h"

void Memary_Init(void);

void addNode(uint8_t angle[4]);

void prinrt_List(void);

void delete_List(void);

uint8_t *getNode(void);

uint8_t sizeof_List(void);

#endif

原PWM.c改成了MG90s.c

cpp 复制代码
#include "MG90s.h"
#include "main.h"
#include "tim.h"
#include "stdio.h"
#include "string.h"
#include "Memary_LinkList.h"

extern uint16_t adc_dma[8];//DMA搬运的ADC采集值
extern uint8_t angle[4];//舵机角度
extern uint8_t Mode;
extern uint8_t cmd_BLE;

uint8_t i;

uint8_t angle_target[4] = {90,90,90,90};
uint8_t *p_angle_target;

uint8_t angle_target_flag = 4;//默认第一次为4



//根据输入的角度获取对应pwm占空比参数
uint8_t Angle(uint8_t pwm_pulse)
{
	return pwm_pulse + 44;
}

void getAngleFromMemary()
{	
	if(angle_target_flag == 4)
	{
		p_angle_target = getNode();
		for(i = 0;i <4;i++)
		{
			angle_target[i] = *(p_angle_target + i);
		}
	}
	
	angle_target_flag = 0;
	
	for(i=0;i<4;i++)
	{
		if(angle[i] == angle_target[i])	angle_target_flag++;
	}
}

void reach_target()
{
	for(i = 0;i <4;i++)
	{
		if(angle[i] > angle_target[i])
		{
			angle[i]--;
		}
		else if(angle[i] < angle_target[i])
		{
			angle[i]++;
		}
	}
}

void translate()
{
	angle_target[3] = (uint8_t)((double)adc_dma[4] / 22.75)/2;
	angle_target[2] = (uint8_t)((double)adc_dma[5] / 22.75);
	angle_target[1] = (uint8_t)((double)adc_dma[6] / 22.75) - 10;
	angle_target[0] = 180 - (uint8_t)((double)adc_dma[7] / 22.75);
	
	if(angle_target[1]<45)	angle_target[1]=45;
	else if(angle_target[1]>135)	angle_target[1]=135;
	
	if(angle_target[2]<45)	angle_target[1]=45;
	else if(angle_target[2]>135)	angle_target[1]=135;
}

//是否记录当前位置信息
void if_BLE_cmd()
{
	switch(cmd_BLE)
	{
		case 'm':
			
			addNode(angle);
			printf("储存动作\r\n");
			cmd_BLE = 's';
				
		break;
		
		case 'g':

			prinrt_List();
			cmd_BLE = 's';
		break;
				
		case 'D':
			
			delete_List();
			printf("已清除动作\r\n");
			cmd_BLE = 's';
		break;
	}
}

void check_sg_cmd()
{
	check_A();
	check_B();
	check_C();
	check_D();
}

//舵机A,夹爪	CH4_B11-D1;adc4_A3
void check_A()
{
	if(Mode == 1)
	{
		if((cmd_BLE == 'c' || adc_dma[3] > 4000) && angle[3] < 90)//合
		{
			angle[3]++;
		}
		else if((cmd_BLE == 'o' || adc_dma[3] <1000) && angle[3] > 0)//开
		{
			angle[3]--;
		}
	}

}

//舵机B,上下	CH3_B10-D2;adc3_A2
void check_B()
{
	if(Mode == 1)
	{
		if((cmd_BLE == 'u' || adc_dma[2] <1000) && angle[2] < 135)//上
		{
			angle[2]++;
		}
		else if((cmd_BLE == 'd' || adc_dma[2] > 4000) && angle[2] > 45)//下
		{
			angle[2]--;
		}
	}

}

//舵机C,前后	CH2_B3-D3;adc2_A1
void check_C()
{
	if(Mode == 1)
	{
		if((cmd_BLE == 'f' || adc_dma[1] <1000) && angle[1] < 135)//前
		{
			angle[1]++;
		}
		else if((cmd_BLE == 'b' || adc_dma[1] > 4000) && angle[1] > 45)//后
		{
			angle[1]--;
		}
	}

}
//舵机D,底座	CH1_A15-D0;adc1_A0
void check_D()
{
	if(Mode == 1)
	{
		if((cmd_BLE == 'l' || adc_dma[0] <1000) && angle[0] < 180)//左
		{
			angle[0]++;
		}
		else if((cmd_BLE == 'r' || adc_dma[0] > 4000) && angle[0] > 0)//右
		{
			angle[0]--;
		}
	}

}

FreeRTOS.c要修改的部分

cpp 复制代码
//                任务1

/* USER CODE END Header_Start_check_angle */
void Start_check_angle(void const * argument)
{
  /* USER CODE BEGIN Start_check_angle */
	Memary_Init();
	//开启4路PWM
	HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
	HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);
	HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3);
	HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_4);
  /* Infinite loop */
  for(;;)
  {
		if(Mode == 1)
		{
			check_sg_cmd();
		}
		else if(Mode == 2)
		{
			translate();
			reach_target();
		}
		else if(Mode == 3)
		{
			getAngleFromMemary();
			reach_target();
		}
		
		if_BLE_cmd();
		
		//输出PWM波使舵机运动
		__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_1, Angle(angle[0]));
		__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_2, Angle(angle[1]));
		__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_3, Angle(angle[2]));
		__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_4, Angle(angle[3]));
		
    osDelay(18);//通过调整此延时可以改变机械臂运行速度
  }
  /* USER CODE END Start_check_angle */
}


//         任务3

/* USER CODE END Header_Start_OLED_Task */
void Start_OLED_Task(void const * argument)
{
  /* USER CODE BEGIN Start_OLED_Task */
	Oled_Init();
	Oled_Clear();
  /* Infinite loop */
  for(;;)
  {
		//串口数据的字符串拼装,speed是格子,每个格子1cm
		sprintf(speedMes,"A: %d ",angle[0]);
		sprintf(speedMes1,"B: %d ",angle[1]);
		sprintf(speedMes2,"C: %d ",angle[2]);
		sprintf(speedMes3,"D: %d ",angle[3]);
		sprintf(speedMes4,"Mode %d ",Mode);
		sprintf(speedMes5,"S %d ",sizeof_List());
		
		Oled_Show_Str(1,5,speedMes);
		Oled_Show_Str(1,69,speedMes1);
		Oled_Show_Str(2,5,speedMes2);
		Oled_Show_Str(2,69,speedMes3);
		
		Oled_Show_Str(4,0,speedMes4);
		Oled_Show_Str(4,54,speedMes5);
		
		
    osDelay(500);
  }
  /* USER CODE END Start_OLED_Task */
}




//         外部中断

		case GPIO_PIN_4:
			if(anti_shake == 0)
			{
				anti_shake = 1;
				osTimerStart(myTimer01Handle,800);
				
				addNode(angle);

				printf("储存动作\r\n");
			}

到这里位置,视频里我实现的内容都可以完成了。

至于这个链表最大能存多长的动作我是没去实测,反正我们玩绝对是够用了。

3.SPI扩容

不幸的是我板子的SPI2坏了,只有SPI1正常,所以我没真正去完成这个功能。

但是这里我提供了完整的存储思路。大家可自己来实现

功能测试

PB12设为GPIO输出引脚默认设为高电平,充当CS口,接线就是CS对CS,SCK对CLK,MOSI对DI,MISO对DO

VCC用3.3,5v没试过。

在spi.c中添加 比如如果SPI1就是hspi1,SPI2就是hspi2,别的都不用动

cpp 复制代码
/* USER CODE BEGIN 1 */
uint8_t spi2_read_write_byte(uint8_t data)
{
    uint8_t rec_data = 0;
    
    HAL_SPI_TransmitReceive(&hspi2, &data, &rec_data, 1, 1000);
    
    return rec_data;
}
/* USER CODE END 1 */

spi.h

cpp 复制代码
/* USER CODE BEGIN Prototypes */
uint8_t spi2_read_write_byte(uint8_t data);
/* USER CODE END Prototypes */

w25q128.h 这里你CS用的哪个GPIO_OUT,W25Q128_CS_GPIO就改成对应的。

cpp 复制代码
#ifndef __W25Q128_H__
#define __W25Q128_H__

#include "stdint.h"

/* W25Q128片选引脚定义 */
#define W25Q128_CS_GPIO_PORT           GPIOB
#define W25Q128_CS_GPIO_PIN            GPIO_PIN_12

/* W25Q128片选信号 */
#define W25Q128_CS(x)      do{ x ? \
                                  HAL_GPIO_WritePin(W25Q128_CS_GPIO_PORT, W25Q128_CS_GPIO_PIN, GPIO_PIN_SET) : \
                                  HAL_GPIO_WritePin(W25Q128_CS_GPIO_PORT, W25Q128_CS_GPIO_PIN, GPIO_PIN_RESET); \
                            }while(0)

/* FLASH芯片列表 */
#define W25Q128     0XEF17          /* W25Q128  芯片ID */

/* 指令表 */
#define FLASH_WriteEnable						0x06 
#define FLASH_ReadStatusReg1				0x05 
#define FLASH_ReadData							0x03 
#define FLASH_PageProgram						0x02 
#define FLASH_SectorErase						0x20 
#define FLASH_ChipErase							0xC7 
#define FLASH_ManufactDeviceID			0x90 

/* 静态函数 */
static void w25q128_wait_busy(void);               /* 等待空闲 */
static void w25q128_send_address(uint32_t address);/* 发送地址 */
static void w25q128_write_page(uint8_t *pbuf, uint32_t addr, uint16_t datalen);    /* 写入page */
static void w25q128_write_nocheck(uint8_t *pbuf, uint32_t addr, uint16_t datalen); /* 写flash,不带擦除 */

/* 普通函数 */
void w25q128_init(void);                   /* 初始化25QXX */
uint16_t w25q128_read_id(void);            /* 读取FLASH ID */
void w25q128_write_enable(void);           /* 写使能 */
uint8_t w25q128_rd_sr1(void);							/* 读取寄存器1的值 */

void w25q128_erase_chip(void);             /* 整片擦除 */
void w25q128_erase_sector(uint32_t saddr); /* 扇区擦除 */
void w25q128_read(uint8_t *pbuf, uint32_t addr, uint16_t datalen);     /* 读取flash */
void w25q128_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen);    /* 写入flash */

#endif

w25q128.c

cpp 复制代码
#include "w25q128.h"
#include "spi.h"
#include "stdio.h"

/**
 * @brief       初始化W25Q128
 * @param       无
 * @retval      无
 */
void w25q128_init(void)
{
		uint16_t flash_type;
    spi2_read_write_byte(0xFF); /* 清除DR的作用 */
    W25Q128_CS(1);
		flash_type = w25q128_read_id();   /* 读取FLASH ID. */
		if (flash_type == W25Q128)
			printf("检测到W25Q128芯片\r\n");
		else
			printf("读取到的芯片ID为:%x\r\n",flash_type);
			printf("未检测到W25Q128芯片\r\n");
}

/**
 * @brief       等待空闲
 * @param       无
 * @retval      无
 */
static void w25q128_wait_busy(void)
{
    while ((w25q128_rd_sr1() & 0x01) == 0x01);   /* 等待BUSY位清空 */
}

/**
 * @brief       读取W25Q128的状态寄存器1的值
 * @param       无
 * @retval      状态寄存器值
 */
uint8_t w25q128_rd_sr1(void)
{
    uint8_t rec_data = 0;
    
    W25Q128_CS(0);
    spi2_read_write_byte(FLASH_ReadStatusReg1);     /* 读状态寄存器1 */
    rec_data = spi2_read_write_byte(0xFF);
    W25Q128_CS(1);
    
    return rec_data;
}

/**
 * @brief       W25Q128写使能
 *   @note      将S1寄存器的WEL置位
 * @param       无
 * @retval      无
 */
void w25q128_write_enable(void)
{
    W25Q128_CS(0);
    spi2_read_write_byte(FLASH_WriteEnable);   /* 发送写使能 */
    W25Q128_CS(1);
}

/**
 * @brief       W25Q128发送地址
 * @param       address : 要发送的地址
 * @retval      无
 */
static void w25q128_send_address(uint32_t address)
{
    spi2_read_write_byte((uint8_t)((address)>>16));     /* 发送 bit23 ~ bit16 地址 */
    spi2_read_write_byte((uint8_t)((address)>>8));      /* 发送 bit15 ~ bit8  地址 */
    spi2_read_write_byte((uint8_t)address);             /* 发送 bit7  ~ bit0  地址 */
}

/**
 * @brief       擦除整个芯片
 *   @note      等待时间超长...
 * @param       无
 * @retval      无
 */
void w25q128_erase_chip(void)
{
    w25q128_write_enable();    /* 写使能 */
    w25q128_wait_busy();       /* 等待空闲 */
    W25Q128_CS(0);
    spi2_read_write_byte(FLASH_ChipErase);  /* 发送读寄存器命令 */ 
    W25Q128_CS(1);
    w25q128_wait_busy();       /* 等待芯片擦除结束 */
}

/**
 * @brief       擦除一个扇区
 *   @note      注意,这里是扇区地址,不是字节地址!!
 *              擦除一个扇区的最少时间:150ms
 *
 * @param       saddr : 扇区地址 根据实际容量设置
 * @retval      无
 */
void w25q128_erase_sector(uint32_t saddr)
{
    //printf("fe:%x\r\n", saddr);   /* 监视falsh擦除情况,测试用 */
    saddr *= 4096;
    w25q128_write_enable();        /* 写使能 */
    w25q128_wait_busy();           /* 等待空闲 */

    W25Q128_CS(0);
    spi2_read_write_byte(FLASH_SectorErase);    /* 发送写页命令 */
    w25q128_send_address(saddr);   /* 发送地址 */
    W25Q128_CS(1);
    w25q128_wait_busy();           /* 等待扇区擦除完成 */
}

/**
 * @brief       读取芯片ID
 * @param       无
 * @retval      FLASH芯片ID
 *   @note      芯片ID列表见: w25q128.h, 芯片列表部分
 */
uint16_t w25q128_read_id(void)
{
    uint16_t deviceid;

    W25Q128_CS(0);
    spi2_read_write_byte(FLASH_ManufactDeviceID);   /* 发送读 ID 命令 */
    spi2_read_write_byte(0);    /* 写入一个字节 */
    spi2_read_write_byte(0);
    spi2_read_write_byte(0);
    deviceid = spi2_read_write_byte(0xFF) << 8;     /* 读取高8位字节 */
    deviceid |= spi2_read_write_byte(0xFF);         /* 读取低8位字节 */
    W25Q128_CS(1);

    return deviceid;
}

/**
 * @brief       读取SPI FLASH
 *   @note      在指定地址开始读取指定长度的数据
 * @param       pbuf    : 数据存储区
 * @param       addr    : 开始读取的地址(最大32bit)
 * @param       datalen : 要读取的字节数(最大65535)
 * @retval      无
 */
void w25q128_read(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    uint16_t i;

    W25Q128_CS(0);
    spi2_read_write_byte(FLASH_ReadData);       /* 发送读取命令 */
    w25q128_send_address(addr);                /* 发送地址 */
    
    for(i=0;i<datalen;i++)
    {
        pbuf[i] = spi2_read_write_byte(0XFF);   /* 循环读取 */
    }
    
    W25Q128_CS(1);
}

/**
 * @brief       SPI在一页(0~65535)内写入少于256个字节的数据
 *   @note      在指定地址开始写入最大256字节的数据
 * @param       pbuf    : 数据存储区
 * @param       addr    : 开始写入的地址(最大32bit)
 * @param       datalen : 要写入的字节数(最大256),该数不应该超过该页的剩余字节数!!!
 * @retval      无
 */
static void w25q128_write_page(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    uint16_t i;

    w25q128_write_enable();    /* 写使能 */

    W25Q128_CS(0);
    spi2_read_write_byte(FLASH_PageProgram);    /* 发送写页命令 */
    w25q128_send_address(addr);                /* 发送地址 */

    for(i=0;i<datalen;i++)
    {
        spi2_read_write_byte(pbuf[i]);          /* 循环写入 */
    }
    
    W25Q128_CS(1);
    w25q128_wait_busy();       /* 等待写入结束 */
}

/**
 * @brief       无检验写SPI FLASH
 *   @note      必须确保所写的地址范围内的数据全部为0XFF,否则在非0XFF处写入的数据将失败!
 *              具有自动换页功能
 *              在指定地址开始写入指定长度的数据,但是要确保地址不越界!
 *
 * @param       pbuf    : 数据存储区
 * @param       addr    : 开始写入的地址(最大32bit)
 * @param       datalen : 要写入的字节数(最大65535)
 * @retval      无
 */
static void w25q128_write_nocheck(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    uint16_t pageremain;
    pageremain = 256 - addr % 256;  /* 单页剩余的字节数 */

    if (datalen <= pageremain)      /* 不大于256个字节 */
    {
        pageremain = datalen;
    }

    while (1)
    {
        /* 当写入字节比页内剩余地址还少的时候, 一次性写完
         * 当写入直接比页内剩余地址还多的时候, 先写完整个页内剩余地址, 然后根据剩余长度进行不同处理
         */
        w25q128_write_page(pbuf, addr, pageremain);

        if (datalen == pageremain)   /* 写入结束了 */
        {
            break;
        }
        else     /* datalen > pageremain */
        {
            pbuf += pageremain;         /* pbuf指针地址偏移,前面已经写了pageremain字节 */
            addr += pageremain;         /* 写地址偏移,前面已经写了pageremain字节 */
            datalen -= pageremain;      /* 写入总长度减去已经写入了的字节数 */

            if (datalen > 256)          /* 剩余数据还大于一页,可以一次写一页 */
            {
                pageremain = 256;       /* 一次可以写入256个字节 */
            }
            else     /* 剩余数据小于一页,可以一次写完 */
            {
                pageremain = datalen;   /* 不够256个字节了 */
            }
        }
    }
}

/**
 * @brief       写SPI FLASH
 *   @note      在指定地址开始写入指定长度的数据 , 该函数带擦除操作!
 *              SPI FLASH 一般是: 256个字节为一个Page, 4Kbytes为一个Sector, 16个扇区为1个Block
 *              擦除的最小单位为Sector.
 *
 * @param       pbuf    : 数据存储区
 * @param       addr    : 开始写入的地址(最大32bit)
 * @param       datalen : 要写入的字节数(最大65535)
 * @retval      无
 */
uint8_t g_w25q128_buf[4096];   /* 扇区缓存 */

void w25q128_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    uint32_t secpos;
    uint16_t secoff;
    uint16_t secremain;
    uint16_t i;
    uint8_t *w25q128_buf;

    w25q128_buf = g_w25q128_buf;
    secpos = addr / 4096;       /* 扇区地址 */
    secoff = addr % 4096;       /* 在扇区内的偏移 */
    secremain = 4096 - secoff;  /* 扇区剩余空间大小 */

    //printf("ad:%X,nb:%X\r\n", addr, datalen); /* 测试用 */
    if (datalen <= secremain)
    {
        secremain = datalen;    /* 不大于4096个字节 */
    }

    while (1)
    {
        w25q128_read(w25q128_buf, secpos * 4096, 4096);   /* 读出整个扇区的内容 */

        for (i = 0; i < secremain; i++)   /* 校验数据 */
        {
            if (w25q128_buf[secoff + i] != 0XFF)
            {
                break;      /* 需要擦除, 直接退出for循环 */
            }
        }

        if (i < secremain)   /* 需要擦除 */
        {
            w25q128_erase_sector(secpos);  /* 擦除这个扇区 */

            for (i = 0; i < secremain; i++)   /* 复制 */
            {
                w25q128_buf[i + secoff] = pbuf[i];
            }

            w25q128_write_nocheck(w25q128_buf, secpos * 4096, 4096);  /* 写入整个扇区 */
        }
        else        /* 写已经擦除了的,直接写入扇区剩余区间. */
        {
            w25q128_write_nocheck(pbuf, addr, secremain);  /* 直接写扇区 */
        }

        if (datalen == secremain)
        {
            break;  /* 写入结束了 */
        }
        else        /* 写入未结束 */
        {
            secpos++;               /* 扇区地址增1 */
            secoff = 0;             /* 偏移位置为0 */

            pbuf += secremain;      /* 指针偏移 */
            addr += secremain;      /* 写地址偏移 */
            datalen -= secremain;   /* 字节数递减 */

            if (datalen > 4096)
            {
                secremain = 4096;   /* 下一个扇区还是写不完 */
            }
            else
            {
                secremain = datalen;/* 下一个扇区可以写完了 */
            }
        }
    }
}

main函数里用这段代码测试

cpp 复制代码
  /* USER CODE BEGIN 2 */
	w25q128_init();
	
	/* 写入测试数据 */
	sprintf((char *)datatemp, "stm32f103c8t6");
	w25q128_write(datatemp, FLASH_WriteAddress, TEXT_SIZE);
	printf("数据写入完成!\r\n");
	
	/* 读出测试数据 */
	memset(datatemp, 0, TEXT_SIZE);
	w25q128_read(datatemp, FLASH_ReadAddress, TEXT_SIZE);
	printf("读出数据:%s\r\n", datatemp);
	
  /* USER CODE END 2 */

运行结果第一句首先一定是都没读取到 w25q128 这个芯片,这段判断代码在w25q128_init();里

w25q128芯片分析与功能实现

以前在这个文章里讲过

STM32:SPI_我有在好好学习的博客-CSDN博客

总之, 整个存储空间的组成是

256个Block * 16个Sector * 16个Page * 256个字节 = 共16MB

而我们代码增删改查储存信息的单位是按照Sector为单位的,即一个扇区一个扇区的处理信息

阅读官方的芯片手册:

可知存储地址为六位十六进制,刚好对应16MB

0 0 0 0 0 0

前两位表示BLOCK,16*16 即256个Block

第三位对应Sector,16个

第四位对应Page,16个

五六位对应16*16 即256个字节

所以通过设置这个六位的地址,我们就可以决定把数据存在哪个扇区

例如 3FD000 ,就表示第63Block的第13个Sector,又因为我们是按照Sector操作的

所以通过前三位来选择不同的扇区来存数据

存和读都是字符串形式。写两个函数分别把字符串转化为链表,链表转化为字符串即可。

相关推荐
森旺电子2 小时前
51单片机仿真摇号抽奖机源程序 12864液晶显示
单片机·嵌入式硬件·51单片机
不过四级不改名6774 小时前
蓝桥杯嵌入式备赛教程(1、led,2、lcd,3、key)
stm32·嵌入式硬件·蓝桥杯
小A1594 小时前
STM32完全学习——SPI接口的FLASH(DMA模式)
stm32·嵌入式硬件·学习
Rorsion4 小时前
各种电机原理介绍
单片机·嵌入式硬件
善 .7 小时前
单片机的内存是指RAM还是ROM
单片机·嵌入式硬件
超级码农ProMax7 小时前
STM32——“SPI Flash”
stm32·单片机·嵌入式硬件
Asa3198 小时前
stm32点灯Hal库
stm32·单片机·嵌入式硬件
end_SJ9 小时前
初学stm32 --- 外部中断
stm32·单片机·嵌入式硬件
gantengsheng10 小时前
基于51单片机和OLED12864的小游戏《贪吃蛇》
单片机·嵌入式硬件·游戏·51单片机
嵌入式小强工作室11 小时前
stm32 查找进硬件错误方法
stm32·单片机·嵌入式硬件