当前位置: 首页 > news >正文

基于STM32+FreeRTOS的四轴机械臂

目录

代码:

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

项目概述:

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

 二 裸机测试功能

1.摇杆控制

接线:

CubeMX配置:

代码

2.蓝牙控制

接线:

CubeMX配置

代码:

3.示教器控制

4.记录动作信息

5.执行记录的动作

注:

三 FreeRTOS上完成项目

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 即可

代码:

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

/* USER CODE BEGIN */。。。。 。。。。/* USER CODE END */

main.c

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

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

/* 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路PWMHAL_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++;//计数,每循环一次+1if(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。

/* 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

/* USER CODE BEGIN Prototypes */
void MX_ADC1_Mode2_Init(void);
/* USER CODE END Prototypes */

usart.c

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

	//=======中断信息处理=======。。。。。。。。        //==========================
/* 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 位置为1UART1_RX_STA |= 0x8000;//=======中断信息处理=======//模式切换if (!strcmp((const char *)UART1_RX_Buffer, "M1")) {Mode = 1;HAL_ADC_Stop_DMA(&hadc1);//停止ADC DMAMX_ADC1_Init();//初始化ADC1HAL_ADC_Start_DMA(&hadc1,(uint32_t *)adc_dma,4); //开启ADC DMAprintf("摇杆模式\r\n");}else if(!strcmp((const char *)UART1_RX_Buffer, "M2")) {Mode = 2;HAL_ADC_Stop_DMA(&hadc1);//停止ADC DMAMX_ADC1_Mode2_Init();//自定义初始化ADC1,把1234换成5678采集电位器HAL_ADC_Start_DMA(&hadc1,(uint32_t *)adc_dma,4); //开启ADC DMAprintf("示教器模式\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 位置为1UART1_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文件。

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

#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]--;}}}
#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 

/* 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添加了一些代码,直接修改上面代码即可。

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

#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

  /* 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 ,对应我们这里 记录当前角度、获取所有记录的角度、删除所有记录。

#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是数组长度,宏定义出来就行,自己调整长度

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

/*机械臂控制模式,默认为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循环一样理解就行,一样用

  /* 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

/* 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 位置为1UART1_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 位置为1UART1_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        解释注释在前面说过

#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

#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

#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 offOled_Write_Cmd(0x00);//---set low column addressOled_Write_Cmd(0x10);//---set high column addressOled_Write_Cmd(0x40);//--set start line addressOled_Write_Cmd(0xB0);//--set page addressOled_Write_Cmd(0x81); // contract controlOled_Write_Cmd(0xFF);//--128Oled_Write_Cmd(0xA1);//set segment remapOled_Write_Cmd(0xA6);//--normal / reverseOled_Write_Cmd(0xA8);//--set multiplex ratio(1 to 64)Oled_Write_Cmd(0x3F);//--1/32 dutyOled_Write_Cmd(0xC8);//Com scan directionOled_Write_Cmd(0xD3);//-set display offsetOled_Write_Cmd(0x00);//Oled_Write_Cmd(0xD5);//set osc divisionOled_Write_Cmd(0x80);//Oled_Write_Cmd(0xD8);//set area color mode offOled_Write_Cmd(0x05);//Oled_Write_Cmd(0xD9);//Set Pre-Charge PeriodOled_Write_Cmd(0xF1);//Oled_Write_Cmd(0xDA);//set com pin configuartionOled_Write_Cmd(0x12);//Oled_Write_Cmd(0xDB);//set VcomhOled_Write_Cmd(0x30);//Oled_Write_Cmd(0x8D);//set charge pump enableOled_Write_Cmd(0x14);//Oled_Write_Cmd(0xAF);//--turn on oled panel
}void Oled_Clear()
{unsigned char i,j; //-128 --- 127for(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-2unsigned int  i;Oled_Write_Cmd(0xb0+(row*2-2));                           //page 0Oled_Write_Cmd(0x00+(col&0x0f));                          //lowOled_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 1Oled_Write_Cmd(0x00+(col&0x0f));                          //lowOled_Write_Cmd(0x10+(col>>4));                            //highfor(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

#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        放了一点初始化配置

/* 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本身有硬件消抖,所以不用软件定时器消抖。

/* 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路PWMHAL_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是格子,每个格子1cmsprintf(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文件,放链表相关代码

#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;//链表里没记忆信息时编号只有0head -> 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;elsetemp = temp->next;return p;
}uint8_t sizeof_List()//别管名字,看temp在哪用的,
{//反应在OLED上就是可以看见当前存储到第几个动作了或者正在执行第一个动作return temp->cnt;
}
#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

#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要修改的部分

//                任务1/* USER CODE END Header_Start_check_angle */
void Start_check_angle(void const * argument)
{/* USER CODE BEGIN Start_check_angle */Memary_Init();//开启4路PWMHAL_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是格子,每个格子1cmsprintf(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,别的都不用动

/* 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

/* 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就改成对应的。

#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

#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");elseprintf("读取到的芯片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函数里用这段代码测试 

  /* 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操作的

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

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

相关文章:

基于STM32+FreeRTOS的四轴机械臂

目录 代码&#xff1a; 注释写的较少&#xff0c;但本文出现的代码都有注释&#xff0c;所以请直接在本文里看注释 项目概述&#xff1a; 一 准备阶段&#xff08;都是些废话&#xff09; 二 裸机测试功能 1.摇杆控制 接线&#xff1a; CubeMX配置&#xff1a; 代码 2…...

【C语言】三子棋游戏——超细教学

&#x1f6a9;纸上得来终觉浅&#xff0c; 绝知此事要躬行。 &#x1f31f;主页&#xff1a;June-Frost &#x1f680;专栏&#xff1a;C语言 &#x1f525;该篇将结合之前的知识来实现 三子棋游戏。 目录&#xff1a; &#x1f31f;思路框架&#xff1a;测试游戏 &#x1f31f…...

redux的介绍、安装、三大核心与执行流程

redux的介绍、安装、三大核心与执行流程 一、redux的基本介绍二、redux的安装三、redux核心概念3.1 action3.2 reducer3.3 store 四、Redux代码执行流程五、加减案例练习 一、redux的基本介绍 redux中文官网Redux 是 React 中最常用的状态管理工具&#xff08;状态容器&#x…...

Redis 5环境搭建

一、环境搭建 如果是Centos8&#xff0c;yum 仓库中默认的 Redis版本就是5&#xff0c;直接yum install即可。如果是Centos7&#xff0c;yum 仓库中默认的 Redis版本是3系列&#xff0c;比较老~ 为了我们能在 Centos7中下载到 Redis5 首先要安装额外的软件源 sudo yum insta…...

stm32红绿灯源代码示例(附带Proteus电路图)

本代码不能直接用于红路灯&#xff0c;只是提供一个思路 #include "main.h" #include "gpio.h" void SystemClock_Config(void); void MX_GPIO_Init(void) {GPIO_InitTypeDef GPIO_InitStruct {0};/* GPIO Ports Clock Enable */__HAL_RCC_GPIOB_CLK_ENAB…...

Qt与电脑管家4

折线图&#xff1a; #ifndef LINE_CHART_H #define LINE_CHART_H#include <QWidget> #include <QPainter> #include "circle.h" class line_chart : public QWidget {Q_OBJECT public:explicit line_chart(QWidget *parent nullptr); protected:void pa…...

使用css美化gradio界面

基本方法 在默认的前端页面中使用检查工具确定要修改的部分的选择器名称&#xff0c;然后在block_css中对其修改&#xff0c;并在启动网页时传入参数&#xff1a;with gr.Blocks(cssblock_css, thememy_theme) as demo: 禁止修改下拉框文字 input.border-none.svelte-c0u3f0…...

Flink流批一体计算(13):PyFlink Tabel API之SQL DDL

1. TableEnvironment 创建 TableEnvironment from pyflink.table import Environmentsettings, TableEnvironment# create a streaming TableEnvironmentenv_settings Environmentsettings.in_streaming_mode()table_env TableEnvironment.create(env_settings)# or create…...

java笔试手写算法面试题大全含答案

1.统计一篇英文文章单词个数。 public class WordCounting { public static void main(String[] args) { try(FileReader fr new FileReader("a.txt")) { int counter 0; boolean state false; int currentChar; while((currentChar fr.read()) ! -1) { i…...

点云平面拟合和球面拟合

一、介绍 In this tutorial we learn how to use a RandomSampleConsensus with a plane model to obtain the cloud fitting to this model. 二、代码 #include <iostream> #include <thread> #include <pcl/point_types.h> #include <pcl/common/io.…...

部署问题集合(十九)linux设置Tomcat、Docker,以及使用脚本开机自启(亲测)

前言 因为不想每次启动虚拟机都要手动启动一遍这些东西&#xff0c;所以想要设置成开机自启的状态 设置Tomcat开机自启 创建service文件 vi /etc/systemd/system/tomcat.service添加如下内容&#xff0c;注意修改启动脚本和关闭脚本的地址 [Unit] DescriptionTomcat9068 A…...

视觉SLAM:一直在入门,如何能精通,CV领域的绝境长城,

目录 前言 福利&#xff1a;文末有chat-gpt纯分享&#xff0c;无魔法&#xff0c;无限制 1 什么是SLAM&#xff1f; 2 为什么用SLAM&#xff1f; 3 视觉SLAM怎么实现&#xff1f; 4 前端视觉里程计 5 后端优化 6 回环检测 7 地图构建 8 结语 前言 上周的组会上&…...

【报错】yarn --version Unrecognized option: --version Error...

文章目录 问题分析解决问题 在使用 npm install -g yarn 全局安装 yarn 后,查看yarn 的版本号,报错如下 PS D:\global-data-display> yarn --version Unrecognized option: --version Error: Could...

二叉搜索树的(查找、插入、删除)

一、二叉搜索树的概念 二叉搜索树又称二叉排序树&#xff0c;它或者是一棵空树&#xff0c;或者是具有以下性质的二叉树: 1、若它的左子树不为空&#xff0c;则左子树上所有节点的值都小于根节点的值&#xff1b; 2、若它的右子树不为空&#xff0c;则右子树上所有节点的值都…...

电力虚拟仿真 | 高压电气试验VR教学系统

在科技进步的推动下&#xff0c;我们的教育方式也在发生着翻天覆地的变化。其中&#xff0c;虚拟现实&#xff08;VR&#xff09;技术的出现&#xff0c;为我们提供了一种全新的、富有沉浸感的学习和培训方式。特别是在电力行业领域&#xff0c;例如&#xff0c;电力系统的维护…...

innovus如何设置size only

我正在「拾陆楼」和朋友们讨论有趣的话题&#xff0c;你⼀起来吧&#xff1f; 拾陆楼知识星球入口 给instance设置size only属性命令如下: dbset [dbGet top.inst.name aa/bb -p] .dontTouch sizeOk 给一个module设置size only需要foreach循环一下: foreach inst [dbGet top.…...

Java之继承详解二

3.7 方法重写 3.7.1 概念 方法重写 &#xff1a;子类中出现与父类一模一样的方法时&#xff08;返回值类型&#xff0c;方法名和参数列表都相同&#xff09;&#xff0c;会出现覆盖效果&#xff0c;也称为重写或者复写。声明不变&#xff0c;重新实现。 3.7.2 使用场景与案例…...

国内常见的几款可视化Web组态软件

组态软件是一种用于控制和监控各种设备的软件&#xff0c;也是指在自动控制系统监控层一级的软件平台和开发环境。这类软件实际上也是一种通过灵活的组态方式&#xff0c;为用户提供快速构建工业自动控制系统监控功能的、通用层次的软件工具。通常用于工业控制&#xff0c;自动…...

通过 git上传到 gitee 仓库

介绍 Git是目前世界上最先进的分布式版本控制系统&#xff0c;有这么几个特点&#xff1a; 分布式 &#xff1a;是用来保存工程源代码历史状态的命令行工具。保存点 &#xff1a;保存点可以追溯源码中的文件&#xff0c;并能得到某个时间点上的整个工程项目额状态&#xff1b;…...

设置Windows主机的浏览器为wls2的默认浏览器

1. 准备工作 wsl是可以使用Windows主机上安装的exe程序&#xff0c;出于安全考虑&#xff0c;默认情况下改功能是无法使用。要使用的话&#xff0c;终端需要以管理员权限启动。 我这里以Windows Terminal为例&#xff0c;介绍如何默认使用管理员权限打开终端&#xff0c;具体…...

森林生物量(蓄积量)估算全流程

python森林生物量&#xff08;蓄积量&#xff09;估算全流程 一.哨兵2号获取/去云处理/提取参数1.1 影像处理与下载1.2 导入2A级产品1.3导入我们在第1步生成的云掩膜文件1.4.SNAP掩膜操作1.5采用gdal计算各类植被指数1.6 纹理特征参数提取 二.哨兵1号获取/处理/提取数据2.1 纹理…...

MySQL数据库概述

MySQL数据库概述 1 SQL SQL语句大小写不敏感。 SQL语句末尾应该使用分号结束。 1.1 SQL语句及相关操作示例 DDL&#xff1a;数据定义语言&#xff0c;负责数据库定义、数据库对象定义&#xff0c;由CREATE、ALTER与DROP三个语法所组成DML&#xff1a;数据操作语言&#xff…...

2023年国赛数学建模思路 - 案例:退火算法

文章目录 1 退火算法原理1.1 物理背景1.2 背后的数学模型 2 退火算法实现2.1 算法流程2.2算法实现 建模资料 ## 0 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 1 退火算法原理 1.1 物理背景 在热力学上&a…...

怎么借助ChatGPT处理数据结构的问题

目录 使用ChatGPT进行数据格式化转换 代码示例 ChatGPT格式化数据提示语 代码示例 批量格式化数据提示语 代码示例 ChatGPT生成的格式化批处理代码 使用ChatGPT合并不同数据源的数据 合并数据提示语 自动合并数据提示语 ChatGPT生成的自动合并代码 结论 数据合并是…...

Docker容器无法启动 Cannot find /usr/local/tomcat/bin/setclasspath.sh

报错信息如下 解决办法 权限不够 加上--privileged 获取最大权限 docker run --privileged --name lenglianerqi -p 9266:8080 -v /opt/docker/lenglianerqi/webapps:/usr/local/tomcat/webapps/ -v /opt/docker/lenglianerqi/webapps/userfile:/usr/local/tomcat/webapps/u…...

Pytorch-day08-模型进阶训练技巧-checkpoint

PyTorch 模型进阶训练技巧 自定义损失函数动态调整学习率 典型案例&#xff1a;loss上下震荡 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BndMyRX0-1692613806232)(attachment:image-2.png)] 1、自定义损失函数 1、PyTorch已经提供了很多常用…...

【ArcGIS Pro二次开发】(61):样式(Style)和符号(Symbol)

在 ArcGIS Pro SDK 中&#xff0c;地图要素符号&#xff08;Symbol&#xff09;和符号样式&#xff08;Style&#xff09;是2个很重要的概念。 【Symbol】是用于表示地图上不同类型的要素&#xff08;如点、线、面&#xff09;的图形化表示。 在地图中&#xff0c;各种要素都…...

深入理解 HTTP/2:提升 Web 性能的秘密

HTTP/2 是一项重大的网络协议升级&#xff0c;旨在提升 Web 页面加载速度和性能。在这篇博客中&#xff0c;我们将深入探讨 HTTP/2 的核心概念以及如何使用它来加速网站。 什么是 HTTP/2&#xff1f; HTTP/2 是 HTTP 协议的下一个版本&#xff0c;旨在解决 HTTP/1.1 中的性能…...

800V高压电驱动系统架构分析

需要电驱竞品样件请联&#xff1a;shbinzer &#xff08;拆车邦&#xff09; 过去一年是新能源汽车市场爆发的一年&#xff0c;据中汽协数据&#xff0c;2021年新能源汽车销售352万辆&#xff0c;同比大幅增长157.5%。新能源汽车技术发展迅速&#xff0c;畅销车辆在动力性能…...

Camunda_3:主动撤回

貌似国际主流认知工作流通常不支持撤回/驳回&#xff0c;流程只能向前进行。而撤回/驳回算是一种中国特色吧。 因此Camunda对于流程修改也仅仅提供了runtimeService.createProcessInstanceModification(instanceId)来修改流程。对于撤回/驳回这种操作得自己想办法。通常的撤回/…...

ClickHouse(二十三):Java Spark读写ClickHouse API

进入正文前&#xff0c;感谢宝子们订阅专题、点赞、评论、收藏&#xff01;关注IT贫道&#xff0c;获取高质量博客内容&#xff01; &#x1f3e1;个人主页&#xff1a;含各种IT体系技术&#xff0c;IT贫道_Apache Doris,大数据OLAP体系技术栈,Kerberos安全认证-CSDN博客 &…...

Linux下的GPIO基本概念指南

一、什么是GPIO 在Linux中&#xff0c;GPIO(General Purpose Input/Output&#xff0c;通用输入输出)是一种用于控制外部设备和传感器的通用接口。它允许你通过软件控制数字信号&#xff0c;从而实现各种硬件设备的交互&#xff0c;如LED、按钮、传感器、马达等。 每个GPIO引脚…...

快速解决Spring Boot跨域困扰:使用CORS实现无缝跨域支持

跨域问题 什么是跨域&#xff1f; 跨域&#xff08;Cross-Origin Issue&#xff09;的存在是因为浏览器的安全限制&#xff0c;它防止恶意网站利用跨域请求来获取用户的敏感信息或执行恶意操作。浏览器通过实施同源策略来限制网页在不同源之间进行资源访问或交互的情况。当一…...

【【萌新的STM32学习-13之GPIO寄存器的用法】】

萌新的STM32学习-13之GPIO寄存器的用法 从外部来看我们有很多个GPIO 分为ABCDEF等等 每个GPIO都有&#xff11;&#xff16;个引脚 每个引脚的名字是PA&#xff10;到PA&#xff11;&#xff15; 这是外部的看法 对于内部 引脚自然会有引脚的功能传入的模式 状态 频率 等…...

Android开发基础知识总结(一)初识安卓Android Studio

一.基础理论知识 1.Linux相当于是地基。 MIUI&#xff0c;EMUI等操作系统&#xff0c;是基于安卓的改版——且裁掉了一部分Google的服务。 &#xff08;鸿蒙虽然是改版&#xff0c;但和安卓的架构基本上一致&#xff09; 2.Kotlin和Java都是JVM语言&#xff0c;必须先复习好…...

常见的网络设备有哪些?分别有什么作用?

个人主页&#xff1a;insist--个人主页​​​​​​ 本文专栏&#xff1a;网络基础——带你走进网络世界 本专栏会持续更新网络基础知识&#xff0c;希望大家多多支持&#xff0c;让我们一起探索这个神奇而广阔的网络世界。 目录 一、网络设备的概述 二、常见的网络设备 1、…...

斗鱼财报盈利的背后:左手艳舞、右手擦边

本月14日&#xff0c;直播平台斗鱼发布了其第二季度财报&#xff0c;面对“看起来还不错的数据”&#xff0c;其对外着重强调了“连续两个季度实现盈利”&#xff0c;并称“斗鱼收入结构持续优化”“斗鱼盈利能力提升”“斗鱼稳健增长可期”“督导提升了内容审核能力”。 财报…...

布隆过滤器

思考一个问题&#xff1a;如果我想判断一个元素是否存在某个集合里面怎么做&#xff1f; 一般的解决方案是先把所有元素保存起来&#xff0c;然后通过循环比较来确定。 但是如果我们有几千万甚至上亿的数据的时候&#xff5d;&#xff0c;虽然可以通过不同的数据结构来优化数据…...

element-ui中二次封装一个带select的form组件

带select的form组件 样式 代码 <template><el-form-item label"是否有" class"append" prop"tag"><el-form-itemprop"isShare"><el-select v-model"query.tag"><el-option v-for"(item, …...

07.利用Redis实现点赞排行榜功能

学习目标&#xff1a; 提示&#xff1a;学习如何利用Redisson实现点赞排行榜功能&#xff0c;按照时间顺序 当用户给某一篇文章点赞后&#xff0c;会再数据库中存储一条数据&#xff0c;并且在Redis中存储一条数据为当前博客的点赞用户标识&#xff0c;来区分哪个用户对文章进…...

【前端vue升级】vue2+js+elementUI升级为vue3+ts+elementUI plus

一、工具的选择 近期想将vuejselementUI的项目升级为vue3tselementUI plus&#xff0c;以获得更好的开发体验&#xff0c;并且vue3也显著提高了性能&#xff0c;所以在此记录一下升级的过程对于一个正在使用的项目手工替换肯定不是个可实现的解决方案&#xff0c;更优方案是基于…...

多维时序 | MATLAB实现SCNGO-BiLSTM-Attention多变量时间序列预测

多维时序 | MATLAB实现SCNGO-BiLSTM-Attention多变量时间序列预测 目录 多维时序 | MATLAB实现SCNGO-BiLSTM-Attention多变量时间序列预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 多维时序 | MATLAB实现SCNGO-BiLSTM-Attention多变量时间序列预测。 模型描…...

go-test

单元测试 基本用法 Go语言测试 常用reflect.DeepEqual()对slice进行比较 跳过某些测试用例 func TestTimeConsuming(t *testing.T) {if testing.Short() {t.Skip("short模式下会跳过该测试用例")}... }当执行go test -short时就不会执行上面的TestTimeConsuming测…...

假设你新换了电脑,如何不用U盘的情况下实现软件文件转移?

要将笔记本和台式机连接到同一个局域网&#xff0c;并实现文件共享或使用文件传输协议进行文件传输&#xff0c;您可以按照以下步骤操作&#xff1a; 设置局域网连接共享文件夹使用文件传输协议 Step 1: 设置局域网连接 确保笔记本和台式机连接到同一个局域网。有几种常见的…...

聊聊 Docker

聊聊 Docker Docker 是什么&#xff1f; 定义 Docker 是一款 开源的应用容器引擎。 简单来说&#xff0c;就是 以容器虚拟化技术为基础的软件。可以把应用程序和所依赖的包一起打包到一个可移植的镜像中&#xff0c;发布到 Linux 或者 Windows 上运行。&#xff08;代码 运…...

运行软件mfc140u.dll丢失怎么办?mfc140u.dll的三个修复方法

最近我在使用一款软件时遇到了一个问题&#xff0c;提示缺少mfc140u.dll文件。。这个文件是我在使用某个应用程序时所需要的&#xff0c;但是由于某种原因&#xff0c;它变得无法正常使用了。经过一番搜索和了解&#xff0c;我了解到mfc140u.dll是Microsoft Visual Studio 2015…...

神经网络基础-神经网络补充概念-54-softmax回归

概念 Softmax回归&#xff08;Softmax Regression&#xff09;是一种用于多分类任务的机器学习算法&#xff0c;特别是在神经网络中常用于输出层来进行分类。它是Logistic回归在多分类问题上的推广。 原理 Softmax回归的主要思想是将原始的线性分数&#xff08;得分&#xf…...

米尔瑞萨RZ/G2L开发板-02 ffmpeg的使用和RTMP直播

最近不知道是不是熬夜太多&#xff0c;然后记忆力减退了&#xff1f; 因为板子回来以后我就迫不及待的试了一下板子&#xff0c;然后发现板子有SSH&#xff0c;但是并没有ffmpeg&#xff0c;最近总是在玩&#xff0c;然后今天说是把板子还原一下哇&#xff0c;然后把官方的固件…...

基于swing的在线考试系统java jsp线上试卷问答mysql源代码

本项目为前几天收费帮学妹做的一个项目&#xff0c;Java EE JSP项目&#xff0c;在工作环境中基本使用不到&#xff0c;但是很多学校把这个当作编程入门的项目来做&#xff0c;故分享出本项目供初学者参考。 一、项目描述 基于swing的在线考试系统 系统有2权限&#xff1a;管…...

C# 读取pcd点云文件数据

pcd文件有ascii 和二进制格式&#xff0c;ascii可以直接记事本打开&#xff0c;C#可以一行行读。但二进制格式的打开是乱码&#xff0c;如果尝试程序中读取&#xff0c;对比下看了数据也对不上。 这里可以使用pcl里的函数来读取pcd&#xff0c;无论二进制或ascii都可以正确读取…...