基于STM32与FreeRTOS的四轴机械臂项目-2
https://developer.aliyun.com/article/1507997
四、裸机三种控制测试
首先我在Core文件夹里面的Src和Inc里分别创建pwm和oled的.c和.h文件
1.摇杆控制
摇杆控制在这里我用上面讲到的两个按键摇杆传感器模块实现,只需要开启四路ADC采集,两个按键摇杆传感器分别接收 IN0~3 ADC的模拟量,其中需要结合实际操作控制舵机初始化角度和运动过程中的角度变化。
代码示例:
pwm.c
extern uint16_t adc_dma[4];//DMA搬运的ADC采集值 extern int8_t angle[4] = {45,45,180,135};//舵机角度 //根据输入的0~180角度获取对应pwm占空比参数 unsigned char Angle(unsigned char pwm_pulse) { return pwm_pulse + 44; } //舵机A,夹爪 CH4_B11 45-135 张开闭合 初始化135 void sg90_A() { if(adc_dma[3] > 4000 && angle[3] < 135) { angle[3]++; } else if(adc_dma[3] <1000 && angle[3] > 45) { angle[3]--; } } //舵机B,上下 CH3_B10 45-180 初始化180 void sg90_B() { if(adc_dma[2] <1000 && angle[2] < 180) { angle[2]++; } else if(adc_dma[2] > 4000 && angle[2] > 45) { angle[2]--; } } //舵机C,前后 CH2_B3 45-180 前后 初始化45 void sg90_C() { if(adc_dma[1] <1000 && angle[1] < 180) { angle[1]++; } else if(adc_dma[1] > 4000 && angle[1] > 45) { angle[1]--; } } //舵机D,底座 CH1_A15 45-135 左到右 初始化45 void sg90_D() { if(adc_dma[0] <1000 && angle[0] < 135) { angle[0]++; } else if(adc_dma[0] > 4000 && angle[0] > 45) { angle[0]--; } }
pwm.h
#ifndef __PWM_H__ #define __PWM_H__ unsigned char Angle(unsigned char pwm_pulse); void sg90_A(void); void sg90_B(void); void sg90_C(void); void sg90_D(void); #endif
oled.c
#include "main.h" #include "i2c.h" #include "oled.h" /*-- 文字: 向 --*/ /*-- 宋体12; 此字体下对应的点阵为:宽x高=16x16 --*/ char x1[16] = {0x00,0xF8,0x08,0x08,0x0C,0xCA,0x49,0x48,0x48,0xC8,0x08,0x08,0x08,0xF8,0x00,0x00}; char x2[16] = {0x00,0xFF,0x00,0x00,0x00,0x1F,0x08,0x08,0x08,0x1F,0x00,0x40,0x80,0x7F,0x00,0x00}; /*-- 文字: 前 --*/ /*-- 宋体12; 此字体下对应的点阵为:宽x高=16x16 --*/ char f1[16] = {0x08,0x08,0xE8,0x29,0x2E,0x28,0xE8,0x08,0x08,0xC8,0x0C,0x0B,0xE8,0x08,0x08,0x00}; char f2[16] = {0x00,0x00,0xFF,0x09,0x49,0x89,0x7F,0x00,0x00,0x0F,0x40,0x80,0x7F,0x00,0x00,0x00}; /*-- 文字: 后 --*/ /*-- 宋体12; 此字体下对应的点阵为:宽x高=16x16 --*/ char b1[16] = {0x00,0x00,0x00,0xFC,0x24,0x24,0x24,0x24,0x22,0x22,0x22,0x23,0x22,0x20,0x20,0x00}; char b2[16] = {0x40,0x20,0x18,0x07,0x00,0xFE,0x42,0x42,0x42,0x42,0x42,0x42,0xFE,0x00,0x00,0x00}; /*-- 文字: 上 --*/ /*-- 宋体12; 此字体下对应的点阵为:宽x高=16x16 --*/ char u1[16] = {0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0x40,0x40,0x40,0x40,0x40,0x40,0x00,0x00,0x00}; char u2[16] = {0x40,0x40,0x40,0x40,0x40,0x40,0x7F,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x00}; /*-- 文字: 下 --*/ /*-- 宋体12; 此字体下对应的点阵为:宽x高=16x16 --*/ char d1[16] = {0x02,0x02,0x02,0x02,0x02,0x02,0xFE,0x02,0x02,0x42,0x82,0x02,0x02,0x02,0x02,0x00}; char d2[16] = {0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0x00,0x00,0x00,0x00,0x01,0x06,0x00,0x00,0x00}; /*-- 文字: 左 --*/ /*-- 宋体12; 此字体下对应的点阵为:宽x高=16x16 --*/ char l1[16] = {0x08,0x08,0x08,0x08,0x88,0x78,0x0F,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x00}; char l2[16] = {0x20,0x10,0x48,0x46,0x41,0x41,0x41,0x41,0x7F,0x41,0x41,0x41,0x41,0x40,0x40,0x00}; /*-- 文字: 右 --*/ /*-- 宋体12; 此字体下对应的点阵为:宽x高=16x16 --*/ char r1[16] = {0x08,0x08,0x08,0x08,0xC8,0x38,0x0F,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x00}; char r2[16] = {0x08,0x04,0x02,0x01,0xFF,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0xFF,0x00,0x00,0x00}; /*-- 文字: 张 --*/ /*-- 宋体12; 此字体下对应的点阵为:宽x高=16x16 --*/ char o1[16] = {0x02,0xE2,0x22,0x22,0x3E,0x80,0x80,0xFF,0x80,0xA0,0x90,0x88,0x86,0x80,0x80,0x00}; char o2[16] = {0x00,0x43,0x82,0x42,0x3E,0x00,0x00,0xFF,0x40,0x21,0x06,0x08,0x10,0x20,0x40,0x00}; /*-- 文字: 夹 --*/ /*-- 宋体12; 此字体下对应的点阵为:宽x高=16x16 --*/ char c1[16] = {0x00,0x08,0x08,0x28,0x48,0x08,0x08,0xFF,0x08,0x08,0x48,0x28,0x08,0x08,0x00,0x00}; char c2[16] = {0x81,0x81,0x41,0x41,0x21,0x11,0x0D,0x03,0x0D,0x11,0x21,0x41,0x41,0x81,0x81,0x00}; /*-- 文字: 爪 --*/ /*-- 宋体12; 此字体下对应的点阵为:宽x高=16x16 --*/ char z1[16] = {0x00,0x00,0x00,0xFC,0x04,0x04,0xFC,0x04,0x02,0x02,0xFE,0x03,0x02,0x00,0x00,0x00}; char z2[16] = {0x80,0x60,0x18,0x07,0x00,0x00,0x7F,0x00,0x00,0x00,0x01,0x0E,0x30,0x40,0x80,0x00}; void Oled_Write_Cmd(uint8_t dataCmd) { HAL_I2C_Mem_Write(&hi2c1, 0x78, 0x00, I2C_MEMADD_SIZE_8BIT, &dataCmd, 1, 0xff); } void Oled_Write_Data(uint8_t dataData) { HAL_I2C_Mem_Write(&hi2c1, 0x78, 0x40, I2C_MEMADD_SIZE_8BIT, &dataData, 1, 0xff); } 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_Screen_Clear(void){ unsigned char i,n; Oled_Write_Cmd (0x20); //set memory addressing mode Oled_Write_Cmd (0x02); //page addressing mode for(i=0;i<8;i++){ Oled_Write_Cmd(0xb0+i); // Oled_Write_Cmd(0x00); // Oled_Write_Cmd(0x10); // for(n=0;n<128;n++)Oled_Write_Data(0x00); } } void Oled_Show_open() { unsigned char i; Oled_Init(); // 选择一个位置确认页寻址模式 Oled_Write_Cmd(0x20); Oled_Write_Cmd(0x02); Oled_Screen_Clear(); // 选择PAGE0 1011 0000 0xB0 Oled_Write_Cmd(0xB0); Oled_Write_Cmd(0x00); Oled_Write_Cmd(0x10); for(i=0;i<16;i++){ Oled_Write_Data(o1[i]); } for(i=0;i<16;i++){ Oled_Write_Data(z1[i]); } Oled_Write_Cmd(0xB1); Oled_Write_Cmd(0x00); Oled_Write_Cmd(0x10); for(i=0;i<16;i++){ Oled_Write_Data(o2[i]); } for(i=0;i<16;i++){ Oled_Write_Data(z2[i]); } } void Oled_Show_close() { unsigned char i; Oled_Init(); Oled_Write_Cmd(0x20); Oled_Write_Cmd(0x02); Oled_Screen_Clear(); Oled_Write_Cmd(0xB0); Oled_Write_Cmd(0x00); Oled_Write_Cmd(0x10); for(i=0;i<16;i++){ Oled_Write_Data(c1[i]); } for(i=0;i<16;i++){ Oled_Write_Data(z1[i]); } Oled_Write_Cmd(0xB1); Oled_Write_Cmd(0x00); Oled_Write_Cmd(0x10); for(i=0;i<16;i++){ Oled_Write_Data(c2[i]); } for(i=0;i<16;i++){ Oled_Write_Data(z2[i]); } } void Oled_Show_up() { unsigned char i; Oled_Init(); Oled_Write_Cmd(0x20); Oled_Write_Cmd(0x02); Oled_Screen_Clear(); Oled_Write_Cmd(0xB0); Oled_Write_Cmd(0x00); Oled_Write_Cmd(0x10); for(i=0;i<16;i++){ Oled_Write_Data(x1[i]); } for(i=0;i<16;i++){ Oled_Write_Data(u1[i]); } Oled_Write_Cmd(0xB1); Oled_Write_Cmd(0x00); Oled_Write_Cmd(0x10); for(i=0;i<16;i++){ Oled_Write_Data(x2[i]); } for(i=0;i<16;i++){ Oled_Write_Data(u2[i]); } } void Oled_Show_down() { unsigned char i; Oled_Init(); Oled_Write_Cmd(0x20); Oled_Write_Cmd(0x02); Oled_Screen_Clear(); Oled_Write_Cmd(0xB0); Oled_Write_Cmd(0x00); Oled_Write_Cmd(0x10); for(i=0;i<16;i++){ Oled_Write_Data(x1[i]); } for(i=0;i<16;i++){ Oled_Write_Data(d1[i]); } Oled_Write_Cmd(0xB1); Oled_Write_Cmd(0x00); Oled_Write_Cmd(0x10); for(i=0;i<16;i++){ Oled_Write_Data(x2[i]); } for(i=0;i<16;i++){ Oled_Write_Data(d2[i]); } } void Oled_Show_left() { unsigned char i; Oled_Init(); Oled_Write_Cmd(0x20); Oled_Write_Cmd(0x02); Oled_Screen_Clear(); Oled_Write_Cmd(0xB0); Oled_Write_Cmd(0x00); Oled_Write_Cmd(0x10); for(i=0;i<16;i++){ Oled_Write_Data(x1[i]); } for(i=0;i<16;i++){ Oled_Write_Data(l1[i]); } Oled_Write_Cmd(0xB1); Oled_Write_Cmd(0x00); Oled_Write_Cmd(0x10); for(i=0;i<16;i++){ Oled_Write_Data(x2[i]); } for(i=0;i<16;i++){ Oled_Write_Data(l2[i]); } } void Oled_Show_right() { unsigned char i; Oled_Init(); Oled_Write_Cmd(0x20); Oled_Write_Cmd(0x02); Oled_Screen_Clear(); Oled_Write_Cmd(0xB0); Oled_Write_Cmd(0x00); Oled_Write_Cmd(0x10); for(i=0;i<16;i++){ Oled_Write_Data(x1[i]); } for(i=0;i<16;i++){ Oled_Write_Data(r1[i]); } Oled_Write_Cmd(0xB1); Oled_Write_Cmd(0x00); Oled_Write_Cmd(0x10); for(i=0;i<16;i++){ Oled_Write_Data(x2[i]); } for(i=0;i<16;i++){ Oled_Write_Data(r2[i]); } } void Oled_Show_front() { unsigned char i; Oled_Init(); Oled_Write_Cmd(0x20); Oled_Write_Cmd(0x02); Oled_Screen_Clear(); Oled_Write_Cmd(0xB0); Oled_Write_Cmd(0x00); Oled_Write_Cmd(0x10); for(i=0;i<16;i++){ Oled_Write_Data(x1[i]); } for(i=0;i<16;i++){ Oled_Write_Data(f1[i]); } Oled_Write_Cmd(0xB1); Oled_Write_Cmd(0x00); Oled_Write_Cmd(0x10); for(i=0;i<16;i++){ Oled_Write_Data(x2[i]); } for(i=0;i<16;i++){ Oled_Write_Data(f2[i]); } } void Oled_Show_behind() { unsigned char i; Oled_Init(); Oled_Write_Cmd(0x20); Oled_Write_Cmd(0x02); Oled_Screen_Clear(); Oled_Write_Cmd(0xB0); Oled_Write_Cmd(0x00); Oled_Write_Cmd(0x10); for(i=0;i<16;i++){ Oled_Write_Data(x1[i]); } for(i=0;i<16;i++){ Oled_Write_Data(b1[i]); } Oled_Write_Cmd(0xB1); Oled_Write_Cmd(0x00); Oled_Write_Cmd(0x10); for(i=0;i<16;i++){ Oled_Write_Data(x2[i]); } for(i=0;i<16;i++){ Oled_Write_Data(b2[i]); } }
oled.h
#ifndef __OLED_H__ #define __OLED_H__ void Oled_Write_Cmd(uint8_t dataCmd); void Oled_Write_Data(uint8_t dataData); void Oled_Init(void); void Oled_Screen_Clear(void); // oled显示封装 void Oled_Show_open(); void Oled_Show_close(); void Oled_Show_up(); void Oled_Show_down(); void Oled_Show_left(); void Oled_Show_right(); void Oled_Show_front(); void Oled_Show_behind(); #endif
main.c
uint16_t adc_dma[8];//DMA搬运的ADC采集值 uint8_t angle[4] = {45,45,180,135};//舵机角度 uint8_t cnt = 0;//计数用,定时串口打印信息 //覆写printf,用于串口打印数据 int fputc(int ch, FILE *f) { unsigned char temp[1]={ch}; HAL_UART_Transmit(&huart1,temp,1,0xffff); return ch; } //int main //开始ADC和DMA采集 HAL_ADC_Start_DMA(&hadc1,(uint32_t *)adc_dma,4); //开启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); printf("test\r\n"); while (1) { sg90_A(); sg90_B(); sg90_C(); sg90_D(); //输出PWM波使舵机运动 __HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_1, Angle(angle[0])); __HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_2, Angle(angle[1])); __HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_3, Angle(angle[2])); __HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_4, Angle(angle[3])); cnt++;//计数,每循环一次+1 if(cnt>= 50)//循环50次,每次20ms,即一共1s。每一秒发送一次数据 { printf("Angle = {%d, %d, %d, %d}\r\n",angle[0],angle[1],angle[2],angle[3]); cnt = 0; } HAL_Delay(20);//每20ms循环一次(改成15更流畅) }
2.示教器控制
示教器控制在这里我用上面讲到的四个旋钮电位器模块实现,跟上面摇杆控制一样只需要开启四路ADC采集,代码跟摇杆控制基本差不多,新增加一个函数封装把采集的模拟值转换为角度,即0~4095 变为 0~180,除以22.75即可
在pwm.c上新增加一个转换函数
void translate()//直接用8通道就是adc_dma[4~7] { 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 - 即可 }
在main.c上增加打印调试信息在while(1)循环里面
printf("adc_dma = {%d, %d, %d, %d}\r\n",adc_dma[0],adc_dma[1],adc_dma[2],adc_dma[3]);
3.蓝牙控制
使用蓝牙模块,打开串口中断,用串口调试助手查看中断测试收发的数据,只需要新增加串口接收中断代码和在原先的pwm.c上面做一些修改
usart.c
#include "stdio.h" #include "string.h" #include "pwm.h" #include "adc.h" #include "dma.h" /*蓝牙控制机械臂指令: 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; //=======中断信息处理======= //获取蓝牙控制指令,A打头,后面一个字母就是指令内容 if(UART1_RX_Buffer[0] == 'A') { HAL_ADC_Stop_DMA(&hadc1);//停止ADC DMA MX_ADC1_Init();//初始化ADC1 HAL_ADC_Start_DMA(&hadc1,(uint32_t *)adc_dma,4); //开启ADC DMA cmd_BLE = UART1_RX_Buffer[1]; } else { if(UART1_RX_Buffer[0] != '\0') printf("指令发送错误:%s\r\n", UART1_RX_Buffer); } //========================== memset(UART1_RX_Buffer, 0, strlen((const char *)UART1_RX_Buffer)); // 重新开始下一次接收 UART1_RX_STA = 0; //========================== } else // 否则认为接收错误,重新开始 UART1_RX_STA = 0; } else // 如果没有收到了 0x0d (回车) { //则先判断收到的这个字符是否是 0x0d (回车) if(buf == 0x0d) { // 是的话则将 bit14 位置为1 UART1_RX_STA |= 0x4000; } else { // 否则将接收到的数据保存在缓存数组里 UART1_RX_Buffer[UART1_RX_STA & 0X3FFF] = buf; UART1_RX_STA++; // 如果接收数据大于UART1_REC_LEN(200字节),则重新开始接收 if(UART1_RX_STA > UART1_REC_LEN - 1) UART1_RX_STA = 0; } } } // 重新开启中断 HAL_UART_Receive_IT(&huart1, &buf, 1); } } // 在串口初始化中开启接收中断 HAL_UART_Receive_IT(&huart1, &buf, 1);
pwm.c
#include "pwm.h" #include "main.h" #include "oled.h" extern uint16_t adc_dma[8];//DMA搬运的ADC采集值 extern uint8_t angle[4];//舵机角度 extern uint8_t Mode; extern uint8_t cmd_BLE; //根据输入的0~180角度获取对应pwm占空比参数 unsigned char Angle(unsigned char pwm_pulse) { return pwm_pulse + 44; } //舵机A,夹爪 CH4_B11 45-135 张开闭合 初始化135 void sg90_A() { if((cmd_BLE == 'c' || adc_dma[3] > 4000) && angle[3] < 135)//合 { angle[3]++; Oled_Show_close(); } else if((cmd_BLE == 'o' || adc_dma[3] <1000) && angle[3] > 45)//开 { angle[3]--; Oled_Show_open(); } } //舵机B,上下 CH3_B10 45-180 初始化180 void sg90_B() { if((cmd_BLE == 'u' || adc_dma[2] <1000) && angle[2] < 180)//上 { angle[2]++; Oled_Show_up(); } else if((cmd_BLE == 'd' || adc_dma[2] > 4000) && angle[2] > 45)//下 { angle[2]--; Oled_Show_down(); } } //舵机C,前后 CH2_B3 45-180 前后 初始化45 void sg90_C() { if((cmd_BLE == 'f' || adc_dma[1] <1000) && angle[1] < 180)//前 { angle[1]++; Oled_Show_front(); } else if((cmd_BLE == 'b' || adc_dma[1] > 4000) && angle[1] > 45)//后 { angle[1]--; Oled_Show_behind(); } } //舵机D,底座 CH1_A15 45-135 左到右 初始化45 void sg90_D() { if((cmd_BLE == 'l' || adc_dma[0] <1000) && angle[0] < 135)//左 { angle[0]++; Oled_Show_left(); } else if((cmd_BLE == 'r' || adc_dma[0] > 4000) && angle[0] > 45)//右 { angle[0]--; Oled_Show_right(); } }
五、裸机与FreeRTOS
移植 FreeRTOS 到 STM32F103C8T6上我们可以手动移植或者使用CubeMX快速移植,在这里我用CubeMX快速移植,具体介绍可看我之前写过的文章,链接如下:CubeMx快速移植FreeRTOS
接着我们要创建三个任务,一个任务负责角度信息处理,一个任务负责串口收发数据,一个任务负责显示OLED屏幕,具体创建删除任务介绍可看我之前写过的文章,链接如下:任务的创建和删除
1.CubeMX配置
2.移植裸机三种控制代码
在这里我们只需要在CubeMX生成的freertos.c文件中移植我们裸机控制的代码放在对应的任务中,可自行进行代码扩展,例如:
六、项目演示视频
四轴机械臂演示视频