目录
完整的代码包链接放在文章的最后了,里面有小车用到的所有代码,包括两个stm32的源码和小程序的源码,还有solidworks的完整建模也放里面了。想看的话直接下载就好啦。
一、前言
1. 欣赏一下整体效果
2. 先唠叨几句
所以为什么叫从0.1开始搭建智能小车呢?其实这是一个比较无奈的事,因为我的硬件属实拉跨,所以这个小车的驱动电路是买的亚博智能的ros机器人拓展板,所以相当于省去了最底层的一部分的工作量。但是在后来发现这个拓展板不能支撑全部的功能,所以我又画了一块拓展电路板,虽然比较的简单,但是也算是做了一点硬件方面的工作吧。
这个小车呢最终是想要跑ros系统的,但是因为时间和技术的原因,现在只做完了底层的驱动部分。而且由于这些设想,小车的载重就必须达到一定的要求。所以我选用了MG513减速电机以及全金属的车架,成本也大了不少。目前的话只能手动的遥控小车和上面的机械臂,但是在以后会在上面加上雷达、摄像头和ros主控,让他真正的变成的智能小车。所以这篇文章就先介绍一下底层的驱动吧。
3. 系统整体简介
整体上小车由一块stm32rct6和一块stm32c8t6同时驱动。主要是带动两个12V的电机,5个20kg和15kg的舵机,一个24灯的WS2812灯带,一个5V的串口屏,以及完成双机的通信以及蓝牙的通信。在机械构造上主体是一个两轮差速小车的双层底盘,最上面加了一个由6自由度机械臂改装的5自由度机械臂,还有一个串口屏来显示机器人的表情。机械臂和屏幕与主体的连接部分是通过3D打印完成的。下面是建模图片:
二、硬件系统
1. 选型
1.1 电路部分
主要的驱动电路是亚博智能的ros机器人拓展板:
上面主要搭载了一块stm32rct6,一个MPU9250,四路AM2857电机驱动模块。这次主要用到的是上面的两路电机驱动接口,还有舵机驱动的电源模块,以及RGB灯带的接口。
然后我自己加的拓展电路搭载了一个stm32c8t6,利用上面这个板子配置好的5V和6.8V电压输出供电,主要负责驱动5个电机,一个HC08蓝牙和一个串口屏。本人硬件属实拉跨,画的电路板非常不建议参考哦: (但是我还是想说我爱嘉立创~)
1.2 机械部分
小车的主体是双层的两轮差速小车底盘,两个MG513的30:1减速比的电机和一个万向轮。在上层是一个机械臂和一个串口屏。
机械臂本来是6轴的,但是装车之后发现6轴机械臂的重心太靠后了,而且未通电的时候难以保存平衡,很容易伤到前面的屏幕。所以我直接拆掉了中间的一个关节和一段力臂,变成5轴后明显稳定了很多,而且仍然可以够到一片区域的地面。可以对比一下前面的建模图(拆之前的)和实物图,很明显可以发现机械臂短了一大截。
串口屏是买的尚视界的3.5寸串口屏,是IPS全视角的但不带触摸,主要用来显示一个机器人的表情,强行增加一点人机交互功能~。本来是想用陶晶驰的屏的,虽然陶晶驰的屏功能真的比较强,但是他们的IPS屏有点贵,而且他们的屏外露的元件比较多,总给我一种很脆的感觉。所以这回选了尚视界的,虽然功能单一了(不能播放动画,表情变化只能刷新图片而且刷新效果不太好),但是不得不说,他们的屏外观真的要精致的多:
2. 控制系统
亚博智能的ros机器人拓展板有四路编码器电机的接口,虽然这次只用了两路,而且编码器也没用到,但以后升级小车的时候肯定都会用到的。rct6一共8个定时器,其中四路编码器电机用到了4个,AM2857电机驱动芯片是双路的PWM输入,所以四个电机的驱动也用到了两个定时器。剩下的定时器6和7是基本定时器,所以如果还想驱动舵机的话是要用软件模拟PWM的,但这种方法比较占用软件资源,所以我把驱动舵机的任务给到了另一个32,而舵机的供电仍然使用亚博智能电路板配置好5V和6.8V电源驱动。
另外这个电路板中rct6的三个串口外设有一个被转成了USB用于和上位机通信,我暂时不打算动他。还有一个是串口舵机用的,也在硬件上被转成了其他的通信协议,另外一个直接没引出来。所以这导致两个32通信的时候不能使用最简单的串口了,这就有点麻烦。但是我发现电路板上面还预留了一个IIC的屏幕接口,所以只能用IIC做一个双机通信来将就一下了。但是两个32的IIC通信涉及到把32当IIC从机的操作,这个真的不太常用,网上的资料也少,我一开始尝试用硬件的IIC从机设置,但弄了半天也不工作。所以最后用软件模拟IIC从机的方法做了一个精简的IIC指令协议,也算是不太优雅的解决了。下文中我会详细的说一下这里的。
下面是整体系统通信的简图:
三、底层驱动
1. 电机驱动
对一个小车来说,最重要的一定是电机的驱动了。我的这块板子有四路的AM2857电机驱动芯片,AM2857一块只能驱动一个电机,只需要两条信号线,但都要求为PWM输入,所以比较占用定时器资源。四路电机共需8条PWM输入,在硬件上是接入了Timer1和8的所有输出比较通道。下面是AM2857手册中的控制原理图表:
可以看出,我们只需要调整输入的两路PWM波的占空比,就能实现电机的正反转和调速。
正转调速我们就将一路的PWM占空比调为0,然后调另一路的占空比,占空比越大电机转速越快。反转就把两路的输入对调就行。另外是正转还是反转是相对来说的,要看实际情况才能确定哪个方向是正哪个方向是反。
而当两路PWM占空比都为0时电机停止,当占空比都拉满时是急刹。这就是电机驱动的最基本的原理。下面上代码:
这一段代码运行在rct6上,初始化了Timer1的CH1、4和8的CH3、4,并且分别封装了两个电机的正反转、控速、刹车的函数。两个电机分别命名为Motor2和3。
#include "stm32f10x.h" // Device header
void Motor_Init(void)
{
//Timer1输出PWM(频率30kHZ) 初始化Motor2(PC8,PC9)和Motor3(PA8,PA11)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//开启GPIOA时钟
//RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);//开启GPIOB时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);//开启GPIOC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);//开启Timer1时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM8, ENABLE);//开启Timer8时钟
GPIO_InitTypeDef GPIO_InitStructure;//初始化GPIO
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//一定要用复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;//Timer1 CH1 Motor3
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;//Timer1 CH4 Motor3
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;//Timer8 CH3 Motor2
GPIO_Init(GPIOC, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;//Timer8 CH4 Motor2
GPIO_Init(GPIOC, &GPIO_InitStructure);
TIM_InternalClockConfig(TIM1);//选择内部时钟为时钟源
TIM_InternalClockConfig(TIM8);//选择内部时钟为时钟源
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;//时基单元初始化(30KHZ)
TIM_TimeBaseInitStruct.TIM_ClockDivision=TIM_CKD_DIV1;//滤波器不分频
TIM_TimeBaseInitStruct.TIM_CounterMode=TIM_CounterMode_Up;//向上计数
TIM_TimeBaseInitStruct.TIM_Period=100-1;//ARR自动重装器(100方便调占空比)
TIM_TimeBaseInitStruct.TIM_Prescaler=24-1;//PSC预分频器
TIM_TimeBaseInitStruct.TIM_RepetitionCounter=0;//重复计数器
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseInitStruct);
TIM_TimeBaseInit(TIM8, &TIM_TimeBaseInitStruct);
TIM_OCInitTypeDef TIM_OCInitStruct;//初始化输出比较
TIM_OCStructInit(&TIM_OCInitStruct);//先给结构体赋初始值,防止使用高级定时器时参数配置不完全导致无法正常输出PWM
TIM_OCInitStruct.TIM_OCMode=TIM_OCMode_PWM1;//PWM模式1(常用)
TIM_OCInitStruct.TIM_OCPolarity=TIM_OCPolarity_High;//TIM_OCPolarity_High REF极性不翻转
TIM_OCInitStruct.TIM_OutputState=TIM_OutputState_Enable;//输出使能
TIM_OCInitStruct.TIM_Pulse=0;//设置CCR的值(占空比)
TIM_OC1Init(TIM1, &TIM_OCInitStruct);//初始化TIM1输出比较单元1 Motor3
TIM_OC4Init(TIM1, &TIM_OCInitStruct);//初始化TIM1输出比较单元4 Motor3
TIM_OC3Init(TIM8, &TIM_OCInitStruct);//初始化TIM8输出比较单元3 Motor2
TIM_OC4Init(TIM8, &TIM_OCInitStruct);//初始化TIM8输出比较单元4 Motor2
TIM_CtrlPWMOutputs(TIM1, ENABLE);
TIM_CtrlPWMOutputs(TIM8, ENABLE);
TIM_Cmd(TIM1, ENABLE);//使能计数器
TIM_Cmd(TIM8, ENABLE);//使能计数器
}
//Motor2:
void Motor2_ForwardSpeed(uint16_t Compare)//调用函数可以更改占空比(前进)
{
TIM_SetCompare4(TIM8, 0);//更改CCR寄存器3的值
TIM_SetCompare3(TIM8, Compare);//更改CCR寄存器4的值
}
void Motor2_BackSpeed(uint16_t Compare)//调用函数可以更改占空比(后退)
{
TIM_SetCompare3(TIM8, 0);
TIM_SetCompare4(TIM8, Compare);
}
void Motor2_Stop(void)//自然停止
{
TIM_SetCompare4(TIM8, 0);
TIM_SetCompare3(TIM8, 0);
}
void Motor2_Brake(void)//急刹
{
TIM_SetCompare4(TIM8, 100);
TIM_SetCompare3(TIM8, 100);
}
//Motor3:
void Motor3_ForwardSpeed(uint16_t Compare)//调用函数可以更改占空比(前进)
{
TIM_SetCompare4(TIM1, 0);//更改CCR寄存器1的值
TIM_SetCompare1(TIM1, Compare);//更改CCR寄存器4的值
}
void Motor3_BackSpeed(uint16_t Compare)//调用函数可以更改占空比(后退)
{
TIM_SetCompare1(TIM1, 0);
TIM_SetCompare4(TIM1, Compare);
}
void Motor3_Stop(void)//自然停止
{
TIM_SetCompare4(TIM1, 0);
TIM_SetCompare1(TIM1, 0);
}
void Motor3_Brake(void)//急刹
{
TIM_SetCompare4(TIM1, 100);
TIM_SetCompare1(TIM1, 100);
}
2. 舵机驱动和调速
2.1舵机驱动
PWM舵机驱动的原理非常简单,只需要充足的电源以及一个PWM的输入。将PWM的频率调到舵机的标准频率20ms,一个周期中高电平在0.5到2ms之间调节就可以驱动舵机了。180度舵机的角度就是高电平0.5到2ms的变化范围线性的映射到0到180度,就例如一个周期中高电平占0.5ms时,舵机为0度;同理占2.5ms时舵机为180度。下面上代码:
这一段代码运行在c8t6上,初始化了Timer2和3,并且带有一部分舵机调速的实现函数,可以和下一段代码一起看。
#include "stm32f10x.h" // Device header
#include "SeriveTimer.h" //用于舵机调速的
//舵机角度值
uint16_t S1_Ag;
uint16_t S2_Ag;
uint16_t S3_Ag;
uint16_t S4_Ag;
uint16_t S5_Ag;
void Serive_Init(void)
{
//舵机初始角度
S1_Ag=180;
S2_Ag=90;
S3_Ag=180;
S4_Ag=30;
S5_Ag=90;
//Timer2通道1234(PA0,PA1,PA2,PA3)
//Timer3通道1234(PA6,PA7,PB0,PB1)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//开启GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);//开启GPIOB时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);//开启Timer2时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);//开启Timer3时钟
GPIO_InitTypeDef GPIO_InitStructure;//初始化GPIO
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//一定要用复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;//Timer2 CH1
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;//Timer2 CH2
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;//Timer2 CH3
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;//Timer2 CH4
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;//Timer3 CH1
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;//Timer3 CH2
GPIO_Init(GPIOA, &GPIO_InitStructure);
TIM_InternalClockConfig(TIM2);//选择内部时钟为时钟源
TIM_InternalClockConfig(TIM3);//选择内部时钟为时钟源
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;//时基单元初始化
TIM_TimeBaseInitStruct.TIM_ClockDivision=TIM_CKD_DIV1;//滤波器不分频
TIM_TimeBaseInitStruct.TIM_CounterMode=TIM_CounterMode_Up;//向上计数
TIM_TimeBaseInitStruct.TIM_Period=20000-1;//ARR自动重装器
TIM_TimeBaseInitStruct.TIM_Prescaler=72-1;//PSC预分频器
TIM_TimeBaseInitStruct.TIM_RepetitionCounter=0;//重复计数器
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStruct);
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStruct);
TIM_OCInitTypeDef TIM_OCInitStruct;//初始化输出比较通道
TIM_OCStructInit(&TIM_OCInitStruct);//先给结构体赋初始值,防止使用高级定时器时参数配置不完全导致无法正常输出PWM
TIM_OCInitStruct.TIM_OCMode=TIM_OCMode_PWM1;//PWM模式1(常用)
TIM_OCInitStruct.TIM_OCPolarity=TIM_OCPolarity_High;//TIM_OCPolarity_High REF极性不翻转
TIM_OCInitStruct.TIM_OutputState=TIM_OutputState_Enable;//输出使能
TIM_OCInitStruct.TIM_Pulse=0;//设置CCR的值(占空比) 取值500~2500(对应高电平0.5ms~2.5ms)
TIM_OC1Init(TIM2, &TIM_OCInitStruct);//Timer2四个CH初始化
TIM_OC2Init(TIM2, &TIM_OCInitStruct);
TIM_OC3Init(TIM2, &TIM_OCInitStruct);
TIM_OC4Init(TIM2, &TIM_OCInitStruct);
TIM_OC1Init(TIM3, &TIM_OCInitStruct);//Timer3四个CH初始化
TIM_OC2Init(TIM3, &TIM_OCInitStruct);
TIM_Cmd(TIM2, ENABLE);//使能计数器
TIM_Cmd(TIM3, ENABLE);//使能计数器
}
//舵机慢速运动(输入0~180度)
void Servo1_MoveTo(float Angle)//Timer2 CH1 PA0
{
uint8_t NowAg=S1_Ag;//目前角度
S1_DfAg=Angle-NowAg;//设置角度差
}
void Servo2_MoveTo(float Angle)//Timer2 CH2 PA1
{
uint8_t NowAg=S2_Ag;//目前角度
S2_DfAg=Angle-NowAg;//设置角度差
}
void Servo3_MoveTo(float Angle)//Timer2 CH3 PA2
{
uint8_t NowAg=S3_Ag;//目前角度
S3_DfAg=Angle-NowAg;//设置角度差
}
void Servo4_MoveTo(float Angle)//Timer2 CH4 PA3
{
uint8_t NowAg=S4_Ag;//目前角度
S4_DfAg=Angle-NowAg;//设置角度差
}
void Servo5_MoveTo(float Angle)//Timer3 CH1 PB6
{
uint8_t NowAg=S5_Ag;//目前角度
S5_DfAg=Angle-NowAg;//设置角度差
}
//舵机全速运动(输入0~180度)
void Servo1_SetAngle(float Angle)//Timer2 CH1 PA0
{
TIM_SetCompare1(TIM2, Angle / 180 * 2000 + 500);
}
void Servo2_SetAngle(float Angle)//Timer2 CH2 PA1
{
TIM_SetCompare2(TIM2, Angle / 180 * 2000 + 500);
}
void Servo3_SetAngle(float Angle)//Timer2 CH3 PA2
{
TIM_SetCompare3(TIM2, Angle / 180 * 2000 + 500);
}
void Servo4_SetAngle(float Angle)//Timer2 CH4 PA3
{
TIM_SetCompare4(TIM2, Angle / 180 * 2000 + 500);
}
void Servo5_SetAngle(float Angle)//Timer3 CH1 PB6
{
TIM_SetCompare1(TIM3, Angle / 180 * 2000 + 500);
}
2.2舵机调速
我们虽然已经可以驱动舵机,但此时我们并不能很好的控制机械臂运行。因为此时舵机的转速很快,导致我们的机械臂的运行非常的抽搐,不管运动到什么角度都是一瞬间到达的。这不仅非常不优雅,而且在调适的时候比较危险,也容易使小车失去平衡。所以,我们需要给舵机减速。
其实舵机的转速是一个固定的值,我们无法直接控制其速度。但是我们可以将舵机的一个运动流程分为若干份,在每一份之间延迟一个时间,就达到了间接调速的目的。而分的份数越多,其运动越丝滑。比如在我的代码中,我把每一个角度分成了一份,这样的话转动180度就会分为180份来完成,比较好计算而且效果还是很不错的。
每一份中间的延迟最简单直接的方式是用delay函数来完成,但这样的话又出现了一个问题,delay会阻塞整个程序,而这样频繁的delay使舵机运动时小车不能做其他任何的事情,这明显不是我们想要的效果。所以,我采用定时器的方式来代替delay。主要的原理是先给每一个舵机设一个记录时实角度的全局变量,然后每一次运动用目标角度减当前角度得到一个角度差值,在定时器每次中断执行角度加或减一的操作,使执行的次数=角度差值。下面上代码:
这一段代码运行在c8t6上,初始化了Timer1,在其中断函数中执行某一个舵机角度加或减一的操作,实现丝滑的调速。这一部分函数的调用方式在上一段代码中。
#include "stm32f10x.h" // Device header
#include "Serive.h"
//舵机角度差
int16_t S1_DfAg;
int16_t S2_DfAg;
int16_t S3_DfAg;
int16_t S4_DfAg;
int16_t S5_DfAg;
void SeriveTimer_Init(void)
{
//舵机调速,定时器1,频率1/72s,180度2.5s
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);//开启定时器1的时钟
TIM_InternalClockConfig(TIM1);//选择内部时钟为时钟源
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;//时基单元初始化
TIM_TimeBaseInitStruct.TIM_ClockDivision=TIM_CKD_DIV1;//滤波器不分频
TIM_TimeBaseInitStruct.TIM_CounterMode=TIM_CounterMode_Up;//向上计数
TIM_TimeBaseInitStruct.TIM_Period=100-1;//ARR自动重装器
TIM_TimeBaseInitStruct.TIM_Prescaler=10000-1;//PSC预分频器
TIM_TimeBaseInitStruct.TIM_RepetitionCounter=0;//重复计数器
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseInitStruct);
TIM_ClearFlag(TIM1, TIM_FLAG_Update);//在初始化之前先清除1次中断标志位,使计数值从0开始(这个可以不加)
TIM_ITConfig(TIM1, TIM_IT_Update, ENABLE);//开启中断,选择更新中断
NVIC_InitTypeDef NVIC_InitStruct;//配置NVIC
NVIC_InitStruct.NVIC_IRQChannel=TIM1_UP_IRQn;//选择定时器1的中断通道
NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=1;//指定抢占优先级
NVIC_InitStruct.NVIC_IRQChannelSubPriority=1;//指定响应优先级
NVIC_Init(&NVIC_InitStruct);
TIM_Cmd(TIM1, ENABLE);//使能计数器
}
void TIM1_UP_IRQHandler(void)
{
if(TIM_GetITStatus(TIM1, TIM_IT_Update)==SET)//判断标志位是否正确
{
if(S1_DfAg==0 && S2_DfAg==0 && S3_DfAg==0 && S4_DfAg==0 && S5_DfAg==0){}
else//有转动指令
{
if(S1_DfAg!=0)//S1
{
if(S1_DfAg>0)//正转1度
{
S1_Ag++;
Servo1_SetAngle(S1_Ag);
S1_DfAg--;//舵机角度差-1
}
else if(S1_DfAg<0)//反转1度
{
S1_Ag--;
Servo1_SetAngle(S1_Ag);
S1_DfAg++;//舵机角度差+1
}
}
if(S2_DfAg!=0)//S2
{
if(S2_DfAg>0)//正转1度
{
S2_Ag++;
Servo2_SetAngle(S2_Ag);
S2_DfAg--;//舵机角度差-1
}
else if(S2_DfAg<0)//反转1度
{
S2_Ag--;
Servo2_SetAngle(S2_Ag);
S2_DfAg++;//舵机角度差+1
}
}
if(S3_DfAg!=0)//S3
{
if(S3_DfAg>0)//正转1度
{
S3_Ag++;
Servo3_SetAngle(S3_Ag);
S3_DfAg--;//舵机角度差-1
}
else if(S3_DfAg<0)//反转1度
{
S3_Ag--;
Servo3_SetAngle(S3_Ag);
S3_DfAg++;//舵机角度差+1
}
}
if(S4_DfAg!=0)//S4
{
if(S4_DfAg>0)//正转1度
{
S4_Ag++;
Servo4_SetAngle(S4_Ag);
S4_DfAg--;//舵机角度差-1
}
else if(S4_DfAg<0)//反转1度
{
S4_Ag--;
Servo4_SetAngle(S4_Ag);
S4_DfAg++;//舵机角度差+1
}
}
if(S5_DfAg!=0)//S5
{
if(S5_DfAg>0)//正转1度
{
S5_Ag++;
Servo5_SetAngle(S5_Ag);
S5_DfAg--;//舵机角度差-1
}
else if(S5_DfAg<0)//反转1度
{
S5_Ag--;
Servo5_SetAngle(S5_Ag);
S5_DfAg++;//舵机角度差+1
}
}
}
TIM_ClearITPendingBit(TIM1, TIM_IT_Update);//清除标志位
}
}
3. 蓝牙 & 串口屏驱动
3.1蓝牙
这一部分比较简单,都是串口通信的基本操作,所以就写到一起了。
蓝牙使用的是HC08,是低功耗的蓝牙模块,这主要是要适配自己写的微信小程序操作页面。然后蓝牙的驱动的话就是简单的串口接收,下面上代码:
这一段代码运行在c8t6上,初始化了Usart1,只配置了其接收的功能。
#include "stm32f10x.h" // Device header
uint8_t Serial_RxData;//存储接收数据
uint8_t Serial_RxFlag;//接收标志位
void BlueTooth_Init(void)
{
//USART1输入输出(PA9Tx,PA10Rx)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//打开GPIO时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);//打开USART1时钟
GPIO_InitTypeDef GPIO_InitStructure;//开启PGPIO
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//输出选择复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;//Tx
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//输入选择上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//Rx
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitTypeDef USART_InitStruct;//初始化串口
USART_InitStruct.USART_BaudRate=9600;//波特率
USART_InitStruct.USART_HardwareFlowControl=USART_HardwareFlowControl_None;//无硬件流控制
USART_InitStruct.USART_Mode=USART_Mode_Tx | USART_Mode_Rx;//串口发送和接收
USART_InitStruct.USART_Parity=USART_Parity_No;//无校验位
USART_InitStruct.USART_StopBits=USART_StopBits_1;//停止位1位
USART_InitStruct.USART_WordLength=USART_WordLength_8b;//8位字长
USART_Init(USART1, &USART_InitStruct);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启接收中断到NVIC USART_IT_RXNE:读数据寄存器非空
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//中断分组
NVIC_InitTypeDef NVIC_InitStruct;//初始化NVIC的USART1通道
NVIC_InitStruct.NVIC_IRQChannel=USART1_IRQn;//选择USART1中断通道
NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=2;//指定抢占优先级
NVIC_InitStruct.NVIC_IRQChannelSubPriority=2;//指定响应优先级
NVIC_Init(&NVIC_InitStruct);
USART_Cmd(USART1, ENABLE);//启动串口
}
//以下为接收部分
uint8_t BlueTooth_GetRxFlag(void)//获取接收标志位函数
{
if(Serial_RxFlag==1)
{
Serial_RxFlag=0;
return 1;
}
return 0;
}
uint8_t BlueTooth_GetRxData(void)//获取接收数据函数
{
return Serial_RxData;
}
void USART1_IRQHandler(void)//接收中断函数
{
if(USART_GetITStatus(USART1, USART_IT_RXNE)==SET)//判断标志位 USART_IT_RXNE:读数据寄存器非空
{
Serial_RxData=USART_ReceiveData(USART1);//读取数据
Serial_RxFlag=1;//置接收标志位
USART_ClearITPendingBit(USART1, USART_IT_RXNE);//清除RXNE标志位
}
}
3.2串口屏
和蓝牙一样,串口屏也是简单的串口操作。为了实现一个机器人表情眨眼的效果,我们开启一个定时器,每4s左右发送一行指令,使串口屏的图片刷新两次(当然这是串口屏不支持动画的无奈之举)。不同厂商的屏幕的指令不同,但原理都是一样的,只需要提前把图片或动画烧录进串口屏中然后按照厂商的协议发送指令就可以驱动串口屏了。下面上代码:
这一段代码运行在c8t6上,初始化了Usart3,只配置了其发送的功能。
#include "stm32f10x.h" // Device header
#include <stdio.h> //用于printf重定向
#include <stdarg.h> //用于封装sprintf实现多串口使用printf
uint8_t Face_RxData;//存储接收数据
uint8_t Face_RxFlag;//接收标志位
void Face_Init(void)
{
//USART3输入输出(PB10Tx,PB11Rx)控制小车表情
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);//打开GPIO时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);//打开USART3时钟
GPIO_InitTypeDef GPIO_InitStructure;//开启PGPIO
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//输出选择复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//Tx
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//输入选择上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;//Rx
GPIO_Init(GPIOB, &GPIO_InitStructure);
USART_InitTypeDef USART_InitStruct;//初始化串口
USART_InitStruct.USART_BaudRate=115200;//波特率
USART_InitStruct.USART_HardwareFlowControl=USART_HardwareFlowControl_None;//无硬件流控制
USART_InitStruct.USART_Mode=USART_Mode_Tx | USART_Mode_Rx;//串口发送和接收
USART_InitStruct.USART_Parity=USART_Parity_No;//无校验位
USART_InitStruct.USART_StopBits=USART_StopBits_1;//停止位1位
USART_InitStruct.USART_WordLength=USART_WordLength_8b;//8位字长
USART_Init(USART3, &USART_InitStruct);
USART_Cmd(USART3, ENABLE);//启动串口
}
//以下为发送部分
void Face_SendByte(uint8_t Byte)//发送一个字节
{
USART_SendData(USART3, Byte);//发送函数
while(USART_GetFlagStatus(USART3, USART_FLAG_TXE)==RESET);
//等待数据发送完成(USART_FLAG_TXE:数据寄存器空标志位,等待它置1,无需手动清零)
}
void Face_SendString(char *string)//发送字符串函数(字符串自带结束标志位0,无须传入长度) 使用\r\n换行
{
uint8_t i;
for(i=0;string[i]!=0;i++)//遍历字符串(若string[i]=结束标志位0则停止循环)
{
Face_SendByte(string[i]);//发送函数
}
}
void Face_Printf(char *format, ...)//封装sprintf实现多串口使用printf
{
char String[100];//定义一个字符串,用于接收printf的内容
va_list arg;
va_start(arg, format);
vsprintf(String, format, arg);//将printf重定向到String中
va_end(arg);
Face_SendString(String);//使用这个串口把这个字符串发出去
}
这一段代码运行在c8t6上,初始化了Timer4,每隔四秒发送一个刷新串口屏图片的指令。
#include "stm32f10x.h" // Device header
#include "Face.h"
void FaceTimer_Init(void)
{
//打开定时器4,频率4秒(循环发送表情屏指令)
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);//开启定时器4的时钟
TIM_InternalClockConfig(TIM4);//选择内部时钟为时钟源
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;//时基单元初始化
TIM_TimeBaseInitStruct.TIM_ClockDivision=TIM_CKD_DIV1;//滤波器不分频
TIM_TimeBaseInitStruct.TIM_CounterMode=TIM_CounterMode_Up;//向上计数
TIM_TimeBaseInitStruct.TIM_Period=10000-1;//ARR自动重装器
TIM_TimeBaseInitStruct.TIM_Prescaler=28800-1;//PSC预分频器
TIM_TimeBaseInitStruct.TIM_RepetitionCounter=0;//重复计数器
TIM_TimeBaseInit(TIM4, &TIM_TimeBaseInitStruct);
TIM_ClearFlag(TIM4, TIM_FLAG_Update);//在初始化之前先清除1次中断标志位,使计数值从0开始(这个可以不加)
TIM_ITConfig(TIM4, TIM_IT_Update, ENABLE);//开启中断,选择更新中断
NVIC_InitTypeDef NVIC_InitStruct;//配置NVIC
NVIC_InitStruct.NVIC_IRQChannel=TIM4_IRQn;//选择定时器2的中断通道
NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=0;//指定抢占优先级
NVIC_InitStruct.NVIC_IRQChannelSubPriority=1;//指定响应优先级
NVIC_Init(&NVIC_InitStruct);
TIM_Cmd(TIM4, ENABLE);//使能计数器
}
void TIM4_IRQHandler(void)
{
if(TIM_GetITStatus(TIM4, TIM_IT_Update)==SET)//判断标志位是否正确
{
Face_Printf("FSIMG(2097152,0,0,480,320,0);;DELAYMS(200);FSIMG(2404352,0,0,480,320,0);\r\n");
TIM_ClearITPendingBit(TIM4, TIM_IT_Update);//清除标志位
}
}
4. 灯带驱动
4.1普通灯效实现
小车的灯带选用的是24灯的WS2812,关于WS2812的优势我就不多说了,这里主要说一下它的驱动。
WS2812只需一个信号输入口,先看一下WS2812手册中有关其协议的图示:
WS2812的协议用32模拟的话,目前大部分是利用其SPI外设或输出比较,这样的话是比较节省软件资源的。因为我们的开发板预留的灯带驱动口是接到了SPI3的MOSI,所以这里就使用SPI来模拟这个协议了。
在使用SPI模拟时,用SPI的24个数据来模拟WS2812的一位数据。而我们想要一次性驱动24灯珠的WS2812至少需要连续发送24*24个数据,这比较占用资源,而且WS2812对时序的要求比较严格,稍微长一点的间隔会被认为是重置信号。如果在发送的过程中程序进入中断,很有可能造成灯带的异常。所以,我们有必要引入DMA。另外要注意SPI的频率要配置符合WS2812的要求,我的程序中的分频配置是可以用的。
使用DMA将内存中的数组转运到SPI的DR寄存器,我们只需要发送一个开始转运的指令,然后程序就可以该干嘛干嘛去了,非常的方便而且稳定。那么下面上代码:
这一段代码运行在rct6上,初始化了SPI3,模拟WS2812的协议,并且包含所有普通灯效的可调用函数的封装。
#include "stm32f10x.h" // Device header
#include "WS2812_Mode.h" //存储24位颜色数组
#include "MyDMA.h" //DMA发送
#include "WS2812_Timer.h" //用于跑马灯等效果
#define RGB_SIZE 24 //WS2812灯珠数量
uint8_t WS2812_Mode_Flag;//闪烁模式标志位
void WS2812_Init(void)//初始化WS2812
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI3, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;//MOSI
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
SPI_InitTypeDef SPI_InitStructure;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;//主模式
SPI_InitStructure.SPI_Direction = SPI_Direction_1Line_Tx;//指定SPI的单向或双向数据模式:单线发送
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;//选择模式:CPOL=0
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;//选择模式:CPHA=1
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8;//指定时钟分频
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;//指定SPI数据大小:8Byte
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;//指定高位先行还是低位先行:高位先行
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;//指定SS信号是由硬件或软件触发:软件
SPI_InitStructure.SPI_CRCPolynomial = 7;//指定用于CRC计算的多项式
SPI_Init(SPI3, &SPI_InitStructure);
SPI_I2S_DMACmd(SPI3, SPI_I2S_DMAReq_Tx, ENABLE);//开启DMA通道
MyDMA_Init((uint32_t)ClearArr);//初始化DMA
WS2812_Timer_Init();//初始化跑马灯定时器
SPI_Cmd(SPI3, ENABLE);
}
void WS2812_AllClear(void)//全部熄灭
{
for(uint8_t i=0;i<RGB_SIZE;i++)
{
MyDMA_Transfer((uint32_t)ClearArr,24);
}
}
void WS2812_AllGreen(void)//全部绿色
{
for(uint8_t i=0;i<RGB_SIZE;i++)
{
MyDMA_Transfer((uint32_t)GreenArr,24);
}
}
void WS2812_AllRed(void)//全部红色
{
for(uint8_t i=0;i<RGB_SIZE;i++)
{
MyDMA_Transfer((uint32_t)RedArr,24);
}
}
void WS2812_AllBlue(void)//全部蓝色
{
for(uint8_t i=0;i<RGB_SIZE;i++)
{
MyDMA_Transfer((uint32_t)BlueArr,24);
}
}
void WS2812_AllWhite(void)//全部白色
{
for(uint8_t i=0;i<RGB_SIZE;i++)
{
MyDMA_Transfer((uint32_t)WhiteArr,24);
}
}
void WS2812_AllYellow(void)//全部黄色
{
for(uint8_t i=0;i<RGB_SIZE;i++)
{
MyDMA_Transfer((uint32_t)YellowArr,24);
}
}
void WS2812_AllQing(void)//全部青色
{
for(uint8_t i=0;i<RGB_SIZE;i++)
{
MyDMA_Transfer((uint32_t)QingArr,24);
}
}
void WS2812_AllPurple(void)//全部紫色
{
for(uint8_t i=0;i<RGB_SIZE;i++)
{
MyDMA_Transfer((uint32_t)PurpleArr,24);
}
}
void WS2812_AllPink(void)//全部粉色
{
for(uint8_t i=0;i<RGB_SIZE;i++)
{
MyDMA_Transfer((uint32_t)PinkArr,24);
}
}
void WS2812_AllColor(void)//所有颜色
{
for(uint8_t i=0;i<RGB_HorseNum;i++)
{
MyDMA_Transfer((uint32_t)RedArr,24);
MyDMA_Transfer((uint32_t)GreenArr,24);
MyDMA_Transfer((uint32_t)BlueArr,24);
MyDMA_Transfer((uint32_t)YellowArr,24);
MyDMA_Transfer((uint32_t)PurpleArr,24);
MyDMA_Transfer((uint32_t)QingArr,24);
}
}
void WS2812_TurnMode(void)//关闭闪烁模式开始正常模式
{
WS2812_Mode_Flag=0;//关闭闪烁模式
CloseWS2812_Timer();//关闭定时器
}
这一段代码运行在rct6上,初始化了DMA2的SPI3通道。
#include "stm32f10x.h" // Device header
void MyDMA_Init(uint32_t AddrA)
{
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA2, ENABLE);//开启DMA时钟
DMA_InitTypeDef DMA_InitStruct;//DMA初始化
//外设站点的参数(起点)
DMA_InitStruct.DMA_PeripheralBaseAddr=AddrA;//起始地址(函数传入)
DMA_InitStruct.DMA_PeripheralDataSize=DMA_PeripheralDataSize_Byte;//数据宽度uint8_t
DMA_InitStruct.DMA_PeripheralInc=DMA_PeripheralInc_Enable;//地址自增
//存储器站点的参数(目的地)
DMA_InitStruct.DMA_MemoryBaseAddr=(uint32_t)&SPI3->DR;
DMA_InitStruct.DMA_MemoryDataSize=DMA_MemoryDataSize_Byte;//数据宽度uint8_t
DMA_InitStruct.DMA_MemoryInc=DMA_MemoryInc_Disable;//地址不自增
DMA_InitStruct.DMA_DIR=DMA_DIR_PeripheralSRC;//内存站点作为起点
DMA_InitStruct.DMA_BufferSize=24;//传输计数器(函数传入)
DMA_InitStruct.DMA_Mode=DMA_Mode_Normal;//不自动重装
//注意:自动重装模式和软件触发模式不能同时使用
DMA_InitStruct.DMA_M2M=DMA_M2M_Disable;//选择硬件触发
DMA_InitStruct.DMA_Priority=DMA_Priority_Medium;//设置优先级中等
DMA_Init(DMA2_Channel2, &DMA_InitStruct);//DMA1的通道5
DMA_Cmd(DMA2_Channel2,DISABLE);//等待调用启动函数时再开启DMA
}
void MyDMA_Transfer(uint32_t Addr,uint16_t Size)//重新启动DMA,开始下一轮转运
{
MyDMA_Init(Addr);//重新设置起始地址
DMA_SetCurrDataCounter(DMA2_Channel2, Size);//重新设置传输计数器
DMA_Cmd(DMA2_Channel2,ENABLE);//重新启动DAM
while(DMA_GetFlagStatus(DMA2_FLAG_TC2)==RESET);//等待DMA2转运完成
DMA_ClearFlag(DMA2_FLAG_TC2);//清除标志位
}
这一段代码运行在rct6上,存储了几种常见颜色的24位数组,发送一个数组可以控制一个WS2812灯珠的颜色。当然这是种很懒的做法,都存起来比较占内存,但是写的时候不费脑子而且也更容易理解,所以我就直接偷懒了~
#define l 0xC0
#define h 0xF8
//24位颜色顺序:绿红蓝
//熄灭
uint8_t ClearArr[24]={l,l,l,l,l,l,l,l,
l,l,l,l,l,l,l,l,
l,l,l,l,l,l,l,l,};
//绿
uint8_t GreenArr[24]={h,h,h,h,h,h,h,h,
l,l,l,l,l,l,l,l,
l,l,l,l,l,l,l,l};
//红
uint8_t RedArr[24]={l,l,l,l,l,l,l,l,
h,h,h,h,h,h,h,h,
l,l,l,l,l,l,l,l};
//蓝
uint8_t BlueArr[24]={l,l,l,l,l,l,l,l,
l,l,l,l,l,l,l,l,
h,h,h,h,h,h,h,h};
//紫
uint8_t QingArr[24]={h,h,h,h,h,h,h,h,
l,l,l,l,l,l,l,l,
h,h,h,h,h,h,h,h};
//青
uint8_t PurpleArr[24]={l,l,l,l,l,l,l,l,
h,h,h,h,h,h,h,h,
h,h,h,h,h,h,h,h};
//白
uint8_t WhiteArr[24]={h,h,h,h,h,h,h,h,
h,h,h,h,h,h,h,h,
h,h,h,h,h,h,h,h};
//黄
uint8_t YellowArr[24]={h,h,h,h,h,h,h,h,
h,h,h,h,h,h,h,h,
l,l,l,l,l,l,l,l};
//粉
uint8_t PinkArr[24]={h,h,l,l,l,l,l,l,
h,h,h,h,h,h,h,h,
h,h,l,l,h,l,h,h};
4.3蹦迪灯效实现
如果想要实现像闪烁或跑马灯这种效果,就需要在发每一个轮信号之间做一些延迟。和舵机调速所遇到的问题一样,如果简单的用delay做会严重阻塞程序,所以这里也是利用定时器实现了蹦迪灯效的效果。
具体的原理就是开启一个定时器,在不同的灯效开启时改变其频率,以达到不同的闪烁速度以及跑马灯的不同速度。在定时器中断中使全局变量count++,并限定其最大值为闪烁定光的不同状态的数量。在主函数中循环等待count的变化,变化时更新灯带状态。下面上代码:
这一段代码运行在rct6上,开启了Timer6,是蹦迪灯效底层实现。
#include "stm32f10x.h" // Device header
uint16_t Count;//计数器计数值
uint16_t CountMax;//Count的最大值
uint8_t WS2812_Send_Flag;//重复发送标志位
void WS2812_Timer_Init(void)
{
//打开定时器6,频率400ms
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE);//开启定时器6的时钟
TIM_InternalClockConfig(TIM6);//选择内部时钟为时钟源
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;//时基单元初始化
TIM_TimeBaseInitStruct.TIM_ClockDivision=TIM_CKD_DIV1;//滤波器不分频
TIM_TimeBaseInitStruct.TIM_CounterMode=TIM_CounterMode_Up;//向上计数
TIM_TimeBaseInitStruct.TIM_Period=4000-1;//ARR自动重装器
TIM_TimeBaseInitStruct.TIM_Prescaler=7200-1;//PSC预分频器
TIM_TimeBaseInitStruct.TIM_RepetitionCounter=0;//重复计数器
TIM_TimeBaseInit(TIM6, &TIM_TimeBaseInitStruct);
TIM_ClearFlag(TIM6, TIM_FLAG_Update);//在初始化之前先清除1次中断标志位,使计数值从0开始(这个可以不加)
TIM_ITConfig(TIM6, TIM_IT_Update, ENABLE);//开启中断,选择更新中断
NVIC_InitTypeDef NVIC_InitStruct;//配置NVIC
NVIC_InitStruct.NVIC_IRQChannel=TIM6_IRQn;//选择定时器6的中断通道
NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=0;//指定抢占优先级
NVIC_InitStruct.NVIC_IRQChannelSubPriority=1;//指定响应优先级
NVIC_Init(&NVIC_InitStruct);
TIM_Cmd(TIM6, DISABLE);//先关闭计数器
}
void OpenWS2812_Timer(void)//开启定时器
{
TIM_Cmd(TIM6, ENABLE);
}
void CloseWS2812_Timer(void)//关闭定时器
{
TIM_Cmd(TIM6, DISABLE);
}
void WS2812_Timer_SetCountMax(uint16_t Max)//设置Count的最大值
{
CountMax=Max;
}
void WS2812_Timer_SetTime(uint16_t Time)//设置时钟频率(传入ms时间)
{
TIM_SetAutoreload(TIM6, Time*10);
}
void TIM6_IRQHandler(void)
{
if(TIM_GetITStatus(TIM6, TIM_IT_Update)==SET)//判断标志位是否正确
{
if(Count<CountMax)
{
Count++;
}
else
{
Count=0;
}
WS2812_Send_Flag=1;
TIM_ClearITPendingBit(TIM6, TIM_IT_Update);//清除标志位
}
}
这一段代码运行在rct6上,是蹦迪灯效底层的发送数据函数(仍然是用了这种比较笨但是好理解的方法)。
void Horse1(void)//跑马灯发送1
{
for(uint8_t i=0;i<RGB_HorseNum;i++)
{
MyDMA_Transfer((uint32_t)RedArr,24);
MyDMA_Transfer((uint32_t)GreenArr,24);
MyDMA_Transfer((uint32_t)BlueArr,24);
MyDMA_Transfer((uint32_t)YellowArr,24);
MyDMA_Transfer((uint32_t)PurpleArr,24);
MyDMA_Transfer((uint32_t)QingArr,24);
}
}
void Horse2(void)//跑马灯发送2
{
for(uint8_t i=0;i<RGB_HorseNum;i++)
{
MyDMA_Transfer((uint32_t)QingArr,24);
MyDMA_Transfer((uint32_t)RedArr,24);
MyDMA_Transfer((uint32_t)GreenArr,24);
MyDMA_Transfer((uint32_t)BlueArr,24);
MyDMA_Transfer((uint32_t)YellowArr,24);
MyDMA_Transfer((uint32_t)PurpleArr,24);
}
}
void Horse3(void)//跑马灯发送3
{
for(uint8_t i=0;i<RGB_HorseNum;i++)
{
MyDMA_Transfer((uint32_t)PurpleArr,24);
MyDMA_Transfer((uint32_t)QingArr,24);
MyDMA_Transfer((uint32_t)RedArr,24);
MyDMA_Transfer((uint32_t)GreenArr,24);
MyDMA_Transfer((uint32_t)BlueArr,24);
MyDMA_Transfer((uint32_t)YellowArr,24);
}
}
void Horse4(void)//跑马灯发送4
{
for(uint8_t i=0;i<RGB_HorseNum;i++)
{
MyDMA_Transfer((uint32_t)YellowArr,24);
MyDMA_Transfer((uint32_t)PurpleArr,24);
MyDMA_Transfer((uint32_t)QingArr,24);
MyDMA_Transfer((uint32_t)RedArr,24);
MyDMA_Transfer((uint32_t)GreenArr,24);
MyDMA_Transfer((uint32_t)BlueArr,24);
}
}
void Horse5(void)//跑马灯发送5
{
for(uint8_t i=0;i<RGB_HorseNum;i++)
{
MyDMA_Transfer((uint32_t)BlueArr,24);
MyDMA_Transfer((uint32_t)YellowArr,24);
MyDMA_Transfer((uint32_t)PurpleArr,24);
MyDMA_Transfer((uint32_t)QingArr,24);
MyDMA_Transfer((uint32_t)RedArr,24);
MyDMA_Transfer((uint32_t)GreenArr,24);
}
}
void Horse6(void)//跑马灯发送6
{
for(uint8_t i=0;i<RGB_HorseNum;i++)
{
MyDMA_Transfer((uint32_t)GreenArr,24);
MyDMA_Transfer((uint32_t)BlueArr,24);
MyDMA_Transfer((uint32_t)YellowArr,24);
MyDMA_Transfer((uint32_t)PurpleArr,24);
MyDMA_Transfer((uint32_t)QingArr,24);
MyDMA_Transfer((uint32_t)RedArr,24);
}
}
void Line_Horse1(void)//连续跑马灯发送1
{
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)RedArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)GreenArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)BlueArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)YellowArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)PurpleArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)QingArr,24);}
}
void Line_Horse2(void)//连续跑马灯发送2
{
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)QingArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)RedArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)GreenArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)BlueArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)YellowArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)PurpleArr,24);}
}
void Line_Horse3(void)//连续跑马灯发送3
{
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)PurpleArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)QingArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)RedArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)GreenArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)BlueArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)YellowArr,24);}
}
void Line_Horse4(void)//连续跑马灯发送4
{
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)YellowArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)PurpleArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)QingArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)RedArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)GreenArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)BlueArr,24);}
}
void Line_Horse5(void)//连续跑马灯发送5
{
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)BlueArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)YellowArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)PurpleArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)QingArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)RedArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)GreenArr,24);}
}
void Line_Horse6(void)//连续跑马灯发送6
{
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)GreenArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)BlueArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)YellowArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)PurpleArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)QingArr,24);}
for(uint8_t i=0;i<RGB_HorseNum;i++){MyDMA_Transfer((uint32_t)RedArr,24);}
}
这一段代码运行在rct6上,开启了Timer6,是蹦迪灯效的上层可调用的函数接口。
uint8_t WS2812_Mode_Flag;//闪烁模式标志位
void WS2812_TurnMode(void)//关闭闪烁模式开始正常模式
{
WS2812_Mode_Flag=0;//关闭闪烁模式
CloseWS2812_Timer();//关闭定时器
}
void WS2812_CircleAll_Slow(void)//所有基础颜色循环闪烁(慢速400ms)
{
WS2812_Timer_SetCountMax(7);//设置Count的最大值
WS2812_Timer_SetTime(400);//设置时钟频率
OpenWS2812_Timer();//开启定时器
WS2812_Mode_Flag=1;//闪烁模式1
}
void WS2812_CircleAll_Quick(void)//所有基础颜色循环闪烁(快速200ms)
{
WS2812_Timer_SetCountMax(7);//设置Count的最大值
WS2812_Timer_SetTime(200);//设置时钟频率
OpenWS2812_Timer();//开启定时器
WS2812_Mode_Flag=1;//闪烁模式1
}
void WS2812_Mode1(void)//闪烁模式1(所有基础颜色循环闪烁)
{
if(WS2812_Mode_Flag==1 && WS2812_Send_Flag==1)
{
switch(Count)
{
case 0: WS2812_AllBlue();WS2812_Send_Flag=0;break;
case 1: WS2812_AllRed();WS2812_Send_Flag=0;break;
case 2: WS2812_AllGreen();WS2812_Send_Flag=0;break;
case 3: WS2812_AllWhite();WS2812_Send_Flag=0;break;
case 4: WS2812_AllQing();WS2812_Send_Flag=0;break;
case 5: WS2812_AllPurple();WS2812_Send_Flag=0;break;
case 6: WS2812_AllYellow();WS2812_Send_Flag=0;break;
case 7: WS2812_AllPink();WS2812_Send_Flag=0;break;
default: break;
}
}
}
void WS2812_Horse_Quick(void)//跑马灯(快速100ms)
{
WS2812_Timer_SetCountMax(5);//设置Count的最大值
WS2812_Timer_SetTime(100);//设置时钟频率
OpenWS2812_Timer();//开启定时器
WS2812_Mode_Flag=2;//闪烁模式2
}
void WS2812_Horse_Slow(void)//跑马灯(慢速300ms)
{
WS2812_Timer_SetCountMax(5);//设置Count的最大值
WS2812_Timer_SetTime(300);//设置时钟频率
OpenWS2812_Timer();//开启定时器
WS2812_Mode_Flag=2;//闪烁模式2
}
void WS2812_Mode2(void)//闪烁模式2(跑马灯)
{
if(WS2812_Mode_Flag==2 && WS2812_Send_Flag==1)
{
switch(Count)
{
case 0: Horse1();WS2812_Send_Flag=0;break;
case 1: Horse2();WS2812_Send_Flag=0;break;
case 2: Horse3();WS2812_Send_Flag=0;break;
case 3: Horse4();WS2812_Send_Flag=0;break;
case 4: Horse5();WS2812_Send_Flag=0;break;
case 5: Horse6();WS2812_Send_Flag=0;break;
default: break;
}
}
}
void WS2812_LineHorse_Quick(void)//连续跑马灯(快速100ms)
{
WS2812_Timer_SetCountMax(5);//设置Count的最大值
WS2812_Timer_SetTime(100);//设置时钟频率
OpenWS2812_Timer();//开启定时器
WS2812_Mode_Flag=3;//闪烁模式2
}
void WS2812_LineHorse_Slow(void)//连续跑马灯(慢速200ms)
{
WS2812_Timer_SetCountMax(5);//设置Count的最大值
WS2812_Timer_SetTime(200);//设置时钟频率
OpenWS2812_Timer();//开启定时器
WS2812_Mode_Flag=3;//闪烁模式2
}
void WS2812_Mode3(void)//闪烁模式3(连续跑马灯)
{
if(WS2812_Mode_Flag==3 && WS2812_Send_Flag==1)
{
switch(Count)
{
case 0: Line_Horse1();WS2812_Send_Flag=0;break;
case 1: Line_Horse2();WS2812_Send_Flag=0;break;
case 2: Line_Horse3();WS2812_Send_Flag=0;break;
case 3: Line_Horse4();WS2812_Send_Flag=0;break;
case 4: Line_Horse5();WS2812_Send_Flag=0;break;
case 5: Line_Horse6();WS2812_Send_Flag=0;break;
default: break;
}
}
}
四、上层通信
1. 双机通信
1.1IIC协议设计
在前面我提到过,由于这个电路板中的三个串口外设全都被占用了,但是上面还预留了一个IIC的屏幕接口,所以这里被迫使用IIC的双机通信来代替串口。
32的IIC协议常用于操作从机模块,但是两个32的IIC通信涉及到把32当IIC从机的操作,这个真的不太常用,网上的资料也少,我一开始尝试用硬件的IIC从机设置,不知道是什么原因,感觉配置也全部正确,但弄了半天也不工作。所以最后用软件模拟IIC从机的方法做了一个精简的IIC指令协议,之所以叫精简版,是因为我直接去掉了选址和应答位这两个流程,而且一次只收发一个字节,算是只借用了IIC的收数据的方式和起始结束位了。
这是IIC接收数据的流程图,其中传统的IIC协议起始条件后第一个或一二个数据为寻址位,这是为了多从机模式设计的。而对我们的双机通信这显然就多余了,所以直接把它除去。同样,我们也去除掉应答位来进一步简化我们的协议。
软件模拟的IIC从机的原理也不复杂,主要是用了外部中断监测SCL和SDA两个引脚的电平变化。在中断函数中置状态机,按照IIC协议接收数据的时序判断接收每一位数据。接收到数据后置接收标志位,在主函数中循环判断标志位,执行对应操作即可。下面上代码:
这一段代码运行在rct6上,是模拟IIC从机协议的底层接收逻辑。
#include "stm32f10x.h" // Device header
#define My_GPIO GPIOB
#define My_SCL GPIO_Pin_10
#define My_SDA GPIO_Pin_11
uint8_t RevStat=0;//状态机 0:等开始 1:接收数据 2:等结束
uint8_t RevData;//存储接收的数据
uint8_t ByteP;//每个字节的位位置
uint8_t RevFlag;//接收到一个字节标志位 1:可读 0:不可读
void MyI2CRev_Init(void)
{
//一:开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);//AFIO时钟
//二:引脚初始化
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//上拉输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_Init(GPIOB, &GPIO_InitStructure);
//三:AFIO初始化
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource10);//AFIO的6号中断线路连接到GPIOB的10号引脚 //改
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource11);//AFIO的7号中断线路连接到GPIOB的11号引脚 //改
//四:EXTI初始化
EXTI_InitTypeDef EXTI_InitStruct;
EXTI_InitStruct.EXTI_Line=EXTI_Line10;//选择EXTI中断线路10(SCL)
EXTI_InitStruct.EXTI_LineCmd=ENABLE;
EXTI_InitStruct.EXTI_Mode=EXTI_Mode_Interrupt;//选择中断模式
EXTI_InitStruct.EXTI_Trigger=EXTI_Trigger_Rising;//上升沿触发
EXTI_Init(&EXTI_InitStruct);
EXTI_InitStruct.EXTI_Line=EXTI_Line11;//选择EXTI中断线路11(SDA)
EXTI_InitStruct.EXTI_LineCmd=ENABLE;
EXTI_InitStruct.EXTI_Mode=EXTI_Mode_Interrupt;//选择中断模式
EXTI_InitStruct.EXTI_Trigger=EXTI_Trigger_Rising_Falling;//上升下降都沿触发
EXTI_Init(&EXTI_InitStruct);
//五:NVIC初始化
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel=EXTI15_10_IRQn;//选择中断通道15~10
NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=2;//指定抢占优先级
NVIC_InitStruct.NVIC_IRQChannelSubPriority=2;//指定响应优先级
NVIC_Init(&NVIC_InitStruct);
}
uint16_t Get_I2CData(void)//获取数据
{
return RevData;
}
uint16_t Get_I2CFlag(void)//获取接收标志位
{
return RevFlag;
}
void EXTI15_10_IRQHandler(void)//中断函数
{
if(EXTI_GetITStatus(EXTI_Line11)==SET)//SDA 获取EXTI11的中断标志位,判断是不是EXTI11进的中断 //改
{
if(RevStat==0)//在等开始的状态
{
if(GPIO_ReadInputDataBit(My_GPIO,My_SCL)==1)//如果SCL是高电平
{
RevStat=1;
RevData=0;//将存储数据清零
RevFlag=0;//不可读取标志位
ByteP=0;
}
}
else if(RevStat==2)//在等待结束位的状态
{
if(GPIO_ReadInputDataBit(My_GPIO,My_SCL)==1)//如果SCL是高电平
{
RevStat=0;
RevFlag=1;//可读取标志位
}
}
EXTI_ClearITPendingBit(EXTI_Line11);//清除中断标志位(重要)
}
else if(EXTI_GetITStatus(EXTI_Line10)==SET)//SCL 获取EXTI10的中断标志位,判断是不是EXTI10进的中断
{
if(RevStat==1)//在接收数据的状态
{
uint8_t BitValue;
BitValue=GPIO_ReadInputDataBit(My_GPIO,My_SDA);//读取SDA的电平
if(BitValue==1){RevData |= (0x80 >> ByteP);}//存储数据
if(ByteP>=7){
ByteP=0;
RevStat=2;
}
else{
ByteP++;
}
}
EXTI_ClearITPendingBit(EXTI_Line10);//清除中断标志位(重要)
}
}
1.2双机通信指令集
这一部分运行在主函数中,是c8t6通过我们设计的IIC发送给rct6的指令,采取了单数据位的指令结构,主要是控制两个电机和WS2812灯带模式的指令。下面直接上代码:
这一段代码运行在rct6上。
while(1)
{
//IIC接收数据
if(Get_I2CFlag()==1)
{
IICRxData=Get_I2CData();
if(IICRxData>100)//指令部分
{
switch(IICRxData)
{
//电机部分:
//M2
case 0xA1:Motor2_ForwardSpeed(MSpeed);break;//M2前进
case 0xA2:Motor2_BackSpeed(MSpeed);break;//M2后退
case 0xE1:Motor2_Stop();break;//M2停止
//M3
case 0xB1:Motor3_ForwardSpeed(MSpeed);break;//M3前进
case 0xB2:Motor3_BackSpeed(MSpeed);break;//M3后退
case 0xE2:Motor3_Stop();break;//M3停止
//刹车
case 0xFE:Motor2_Stop();Motor3_Stop();break;//自然停止
case 0xFD:Motor2_Brake();Motor3_Brake();break;//急刹
//灯光部分:
//普通模式
case 0xC0:WS2812_TurnMode();WS2812_AllClear();break;//关闭灯带
case 0xC1:WS2812_TurnMode();WS2812_AllGreen();break;//全部绿色
case 0xC2:WS2812_TurnMode();WS2812_AllRed();break;//全部红色
case 0xC3:WS2812_TurnMode();WS2812_AllBlue();break;//全部蓝色
case 0xC4:WS2812_TurnMode();WS2812_AllWhite();break;//全部白色
case 0xC5:WS2812_TurnMode();WS2812_AllYellow();break;//全部黄色
case 0xC6:WS2812_TurnMode();WS2812_AllQing();break;//全部青色
case 0xC7:WS2812_TurnMode();WS2812_AllPurple();break;//全部紫色
case 0xC8:WS2812_TurnMode();WS2812_AllPink();break;//全部粉色
case 0xC9:WS2812_TurnMode();WS2812_AllColor();break;//全部颜色
//闪烁模式
case 0xD0:WS2812_CircleAll_Slow();break;//所有基础颜色循环闪烁(慢速)
case 0xD1:WS2812_CircleAll_Quick();break;//所有基础颜色循环闪烁(快速)
case 0xD2:WS2812_Horse_Slow();break;//跑马灯(慢速)
case 0xD3:WS2812_Horse_Quick();break;//跑马灯(快速)
case 0xD4:WS2812_LineHorse_Slow();break;//连续跑马灯(慢速)
case 0xD5:WS2812_LineHorse_Quick();break;//连续跑马灯(快速)
//bug
default:break;
}
}
else{MSpeed=IICRxData;}//控速部分
RevFlag=0;//清除标志位
}
//灯带循环判断
WS2812_Mode1();
WS2812_Mode2();
WS2812_Mode3();
}
2. 上位机通信
2.1串口协议设计
我们的上位机也就是微信小程序的操作页面通过蓝牙透传给c8t6发送指令,c8t6通过串口接收指令。这里的协议设计为达到最快的解析效率,使用了单个数据的文本指令。虽然指令的可读性差了一点,但是可以让小车更快的反应。这一部分的代码和上面蓝牙驱动的代码是一样的,我就只放接收数据的那一部分了。直接上代码:
这一段代码运行在c8t6上。
//以下为接收部分
uint8_t BlueTooth_GetRxFlag(void)//获取接收标志位函数
{
if(Serial_RxFlag==1)
{
Serial_RxFlag=0;
return 1;
}
return 0;
}
uint8_t BlueTooth_GetRxData(void)//获取接收数据函数
{
return Serial_RxData;
}
void USART1_IRQHandler(void)//接收中断函数
{
if(USART_GetITStatus(USART1, USART_IT_RXNE)==SET)//判断标志位 USART_IT_RXNE:读数据寄存器非空
{
Serial_RxData=USART_ReceiveData(USART1);//读取数据
Serial_RxFlag=1;//置接收标志位
USART_ClearITPendingBit(USART1, USART_IT_RXNE);//清除RXNE标志位
}
}
2.2上位机通信指令集
这一部分运行在主函数中,是c8t6接收到蓝牙数据后的具体执行内容,主要是控制机械臂的指令和进一步下发到rct6的指令。但是由于这部分的指令数量过大,每个指令的具体实现函数就没有放出来,如果想看可以在完整的代码包中查看。下面直接上代码:
这一段代码运行在c8t6上。
while(1)
{
if(BlueTooth_GetRxFlag()==1)
{
switch(BlueTooth_GetRxData())
{
//下面的指令会进一步下发到rct6
//电机指令:
//M2
case 'A':MotorCmd_A();break;//M2前进
case 'B':MotorCmd_B();break;//M2后退
case 'G':MotorCmd_G();break;//M2停止
//M3
case 'C':MotorCmd_C();break;//M3前进
case 'D':MotorCmd_D();break;//M3后退
case 'H':MotorCmd_H();break;//M3停止
//刹车
case 'E':MotorCmd_E();break;//自然停止
case 'F':MotorCmd_F();break;//急刹
//控速
case '1':MotorCmd_1();break;//50
case '2':MotorCmd_2();break;//55
case '3':MotorCmd_3();break;//60
case '4':MotorCmd_4();break;//65
case '5':MotorCmd_5();break;//70
case '6':MotorCmd_6();break;//75
case '7':MotorCmd_7();break;//80
case '8':MotorCmd_8();break;//85
//下面的指令由c8t6自己执行
//舵机指令:
//单个舵机正转反转
case 'a':SeriveCmd_a();break;//s2++
case 'b':SeriveCmd_b();break;//s2--
case 'c':SeriveCmd_c();break;//s3++
case 'd':SeriveCmd_d();break;//s3--
case 'e':SeriveCmd_e();break;//s4++
case 'f':SeriveCmd_f();break;//s4--
case 'g':SeriveCmd_g();break;//s5++
case 'h':SeriveCmd_h();break;//s5--
//单个舵机停止
case 'i':SeriveCmd_i();break;//s2停止
case 'j':SeriveCmd_j();break;//s3停止
case 'k':SeriveCmd_k();break;//s4停止
case 'l':SeriveCmd_l();break;//s5停止
//爪夹动作
case 'm':SeriveCmd_m();break;//开爪
case 'n':SeriveCmd_n();break;//收爪
//固定抓取动作
case 'o':SeriveCmd_o();break;//抓取动做预备
case 'p':SeriveCmd_p();break;//抓取
case 'q':SeriveCmd_q();break;//提起动作
//所有舵机归位
case 's':SeriveCmd_s();break;//所有舵机归位90
case 't':SeriveCmd_t();break;//初始位置
//下面的指令会进一步下发到rct6
//灯光指令:
//普通效果
case '!':WS2812_AllClear();break;//关闭灯带
case '@':WS2812_AllGreen();break;//全部绿色
case '#':WS2812_AllRed();break;//全部红色
case '$':WS2812_AllBlue();break;//全部蓝色
case '%':WS2812_AllWhite();break;//全部白色
case '^':WS2812_AllYellow();break;//全部黄色
case '&':WS2812_AllQing();break;//全部青色
case '*':WS2812_AllPurple();break;//全部紫色
case '(':WS2812_AllPink();break;//全部粉色
case ')':WS2812_AllColor();break;//全部颜色
//闪烁效果
case 'M':WS2812_CircleAll_Slow();break;//所有基础颜色循环闪烁(慢速)
case 'N':WS2812_CircleAll_Quick();break;//所有基础颜色循环闪烁(快速)
case 'O':WS2812_Horse_Slow();break;//跑马灯(慢速)
case 'P':WS2812_Horse_Quick();break;//跑马灯(快速)
case 'Q':WS2812_LineHorse_Slow();break;//连续跑马灯(慢速)
case 'R':WS2812_LineHorse_Quick();break;//连续跑马灯(快速)
//报错
default:
OLED_ShowString(1,1," ");
OLED_ShowString(1,1,"wtf?");
break;
}
}
SeriveCmd_OLEDShow();//OLED显示舵机2345的角度
}
五:小程序页面
1. 界面展示
1.1蓝牙连接界面
1.2小车操作界面
2. 界面设计
2.1整体概况
说实话我这页面设计的真的挺丑的,没好意思把这个加到封面上。。但是不管怎么说,能用就行是吧~ 而且这个小程序的代码写的比较臃肿,可能是我的CSS和HTML没学好。所以这里就只放出少部分代码,如果想看整体程序的话可以下载代码包查看。
这个小程序的蓝牙连接部分我是直接套用的汇承BLE串口助手的开源代码,因为小程序蓝牙开发这一块还是有点复杂的,虽然网上的资料不少,但是坑还是很多。这里因为嫌麻烦,就直接套用开源的项目了,这样我们就只需要关心操作界面的设计和发送数据的方式了。
其实吧,现在的蓝牙app已经非常多了,如果没有特殊的需求根本不需要费时间自己写的。但是我们需要同时操作小车移动和机械臂,需要的按钮比较多。而且为了操作感,我们还需要实现例如按下按钮小车开始运动,松开小车停止这样的效果。显然这样的需求现成的蓝牙app是无法满足的,所以我就简单的写了一个这样的蓝牙操作界面。
2.2小程序蓝牙发送数据
因为蓝牙搜索和连接部分是抄的,我也不太会这一部分,这里就不再说了。在汇承BLE串口助手的源程序中,连接上蓝牙后会跳转这样的一个界面:
我们的操作界面就是重新改写的这个页面。具体的方法就是把原页面的HTML和CSS全删掉,然后在JS里面保留原页面的发送按钮的函数,然后我们新界面中每个按钮的绑定事件都改为这个函数并发送各自的指令就好了。
这一段代码就是其中一个按键的事件绑定函数,其中sendText后的内容就是发送的数据。其他的部分都不需要我们修改,拿过来直接用就完了。
M2_ForwardGo: function(event) {
const deviceId = this.customData.deviceId // 设备ID
const serviceId = this.customData.serviceId // 服务ID
const characteristicId = this.customData.characteristicId // 特征值ID
const sendText = 'A'
const sendPackage = app.subPackage(sendText) // 数据分每20个字节一个数据包数组
if (app.globalData.connectState) {
if (this.customData.canWrite) { // 可写
this.writeData({ deviceId, serviceId, characteristicId, sendPackage })
}
} else {
wx.showToast({
title: '已断开连接',
icon: 'none'
})
}
},
2.3操作界面具体实现
这个界面在控制小车运动时,其实是实现了像在我的世界中开船的那种效果。两边各有两个按钮控制左右两个电机的前进或后退,而且当按钮按下时电机开始运动,按钮松开后电机停止,这样直接操作感拉满。同样在控制机械臂的舵机的按钮上也采样了这样的点击设计。
想实现这样的效果其实非常简单,首先我们要在HTML中给这些按键都绑定上两个事件,一个是按下执行,另一个是松开执行。按下执行发送的指令让电机或舵机开始运动,松开执行发送的指令让电机或舵机停止运动。就例如下面这个标签:
在小程序代码中,capture-bind:touchstart是按下执行事件,capture-bind:touchend是松开执行事件。
<view class="forward_M2" capture-bind:touchstart="M2_ForwardGo" capture-bind:touchend="M2_Stop">
<image src="../../imgs/GoImg.png" class="GoImg" mode=""/>
</view>
下面这段代码就是上面这个标签的两个JS事件绑定, 可以参考一下。
// M2前进
M2_ForwardGo: function(event) {
const deviceId = this.customData.deviceId // 设备ID
const serviceId = this.customData.serviceId // 服务ID
const characteristicId = this.customData.characteristicId // 特征值ID
const sendText = 'A'
const sendPackage = app.subPackage(sendText) // 数据分每20个字节一个数据包数组
if (app.globalData.connectState) {
if (this.customData.canWrite) { // 可写
this.writeData({ deviceId, serviceId, characteristicId, sendPackage })
}
} else {
wx.showToast({
title: '已断开连接',
icon: 'none'
})
}
},
// M2停止
M2_Stop: function(event) {
const deviceId = this.customData.deviceId // 设备ID
const serviceId = this.customData.serviceId // 服务ID
const characteristicId = this.customData.characteristicId // 特征值ID
const sendText = 'G'
const sendPackage = app.subPackage(sendText) // 数据分每20个字节一个数据包数组
if (app.globalData.connectState) {
if (this.customData.canWrite) { // 可写
this.writeData({ deviceId, serviceId, characteristicId, sendPackage })
}
} else {
wx.showToast({
title: '已断开连接',
icon: 'none'
})
}
},
除了电机和舵机控制的按钮,其他的按钮如速控按钮,机械臂功能按钮,灯带功能按钮等都绑定了轻触的事件,就是点击按钮只触发一次的事件。在小程序中轻触事件是bindtap,很像网页开发中的click。下面就是这些按钮的标签和事件绑定的示例:
<!-- 调速 -->
<view class="speed">
<view class="low" bindtap="SpeedDown">
-
<image src="../../imgs/SpeedLowImg.png" class="SpeedLowImg" mode=""/>
</view>
// 减速
SpeedDown: function(event) {
if(Speed>1){
Speed-=1
this.setData({
ShowSpeed:Speed
})
const deviceId = this.customData.deviceId // 设备ID
const serviceId = this.customData.serviceId // 服务ID
const characteristicId = this.customData.characteristicId // 特征值ID
const sendText = `${Speed}`
const sendPackage = app.subPackage(sendText) // 数据分每20个字节一个数据包数组
if (app.globalData.connectState) {
if (this.customData.canWrite) { // 可写
this.writeData({ deviceId, serviceId, characteristicId, sendPackage })
}
} else {
wx.showToast({
title: '已断开连接',
icon: 'none'
})
}
}
else{
wx.showToast({
title: '已是最小速度',
icon: 'none'
})
}
},
完整的小程序代码可以下载代码包获取。
六、组装和调试
那么代码的正文部分就已经结束了,下面属于是记录一下这个小车的组装和调试过程。
1. 组装
1.1组件展示
啥也不说了,直接上图吧。
电路主控:
小车底盘:
机械臂:
机器人表情屏:
24位灯带:
锂电池:
1.2组装成果
装车效果:
2. 调试
2.1电机调试
这次的电机调试还是很简单的,因为只使用了手动的摇控控制,没有pid算法,所以电机的调试只涉及到了电机的调速。实测发现本车的AM2857芯片驱动电机输入PWM的占空比小于50%时电机不会工作,占空比50到100间线性递增。而达到85之后速度会过快,所以就简单的做了一个50到85的限速,在控制时对应分成了8个速度等级。
2.2机械臂调试
机械臂的调试还是稍微复杂一点的。首先我们要确定机械臂不同状态下每一个舵机的限位,其次为了更好的操作感,我们最好还要设计几个固定的动作。
其实机械臂调试的原理还是很简单的,只须要不断的显示舵机的角度值,记录下来不同状态下的限位即可。设计固定的动作也是一样,先手动把机械臂调到预期的动作位置,记录每一个舵机的角度,然后这个动作就可以转化为每个舵机运动到自己的目标角度了。最后在程序中我们就加上角度的限制条件和固定动作的函数就行了。
虽然原理很简单,但是调试过程还是比较繁琐的。所以这里我就只展示一下最后的调试记录和代码实现吧。
舵机限位记录:
//舵机最大限位
//S1:123~180
//S2:0~180
//S3:15~180
//S4:0~165
//S5:0~180
//舵机特殊位置限位
//s3 180时s4 最大135
//s3 160时s4 最大140
//s3 140时s4 最大145
//s3 130时s4 最大150
//s3 120时s4 最大155
//s3 110时s4 最大160
//s3 100时s4 最大165
//s4 165时s3 最大105
//s4 160时s3 最大115
//s4 155时s3 最大125
//s4 150时s3 最大135
//s4 145时s3 最大140
//s4 140时s3 最大160
//s4 135时s3 最大180
代码实现示例:
void SeriveCmd_c(void)//s3++
{
if(S3_Ag<180)
{
if(S4_Ag<=135)//s4 135时s3 最大180
{
Servo3_MoveTo(180);
}
else if(S4_Ag<=140)//s4 140时s3 最大160
{
Servo3_MoveTo(160);
}
else if(S4_Ag<=145)//s4 145时s3 最大140
{
Servo3_MoveTo(140);
}
else if(S4_Ag<=150)//s4 150时s3 最大135
{
Servo3_MoveTo(135);
}
else if(S4_Ag<=155)//s4 155时s3 最大125
{
Servo3_MoveTo(125);
}
else if(S4_Ag<=160)//s4 160时s3 最大115
{
Servo3_MoveTo(115);
}
else if(S4_Ag<=165)//s4 165时s3 最大105
{
Servo3_MoveTo(105);
}
}
}
机械臂动作记录:
//抓取动做预备:s4:130 s3:160 s1:123
//抓取动抓:s1:180 s4:145 s3:145
//抓取动提:s4:60 s3:180
代码实现示例:
//固定抓取动作
void SeriveCmd_o(void)//抓取动做预备
{
Servo4_MoveTo(130);
Servo3_MoveTo(160);
Servo1_MoveTo(123);
}
void SeriveCmd_p(void)//抓取
{
Servo3_MoveTo(145);
Servo4_MoveTo(145);
Servo1_MoveTo(180);
}
void SeriveCmd_q(void)//提起动作
{
Servo4_MoveTo(60);
Servo3_MoveTo(180);
}
2.3整车运动调试
第一次装上机械臂之后下地运行了一下,结果加速时直接后翻了。这的确是没想到,因为机械臂安装位置靠后,小车重心后移了不少,而小车底盘用于固定的万向轮在前面,后面没支撑,就直接后翻了。
随后我上淘宝找固定轮,但是由于小车后部空间很小,没一个是尺寸合适的。。所以就买了一个小的牛眼轮,然后用一个铜柱把牛眼轮固定,中间的连接还是502粘的。。虽然不太优雅,但是稳定性是有了。
后部支撑最后做成了这个样子:
七、最终效果展示
最后来看一下整体效果吧:
直接上视频:
从0.1开始搭建智能小车(一):抓坤
从0.1开始搭建智能小车(一):转圈
八、完整代码包链接
csdn链接:
https://download.csdn.net/download/2303_76380160/87761803?spm=1001.2014.3001.5501