代码:
注释写的较少,但本文出现的代码都有注释,所以请直接在本文里看注释
–来自百度网盘超级会员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的教程推荐:
其它硬件准备:
| 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路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}rn',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 位置为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('摇杆模式rn'); } 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('示教器模式rn'); } //获取蓝牙控制指令,A打头,后面一个字母就是指令内容 else if(Mode == 1 && UART1_RX_Buffer[0] == 'A') { cmd_BLE = UART1_RX_Buffer[1]; } else { if(UART1_RX_Buffer[0] != '') printf('指令发送错误:%srn', 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文件。
把蓝牙指令控制和摇杆控制放在一起判断了。
#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('Startrn'); 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}rn',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('Startrn'); 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}rn',angle[0],angle[1],angle[2],angle[3]); //printf('adc_dma = {%d, %d, %d, %d}rn',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('储存动作rn'); cmd_BLE = 's'; i++; } else if(cmd_BLE == 'm' && i>=9) printf('动作已满rn'); 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('rn'); if(memory[i][j] == '') break; } cmd_BLE = 's'; } else if(cmd_BLE == 'D') { for(i=0;i<10;i++) { memset(memory[i],'',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] == '') { 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('储存动作rn'); cmd_BLE = 's'; i++; } else { printf('动作已满rn'); 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('rn'); if(memory[i][j] == '') break; } cmd_BLE = 's'; break; case 'D': for(i=0; i < location_cnt ;i++) { memset(memory[i],'',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('摇杆模式rn'); } else if(!strcmp((const char *)UART1_RX_Buffer, 'M2')) { Mode = 2; printf('示教模式rn'); } else if(!strcmp((const char *)UART1_RX_Buffer, 'M3')) { Mode = 3; printf('执行记忆动作rn'); }
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 位置为1 UART1_RX_STA |= 0x8000; //=======中断信息处理======= //模式切换 if (!strcmp((const char *)UART1_RX_Buffer, 'M1')) { Mode = 1; printf('摇杆模式rn'); } else if(!strcmp((const char *)UART1_RX_Buffer, 'M2')) { Mode = 2; printf('示教模式rn'); } else if(!strcmp((const char *)UART1_RX_Buffer, 'M3')) { Mode = 3; printf('执行记忆动作rn'); } //获取蓝牙控制指令 else if(UART1_RX_Buffer[0] == 'A') { cmd_BLE = UART1_RX_Buffer[1]; } else { if(UART1_RX_Buffer[0] != '') printf('指令发送错误:%srn', 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 解释注释在前面说过
#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] == '') { 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('储存动作rn'); cmd_BLE = 's'; i++; } else { printf('动作已满rn'); 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('rn'); if(memory[i][j] == '') break; } cmd_BLE = 's'; break; case 'D': for(i=0; i < location_cnt ;i++) { memset(memory[i],'',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 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
#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('Startrn');//程序开始运行 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路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}rn',angle[0],angle[1],angle[2],angle[3]); printf('adc_dma1 = {%d, %d, %d, %d}rn',adc_dma[0],adc_dma[1],adc_dma[2],adc_dma[3]); printf('adc_dma2 = {%d, %d, %d, %d}rn',adc_dma[4],adc_dma[5],adc_dma[6],adc_dma[7]); printf('rn'); 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('摇杆模式rn'); 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('示教模式rn'); 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('储存动作rn'); i++; } else if(i>=9) { printf('动作已满rn'); } } 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('执行记忆动作rn'); 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;//链表里没记忆信息时编号只有0 head -> next = NULL; tail = head; temp = head; } void prinrt_List()//打印整个链表的内容 { temp = head; while(1) { printf('%d:{%d,%d,%d,%d}rn',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; }
#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('储存动作rn'); cmd_BLE = 's'; break; case 'g': prinrt_List(); cmd_BLE = 's'; break; case 'D': delete_List(); printf('已清除动作rn'); 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路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('储存动作rn'); }
到这里位置,视频里我实现的内容都可以完成了。
至于这个链表最大能存多长的动作我是没去实测,反正我们玩绝对是够用了。
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芯片rn'); else printf('读取到的芯片ID为:%xrn',flash_type); printf('未检测到W25Q128芯片rn'); } /** * @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:%xrn', 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:%Xrn', 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('数据写入完成!rn'); /* 读出测试数据 */ memset(datatemp, 0, TEXT_SIZE); w25q128_read(datatemp, FLASH_ReadAddress, TEXT_SIZE); printf('读出数据:%srn', datatemp); /* USER CODE END 2 */
运行结果第一句首先一定是都没读取到 w25q128 这个芯片,这段判断代码在w25q128_init();里
w25q128芯片分析与功能实现
以前在这个文章里讲过
总之, 整个存储空间的组成是
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操作的
所以通过前三位来选择不同的扇区来存数据
存和读都是字符串形式。写两个函数分别把字符串转化为链表,链表转化为字符串即可。
#artContent h1{font-size:16px;font-weight: 400;}
