#ifdef ARDUINO_ENC_COUNTER
//below can be changed, but should be PORTD pins;
//otherwise additional changes in the code are required
#define LEFT_ENC_PIN_A PD2 //pin 2
#define LEFT_ENC_PIN_B PD3 //pin 3
//below can be changed, but should be PORTC pins
#define RIGHT_ENC_PIN_A PC4 //pin A4
#define RIGHT_ENC_PIN_B PC5 //pin A5
#endif
long readEncoder(int i); //读取编码器数值
void resetEncoder(int i); //重置编码器数值
void resetEncoders(); //重置所有编码器数值
还是头文件的定义,基于Atmega328P芯片的Arduino,只有D2、D3两个引脚支持外部中断,A4、A5两个引脚通常是用作模拟量读取、数字量读取/写入、IIC通讯,这里将这两个两个当作编码器的中断使用。然后就是三个函数读取编码器、重置指定编码器、重置所有编码器。
这部分的内容实现在encoder_dirver.cpp文件内,我们继续分析~
#ifdef USE_BASE
#ifdef ROBOGAIA
#include "MegaEncoderCounter.h"
MegaEncoderCounter encoders = MegaEncoderCounter(4);
long readEncoder(int i)
{
if (i == LEFT)
return encoders.YAxisGetCount();
else
return encoders.XAxisGetCount();
}
void resetEncoder(int i)
{
if (i == LEFT)
return encoders.YAxisReset();
else
return encoders.XAxisReset();
}
//上面的不管,下面的是是Arduino编码器的实现
#elif defined(ARDUINO_ENC_COUNTER)
volatile long left_enc_pos = 0L;
volatile long right_enc_pos = 0L;
static const int8_t ENC_STATES [] = {0,1,-1,0,-1,0,0,1,1,0,0,-1,0,-1,1,0};
//编码器查找表
//中断程序为左编码器,实际计数
ISR (PCINT2_vect)
{
static uint8_t enc_last=0;
enc_last <<=2;
enc_last |= (PIND & (3 << 2)) >> 2;
left_enc_pos += ENC_STATES[(enc_last & 0x0f)];
}
//中断程序为右编码器,实际计数
ISR (PCINT1_vect)
{
static uint8_t enc_last=0;
enc_last <<=2;
enc_last |= (PINC & (3 << 4)) >> 4;
right_enc_pos += ENC_STATES[(enc_last & 0x0f)];
}
//读取指定编码器计数
long readEncoder(int i)
{
if (i == LEFT)
return left_enc_pos;
else
return right_enc_pos;
}
//重置指定编码器计数
void resetEncoder(int i)
{
if (i == LEFT)
{
left_enc_pos=0L;
return;
}else
{
right_enc_pos=0L;
return;
}
}
#else
#error A encoder driver must be selected!
#endif
//重置所有编码器计数
void resetEncoders()
{
resetEncoder(LEFT);
resetEncoder(RIGHT);
}
#endif
Arduino编程使用ISR,这个确实是很冷门,对于不是很懂寄存器的同学来说确实很难受。但是这个逻辑,读取编码器计数,如果我们使用Arduino Mega的话,是完全可以把这部分程序迁移过去的,当然你使用UNO或者Nano的情况下,也可以直接使用这个函数来实现。最后的重置所有编码器计数,完全就是套娃!
到这里基本上就已经讲完了的,ros_arduino_bridge所实现的也就是控制电机转速+读取编码器数值,这是最为核心的功能。我们继续来看舵机驱动是怎么实现的!首先是servos.h舵机驱动头文件的内容~
#ifndef SERVOS_H
#define SERVOS_H
#define N_SERVOS 2 //舵机数量声明
//这个以毫秒为单位的延迟决定了伺服每一步行程之间的停顿
//增加这个数字将使伺服扫描更慢。
//减少这个数字将使伺服扫描更快。
//0是默认的数字,将使伺服旋转全速;150毫秒使它们旋转得非常慢。
int stepDelay [N_SERVOS] = { 0, 0 }; //延迟时间
//舵机引脚声明
byte servoPins [N_SERVOS] = { 3, 4 };
//舵机位置初始化
byte servoInitPosition [N_SERVOS] = { 90, 90 }; // [0, 180] degrees
class SweepServo
{
public:
SweepServo();
void initServo(
int servoPin,
int stepDelayMs,
int initPosition);
void doSweep();
void setTargetPosition(int position);
Servo getServo();
private:
Servo servo;
int stepDelayMs;
int currentPositionDegrees;
int targetPositionDegrees;
long lastSweepCommand;
};
SweepServo servos [N_SERVOS];
#endif
哈哈,这代码写的相当有意思~
我们平时在Arduino下使用舵机,是通过模拟输出引脚D3、D5、D6、D9、D10、D11六个引脚。在ros_arduino_bridge当中,我们却不需要!
从这个头文件定义可以看出,我们需要声明舵机的数量、速度、引脚、初始位置在这个头文件进行声明。在SweepServo这类里面,我们只需要去关注的只有两个函数——setTargetPosition函数和getServo函数。我们来看下这部分是怎么实现的~
#ifdef USE_SERVOS
//构造函数
SweepServo::SweepServo()
{
this->currentPositionDegrees = 0;
this->targetPositionDegrees = 0;
this->lastSweepCommand = 0;
}
//初始化
void SweepServo::initServo(
int servoPin,
int stepDelayMs,
int initPosition)
{
this->servo.attach(servoPin);
this->stepDelayMs = stepDelayMs;
this->currentPositionDegrees = initPosition;
this->targetPositionDegrees = initPosition;
this->lastSweepCommand = millis();
}
// 执行扫描
void SweepServo::doSweep()
{
int delta = millis() - this->lastSweepCommand; //获取时间差值
//判定时间差值,每经过一定时间执行
if (delta > this->stepDelayMs) {
//检差当前角度值和目标角度值
if (this->targetPositionDegrees > this->currentPositionDegrees) {
this->currentPositionDegrees++;
this->servo.write(this->currentPositionDegrees); //舵机执行
}
else if (this->targetPositionDegrees < this->currentPositionDegrees) {
this->currentPositionDegrees--;
this->servo.write(this->currentPositionDegrees); //舵机执行
}
this->lastSweepCommand = millis(); //迭代计时
}
}
//设置舵机角度值
void SweepServo::setTargetPosition(int position)
{
this->targetPositionDegrees = position;
}
//读取舵机角度值
Servo SweepServo::getServo()
{
return this->servo;
}
#endif
在这里我看到了熟悉的servo.attach设定舵机引脚和servo.write设定舵机角度值~
又是一个百思不得姐的地方!但是确实是可以实现非模拟输出引脚驱动舵机~
代码单独看每行都认知,放到一起来解读大佬的思想,确实是很懵逼。不知道有没有小伙伴试一下非模拟输出引脚能否驱动舵机呢~
最后是sensors.h传感器驱动头文件,我们来看下内容~
float microsecondsToCm(long microseconds)
{
return microseconds / 29 / 2;
}
long Ping(int pin) {
long duration, range;
pinMode(pin, OUTPUT);
digitalWrite(pin, LOW);
delayMicroseconds(2);
digitalWrite(pin, HIGH);
delayMicroseconds(5);
digitalWrite(pin, LOW);
//同样的引脚用于从PING读取信号))):HIGH
//脉冲的持续时间是发送的时间(以微秒为单位)
// ping到接收到它的回波的对象。
pinMode(pin, INPUT);
duration = pulseIn(pin, HIGH);
range = microsecondsToCm(duration);
return(range);
}
好家伙,这不就是我们超声波测距传感器的代码~
但是我是万万没想到,超声波传感器的发射引脚和接收引脚是同一个!
这真的是把Atmega328P的资源给用到了极致,牛逼!
整体代码的骚操作有很多,真的是佩服!不过个人感觉,这份代码最低可能是两个人完成的,感受了两种完全不同的代码规范!
最后就是我们的主程序,也是我们编译烧录的主体部分内容!
#define USE_BASE // 启用基本控制器代码
//#undef USE_BASE // 禁用基本控制器代码
/ /定义你正在使用的电机控制器和编码器库
#ifdef USE_BASE
/* The Pololu VNH5019 dual motor driver shield */
//#define POLOLU_VNH5019
/* The Pololu MC33926 dual motor driver shield */
//#define POLOLU_MC33926
/* The RoboGaia encoder shield */
//#define ROBOGAIA
/*编码器直接连接到Arduino板*/
#define ARDUINO_ENC_COUNTER
/*L298电机驱动器*/
#define L298_MOTOR_DRIVER
#endif
#define USE_SERVOS // 使能使用PWM伺服定义在servos.h
//#undef USE_SERVOS // 禁用PWM伺服的使用
在代码的开头,我们要启用基本控制器代码,基本控制器我们是编码器连接到Arduino、L298的电机驱动器,如果你有舵机要有的话,需要打开舵机的PWM伺服使能。这里使用预编译 处理,好评!
/*串口波特率*/
#define BAUDRATE 57600
/*最大PWM信号*/
#define MAX_PWM 255
#if defined(ARDUINO) && ARDUINO >= 100
#include "Arduino.h"
#else
#include "WProgram.h"
#endif
/*包含串行命令的定义
#include "commands.h"
/* 传感器函数 */
#include "sensors.h"
/*包括伺服支持,如果需要*/
#ifdef USE_SERVOS
#include <Servo.h>
#include "servos.h"
#endif
#ifdef USE_BASE
/*电机驱动函数定义*/
#include "motor_driver.h"
/*编码器驱动函数的定义*/
#include "encoder_driver.h"
/* PID参数和函数*/
#include "diff_controller.h"
/* PID循环每秒运行30次*/
#define PID_RATE 30 // Hz
/*将速率转换为间隔
const int PID_INTERVAL = 1000 / PID_RATE;
/*跟踪下一次我们做PID计算的时候
unsigned long nextPID = PID_INTERVAL;
/*停止机器人,如果它没有收到一个移动命令在这个毫秒数*/
#define AUTO_STOP_INTERVAL 2000
long lastMotorCommand = AUTO_STOP_INTERVAL;
#endif
/* 变量初始化 */
// 一对变量来帮助解析串行命令
int arg = 0;
int index = 0;
// 变量来保存输入字符
char chr;
// 变量来保存当前的单字符命令
char cmd;
// 存放第一个和第二个参数的字符数组
char argv1[16];
char argv2[16];
// 参数转换为整数
long arg1;
long arg2;
/*清除当前命令参数*/
void resetCommand() {
cmd = NULL;
memset(argv1, 0, sizeof(argv1));
memset(argv2, 0, sizeof(argv2));
arg1 = 0;
arg2 = 0;
arg = 0;
index = 0;
}
/* 运行一个命令,命令在Commands.h中定义 */
int runCommand() {
int i = 0;
char *p = argv1;
char *str;
int pid_args[4];
arg1 = atoi(argv1);
arg2 = atoi(argv2);
switch(cmd) {
case GET_BAUDRATE:
Serial.println(BAUDRATE);
break;
case ANALOG_READ:
Serial.println(analogRead(arg1));
break;
case DIGITAL_READ:
Serial.println(digitalRead(arg1));
break;
case ANALOG_WRITE:
analogWrite(arg1, arg2);
Serial.println("OK");
break;
case DIGITAL_WRITE:
if (arg2 == 0) digitalWrite(arg1, LOW);
else if (arg2 == 1) digitalWrite(arg1, HIGH);
Serial.println("OK");
break;
case PIN_MODE:
if (arg2 == 0) pinMode(arg1, INPUT);
else if (arg2 == 1) pinMode(arg1, OUTPUT);
Serial.println("OK");
break;
case PING:
Serial.println(Ping(arg1));
break;
#ifdef USE_SERVOS
case SERVO_WRITE:
servos[arg1].setTargetPosition(arg2);
Serial.println("OK");
break;
case SERVO_READ:
Serial.println(servos[arg1].getServo().read());
break;
#endif
#ifdef USE_BASE
case READ_ENCODERS:
Serial.print(readEncoder(LEFT));
Serial.print(" ");
Serial.println(readEncoder(RIGHT));
break;
case RESET_ENCODERS:
resetEncoders();
resetPID();
Serial.println("OK");
break;
case MOTOR_SPEEDS:
/* 重置自动停止定时器 */
lastMotorCommand = millis();
if (arg1 == 0 && arg2 == 0) {
setMotorSpeeds(0, 0);
resetPID();
moving = 0;
}
else moving = 1;
leftPID.TargetTicksPerFrame = arg1;
rightPID.TargetTicksPerFrame = arg2;
Serial.println("OK");
break;
case UPDATE_PID:
while ((str = strtok_r(p, ":", &p)) != '\0') {
pid_args[i] = atoi(str);
i++;
}
Kp = pid_args[0];
Kd = pid_args[1];
Ki = pid_args[2];
Ko = pid_args[3];
Serial.println("OK");
break;
#endif
default:
Serial.println("Invalid Command");
break;
}
}
这部分的代码是很核心的一个内容,我们串口控制部分的指令解析就是由这一部分的switch...case语句来完成的。同时在这部分也可以看到波特率、数据处理的频率等信息。我们根据这部分的内容,可以推导出怎么来用这些指令,当然你也可以去阅读官方的手册!
/* 初始化函数 */
void setup() {
Serial.begin(BAUDRATE);
// 初始化电机控制器(如果使用) */
#ifdef USE_BASE
#ifdef ARDUINO_ENC_COUNTER
//设置为输入模式
DDRD &= ~(1<<LEFT_ENC_PIN_A);
DDRD &= ~(1<<LEFT_ENC_PIN_B);
DDRC &= ~(1<<RIGHT_ENC_PIN_A);
DDRC &= ~(1<<RIGHT_ENC_PIN_B);
//使能上拉电阻
PORTD |= (1<<LEFT_ENC_PIN_A);
PORTD |= (1<<LEFT_ENC_PIN_B);
PORTC |= (1<<RIGHT_ENC_PIN_A);
PORTC |= (1<<RIGHT_ENC_PIN_B);
//告诉引脚更改掩码监听左编码器引脚
PCMSK2 |= (1 << LEFT_ENC_PIN_A)|(1 << LEFT_ENC_PIN_B);
//告诉引脚更改掩码监听右编码器引脚
PCMSK1 |= (1 << RIGHT_ENC_PIN_A)|(1 << RIGHT_ENC_PIN_B);
//在普通中断掩码中启用PCINT1和PCINT2中断
PCICR |= (1 << PCIE1) | (1 << PCIE2);
#endif
initMotorController();
resetPID();
#endif
/* 如果使用舵机 */
#ifdef USE_SERVOS
int i;
for (i = 0; i < N_SERVOS; i++) {
servos[i].initServo(
servoPins[i],
stepDelay[i],
servoInitPosition[i]);
}
#endif
}
对不起,请原谅我的无知!
看到了这setup函数,看到了关于电机编码器中断部分的内容,算是把之前留的坑给补回来!牛逼!
/* 进入主循环。
从串口读取和解析输入,并运行任何有效的命令。
在目标时间间隔运行PID计算并检查自动停止条件。
*/
void loop() {
while (Serial.available() > 0) {
chr = Serial.read();
//使用CR终止符
if (chr == 13) {
if (arg == 1) argv1[index] = NULL;
else if (arg == 2) argv2[index] = NULL;
runCommand();
resetCommand();
}
//使用空格分隔命令的各个部分
else if (chr == ' ') {
//遍历参数
if (arg == 0) arg = 1;
else if (arg == 1) {
argv1[index] = NULL;
arg = 2;
index = 0;
}
continue;
}
else {
if (arg == 0) {
//第一个参数是单字母命令
cmd = chr;
}
else if (arg == 1) {
//后续参数可以是多个字符
argv1[index] = chr;
index++;
}
else if (arg == 2) {
argv2[index] = chr;
index++;
}
}
}
//如果我们使用基础控制,在适当的时间间隔运行PID计算
#ifdef USE_BASE
if (millis() > nextPID) {
updatePID();
nextPID += PID_INTERVAL;
}
//检查是否超过了自动停止间隔
if ((millis() - lastMotorCommand) > AUTO_STOP_INTERVAL) {;
setMotorSpeeds(0, 0);
moving = 0;
}
#endif
//舵机扫描执行
#ifdef USE_SERVOS
int i;
for (i = 0; i < N_SERVOS; i++) {
servos[i].doSweep();
}
#endif
}
这是程序的最后一部分内容,可以看到loop主函数部分只有串口读取数据,然后就是把数据传到runCommand函数执行即可,而最后的内容是更新PID以及检测是否存在AUTO_STOP_INTERVAL(前文有,2000ms)内没有数据(意味着断开连接),如果出现后者,机器人则会执行停止。
不过关于舵机和超声波传感器的内容,还是有待考究~
怪哉~
评论(0)
您还未登录,请登录后发表或查看评论