写在前面
上一小节我们讲到了机器人的轮胎(编码器)驱动,本节我们来介绍:
- 机器人上舵机的工作原理,硬件连接和舵机的软件驱动方法
- 串口通讯的底层通讯机理,单片机-主机串口通讯以及“数据包”的基本介绍
机器人的舵机与云台控制
云台和舵机的硬件连接及原理
我们机器人上有一个两轴的云台,云台上又两个舵机分别控制云台的横向和纵向旋转。
舵机一般有三条控制线,分别对应着电源正极(红色),电源负极(黑色),信号线(白色或蓝色)。舵机的电路连接相对比较简单,如下图。值得注意的是,由于舵机很有可能出现堵转的情况(比如云台卡住),为防止烧毁器件,可以在舵机的正极或负极连接保险丝。
舵机的控制方法为高电平脉宽调制控制。简单老说,就是我们在控制电机的时候,需要给信号线输入变换的高低电平,可以理解为PWM,如下图:
舵机(180度)的角度与脉宽关系为: 0.5ms—–0度 1ms——-45度 1.5——-90度 2———135度 2.5——-180度 上一节我们讲到可以使用PWM的占空比控制轮胎的动力。如果我们将PWM的频率固定(20ms),然后调整占空比,就可以实现高电平脉宽的控制了。
使用脉宽控制云台旋转
可以看到,我们在电路中一共配置了8组舵机,前四组对应TIM3,后四组对应TIM8.因为我们在机器人中只用到了两组电机,所以我们初始化Timer4即可,Timer8与Timer4的初始化方式相同: 首先来配置一些宏和基本寄存器:
#define SERVO_PWM_FREQUENCE 50 // 50Hz
#define SERVO_PWM_RESOLUTION 20000 // 20ms = 20000us
#define SERVO_DEFAULT_DUTY 1500 // 1500us
#define APB1_TIMER_CLOCKS 72000000
#define APB2_TIMER_CLOCKS 72000000
#define SERVO_TIM_PSC_APB1 ((APB1_TIMER_CLOCKS/SERVO_PWM_FREQUENCE)/SERVO_PWM_RESOLUTION -1)
#define SERVO_TIM_PSC_APB2 ((APB2_TIMER_CLOCKS/SERVO_PWM_FREQUENCE)/SERVO_PWM_RESOLUTION -1)
舵机初始化函数(我们这里用了Timer3 和Timer8) 接下来,我们使用一个函数来调整8路舵机的旋转角度(为了节省空间,我们在这里初始化Timer3,Timer8的初始化跟Timer3一模一样):
void Servo_Bundle1_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
// 输出比较通道1和2 GPIO 初始化
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 输出比较通道3和4 GPIO 初始化
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
// 开启定时器时钟,即内部时钟CK_INT=72M
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
/*--------------------时基结构体初始化-------------------------*/
// 配置周期,这里配置为20ms
// 自动重装载寄存器的值,累计TIM_Period+1个频率后产生一个更新或者中断
TIM_TimeBaseStructure.TIM_Period = SERVO_PWM_RESOLUTION;
TIM_TimeBaseStructure.TIM_Prescaler = SERVO_TIM_PSC_APB1;
TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_RepetitionCounter=0;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
/*--------------------输出比较结构体初始化-------------------*/
// 配置为PWM模式1
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
// 输出使能
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
// 输出通道电平极性配置
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
// 输出比较通道 1
TIM_OCInitStructure.TIM_Pulse = SERVO_DEFAULT_DUTY;
TIM_OC1Init(TIM3, &TIM_OCInitStructure);
TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable);
// 输出比较通道 2
TIM_OCInitStructure.TIM_Pulse = SERVO_DEFAULT_DUTY;
TIM_OC2Init(TIM3, &TIM_OCInitStructure);
TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable);
// 输出比较通道 3
TIM_OCInitStructure.TIM_Pulse = SERVO_DEFAULT_DUTY;
TIM_OC3Init(TIM3, &TIM_OCInitStructure);
TIM_OC3PreloadConfig(TIM3, TIM_OCPreload_Enable);
// 输出比较通道 4
TIM_OCInitStructure.TIM_Pulse = SERVO_DEFAULT_DUTY;
TIM_OC4Init(TIM3, &TIM_OCInitStructure);
TIM_OC4PreloadConfig(TIM3, TIM_OCPreload_Enable);
// 使能计数器
TIM_Cmd(TIM3, ENABLE);
}
最后,我们直接调用Set_Servo_PWM就可以实现舵机的控制了。其中Channel代表控制的那一路舵机,pwm代表脉宽。Pwm的数值应当介于500-2500之间;500对应0.5ms脉宽(舵机0度),2500对应2.5ms脉宽(舵机180度)。
void Set_Servo_PWM(uint8_t channel, uint16_t pwm)
{
TIM_TypeDef *tim;
//第六路Servo连接的是雷达的控制端
if(channel==6) return;
if(channel > 8)
{
return;
}
// 如果PWM值不在区间[500,2500]内,则不处理
if(pwm > 2500)
{
return;
}
else if(pwm < 500)
{
return;
}
if(channel > 4)
{
tim = TIM8;
channel -= 4;
}
else
{
tim = TIM3;
}
switch(channel)
{
case 1: tim->CCR1 = pwm; break;
case 2: tim->CCR2 = pwm; break;
case 3: tim->CCR3 = pwm; break;
case 4: tim->CCR4 = pwm; break;
default:
break;
}
}
至此,我们完成了舵机的控制函数。下面,我们来看一下单片机与主控板的通讯:
下位机——上位机的底层通讯及协议设计
串口通讯的硬件机理简介
串口硬件
芯片之间的通讯方法有很多种,比如SPI,IIC,CAN…对于一套DIY机器人,比较常用的方法是异步串口UART通讯。 我们在硬件上设计这样的串口通讯方案:
大家都知道USART/UART具有TX和RX两根线,通常我们都是直接用,了解其中通讯机理的人比较少。在这里我简单介绍一下硬件的通讯机制: 首先,异步串口通讯跟SPI,CAN等总先不同,它一般为点对点,如下图:
在正常情况下,TX和RX(上图绿色和黄色的两根线)都处于高电位(空闲状态)。
串口通讯的底层机制
假设现在下位机要给上位机发送2个字节的数据,那么下位机的TX将把空先状态拉低,形成一个起始位,如下图。 那么这个起始位究竟要维持多久呢?这跟串口的通讯波特率有关,我们以常用的9600bps波特率为例,这就代表一个bit(注意是bit不是BYTE)占用的时长为1/9600=104微秒。 在接收端发现通讯开始,也就是TX–>RX这跟导线上的电压被拉低的一瞬间,接收端根据自己预设的波特率(假设也是9600)生成了一个9600Hz的时钟,发送端每隔104微秒发送一个bit,接收端每隔104微秒接受一个bit。就这样先传输一个起始位(0),再传输8个数据位,再传输一个校验位。经过104X(1+8+1)=1.04毫秒,我们就成功的传输了一个数据(BYTE字节)。
很多DIY爱好者在调试串口时由于主机和单片机的波特率设置不一致而出现传输乱码的情况,如果你能理解之前的内容,就应该知道乱码的原因了。
为什么串口通讯波特率一定要匹配
假设我们将发送端的波特率配置为9600kbps,接收端数据配置为4800bps。那么从第一个数据的起始位开始,发送端的时钟每走1个周期,接收端才走一个周期。就像下图所示,发送端发送了0x01,那么接收端接收到的就会是个随机数。这就导致两端根本没法通讯了~
基于串口的通讯协议设计
代码设计
STM单片机的串口通讯也首先要进行初始化,具体的流程为:
- 配置串口时钟和相关的GPIO引脚,代码如下图。值得注意的是,单片机的TX引脚要配置为复用推挽输出(AF_PP),RX引脚配置为浮空输入(IN_FLOATING):
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
USART_ClockInitTypeDef USART_ClockInitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
// 打开串口GPIO和串口外设的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC | RCC_APB2Periph_AFIO, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_UART4,ENABLE);
// 将USART Tx的GPIO配置为推挽复用模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure);
// 将USART Rx的GPIO配置为浮空输入模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOC, &GPIO_InitStructure);
- 初始化串口功能(注意配置波特率,停止位,数据位等参数):
USART_InitStructure.USART_BaudRate = 115200;
// 配置 针数据字长
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
// 配置停止位
USART_InitStructure.USART_StopBits = USART_StopBits_1;
// 配置校验位
USART_InitStructure.USART_Parity = USART_Parity_No ;
// 配置硬件流控制
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
// 配置工作模式,收发一起
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
// 完成串口的初始化配置
USART_Init(UART4, &USART_InitStructure);
USART_ClockInitStructure.USART_Clock = USART_Clock_Disable; // SCLK时钟配置(同步模式下需要)
USART_ClockInitStructure.USART_CPOL = USART_CPOL_Low; // 时钟极性(同步模式下需要)
USART_ClockInitStructure.USART_CPHA = USART_CPHA_2Edge; // 时钟相位(同步模式下需要)
USART_ClockInitStructure.USART_LastBit = USART_LastBit_Disable; // 最后一位时钟脉冲(同步模式下需要)
USART_ClockInit(UART4, &USART_ClockInitStructure);
// NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
NVIC_InitStructure.NVIC_IRQChannel = UART4_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
USART_ITConfig(UART4, USART_IT_RXNE, ENABLE);
// 使能串口
USART_Cmd(UART4, ENABLE);
- 使用串口中断以及UART_Send(…)函数收发数据:
// 串口4中断, 接控制信号CH4
void UART4_IRQHandler()
{
uint8_t temp = 0;
if(USART_GetITStatus(UART4, USART_IT_RXNE)!=RESET)
{
temp = USART_ReceiveData(UART4);
USART_SendData(UART4,temp);
}
}
这个函数可以实现给单片机发什么数据,它就回传什么数据。
串口调试助手验证串口可用性
在做串口上层协议之前,我们要使用串口助手验证一下我们之前的配置是否能用。 因为不论是设计机器人还是做其他任何系统性工程,我们都要做一个模块验证一个模块,验证好了再往下做,免得最后测试的时候四处出问题~ 我们用一个ST-Link和串口调试助手一起来调试,就像下面这张图:
我们运行,然后打开助手,配置为跟单片机一样的波特率,可以看到串口调试小助手上回传了之前发送的数据:
基于串口的数据包协议
我们一般都听说过TCP/CAN数据包……那么串口和数据包之间是什么关系呢?我们如何使用串口与数据包进行单片机和主机的通讯呢? 留点儿好奇,我们下节再讨论~
本章小结
1. 本章节我们介绍了舵机的使用,舵机驱动只有一根数据线,靠高电平的脉冲宽度来控制旋转角度 2. 本章我们介绍了机器人上位机与下位机的串口通讯,介绍了串口通讯的底层原理,为什么波特率不匹配会造成乱码~ 3. 下一节我们介绍上位机与下位机通讯的数据包,从而展开下位机C语言与 ARM上位机Python语言在系统层面的程序设计~
联系作者