目录
一、项目介绍
二、前期准备
1.硬件准备
2.开发环境
3.CubeMX配置
三、裸机各种模块测试
1.舵机模块
2.蓝牙模块
3.按键摇杆传感器模块和旋钮电位器模块
4.OLED模块
5.W25Q128模块
四、裸机三种控制测试
1.摇杆控制
2.示教器控制
3.蓝牙控制
五、裸机与FreeRTOS
1.CubeMX配置
2.移植裸机三种控制代码
六、项目演示视频
一、项目介绍
该项目是基于FreeRTOS实时操作系统,主控为 STM32F103C8T6 开发板 ,机械臂为四轴分别被四个舵机控制。本项目实现了 3 种控制方法,分别为摇杆控制、示教器控制和串口蓝牙控制,采用8路ADC采集按键摇杆传感器和旋钮电位器的模拟量并由DMA搬运数据,可自制手机蓝牙APP或者直接使用官方手机蓝牙助手作为上位机,USART串口蓝牙实时收发信息,IIC驱动OLED屏幕实时显示机械臂移动张爪夹爪信息,人为控制抓取目标物。
扩展:后续可以通过二维数组或者链表实现存储动作,通过SPI驱动W25Q128模块进行动作记忆扩容,即可以录制上百组动作,还可以附加树莓派等开发板进行视觉抓取开发,等以后有时间我再把扩展功能一起实现呈现给大家。
二、前期准备
1.硬件准备
本项目可用步进电机和驱动器作为支撑,以便提高项目扩展性,但在这里我直接用四个舵机实现。
首先你可以自己建模3D打印四轴机械臂模型,也可以直接去网上购买一套成品套件。然后需要四个舵机控制机械臂,型号无所谓,控制起来是一样的,注意需要是180度的角度型舵机,而不是360度的速度型舵机。
然后需要购买两个按键摇杆传感器实现摇杆控制,购买蓝牙模块实现串口蓝牙控制,购买四个旋钮电位器实现示教器控制。
硬件清单:
- 四轴机械臂模型
- 四个舵机
- 两个按键摇杆传感器
- HC系列蓝牙串口模块
- 四个旋钮电位器
- IIC协议OLED屏幕
- SPI协议W25Q128模块
2.开发环境
单片机型号为STM32F103C8T6,开发环境为STM32CubeMX和Keil5,蓝牙控制需要手机下载蓝牙助手,在这里我下载的是官方给的HC蓝牙助手,(需要蓝牙调试助手的可以私信作者提供)
STM32F103C8T6原理图:
3.CubeMX配置
我用STM32CubeMX配置如下,仅供参考:
RCC:配置外部高速晶振
SYS:Debug设置成Serial Wire
ADC:打开8个通道
DMA:搬运ADC数据
TIM2:PWM输出:选用799*1799,这样可以把舵机有效的 0.5~2.5ms / 20ms 这个区间分成180段,对应0~180度。
usart:设置波特率为9600,因为蓝牙模块默认波特率为9600,开启NVIC中断接收信息
I2C:用来显示OLED模块
时钟树配置:
最后点击 generate code 生成代码
三、裸机各种模块测试
硬件模块接线:
- 四个舵机分别接CH1_A15,CH2_B3,CH3_B10, CH4_B11
- 蓝牙模块TX接RX,RX接TX
- 两个按键摇杆传感器分别接 PA0 到 PA3 对应 IN0 到 IN3
- 四个旋钮电位器分别接 PA4 到 PA7 对应 IN4 到 IN7
- OLED模块 SCL 和 SDA 分别接 PB6 和 PB7
- W25Q128模块自行扩展
1.舵机模块
舵机模块测试可以看之前我写过的文章,链接如下:
SG90舵机模块测试
然后对四个舵机进行函数封装,舵机初始化角度跟运动角度自行配置调试:
//舵机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]--;
}
}
//开启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);
2.蓝牙模块
可以使用HC-05(下图蓝色)或者HC-08(下图绿色),这两种我都测试通过。
有关蓝牙模块具体使用跟AT指令可以看以下两篇博客,链接如下:
HC-05介绍 和 HC-08介绍
使用串口中断测试收发的数据,串口重映射设置:
重映射代码:
int fputc(int ch, FILE *f)
{
unsigned char temp[1]={ch};
HAL_UART_Transmit(&huart1,temp,1,0xffff);
return ch;
}
中断接收信息代码:
// 接收完成回调函数,收到一个数据后,在这里处理
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;
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);
}
}
在main.c函数中开启中断测试,然后打开蓝牙助手:
// 开启接收中断
HAL_UART_Receive_IT(&huart1, &buf, 1);
while (1)
{
//判断判断串口是否接收完成
if(UART1_RX_STA & 0x8000)
{
if(!strcmp((const char *)UART1_RX_Buffer, "open"))
{
printf("张爪\r\n");
}
else if (!strcmp((const char *)UART1_RX_Buffer, "close"))
{
printf("夹爪\r\n");
}
else
{
if(UART1_RX_Buffer[0] != '\0')
printf("指令发送错误:%s\r\n", UART1_RX_Buffer);
}
printf("\r\n");
// 重新开始下一次接收
UART1_RX_STA = 0;
}
HAL_Delay(40);
}
手机打开蓝牙助手,记得把发送新行勾上,不然中断接收不到数据
3.按键摇杆传感器模块和旋钮电位器模块
这里用两个按键摇杆传感器分别接收 IN0~3 ADC的模拟量,用四个旋钮电位器分别接收 IN4~7 ADC的模拟量
利用DMA传输接收到的ADC的值通过串口打印进行调试,通过按钮和电位器控制角度
代码示例:
uint16_t adc_dma[8];//DMA搬运的ADC采集值
uint8_t angle[4] = {45,45,180,135};//舵机角度
uint8_t cnt = 0;//计数用,定时串口打印信息
//根据输入的0~180角度获取对应pwm占空比参数
uint8_t Angle(uint8_t pwm_pulse)
{
return pwm_pulse + 44;
}
//开始ADC和DMA采集
HAL_ADC_Start_DMA(&hadc1,(uint32_t *)adc_dma,8);
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]);
printf("adc_dma = {%d, %d, %d, %d, %d, %d, %d, %d}\r\n",adc_dma[0],adc_dma[1],adc_dma[2],adc_dma[3],adc_dma[4],adc_dma[5],adc_dma[6],adc_dma[7]);
cnt = 0;
}
HAL_Delay(20);//每20ms循环一次
}
4.OLED模块
在这里我们可以用取模软件显示机械臂动作,张爪夹爪,向左向右,向前向后,向上向下,也可以后续自行扩展其他内容,例如角度和控制模式等。
oled介绍可看我之前写过的:OLED取模生成文字图片
代码实现如下:
/*-- 文字: 向 --*/
/*-- 宋体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]);
}
}
while (1)
{
Oled_Show_open();
HAL_Delay(1000);
Oled_Show_close();
HAL_Delay(1000);
Oled_Show_up();
HAL_Delay(1000);
Oled_Show_down();
HAL_Delay(1000);
Oled_Show_left();
HAL_Delay(1000);
Oled_Show_right();
HAL_Delay(1000);
Oled_Show_front();
HAL_Delay(1000);
Oled_Show_behind();
HAL_Delay(1000);
}
5.W25Q128模块
W25Q128模块测试可看我之前写过的文章然后自行扩展:W25Q128模块测试
四、裸机三种控制测试
首先我在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文件中移植我们裸机控制的代码放在对应的任务中,可自行进行代码扩展,例如:
文章来源:https://www.toymoban.com/news/detail-825754.html
六、项目演示视频
四轴机械臂演示视频文章来源地址https://www.toymoban.com/news/detail-825754.html
到了这里,关于基于STM32与FreeRTOS的四轴机械臂项目的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!