Arduino:关于解决 pluseIn与官方舵机库冲突的办法

关于解决arduino pluseIn与官方舵机库冲突的办法

一、前言

Arduino在国内的知名度逐年提高,多数单片机爱好者选择使用其进行项目开发。期间难免遇见一些从未见过的bug。在实际工程中,能遇到教程书里所见不到的神奇bug,在国内的网络里一般找不到解决办法。就如这次的官方库冲突,也是我在工程训练大赛中才遇见的。比赛中,我们使用arduino作为车体的控制,车体位置定位是用的超声波HR-04模块,车体姿态陀螺仪输出的量也是PWM信号,都需要pulseIn函数进行读取。而车体运动我们采用的是有刷电机带电调,需要向电调输入pwm信号。arduino输出PWM信号我们用的是舵机库中的Servo.writeMicroseconds()函数。发现两者出现干涉。国内很少关于这个问题的解决方法。本例只针对Arduino的AVR系列。maxkz

二、问题所在

arduino常用的UNO板子主要有三个定时器:
timer0————delay(),millis().micros()
timer1————Servo()
timer2————Tone()
冲突根源:
C:\Program Files (x86)\Arduino/libraries/Servo/src/avr/Servo.cpp中的这段程序:

void Servo::write(int value)
{
  if(value < MIN_PULSE_WIDTH)
  {  // treat values less than 544 as angles in degrees (valid values in microseconds are handled as microseconds)
    if(value < 0) value = 0;
    if(value > 180) value = 180;
    value = map(value, 0, 180, SERVO_MIN(),  SERVO_MAX());
  }
  this->writeMicroseconds(value);
}

void Servo::writeMicroseconds(int value)
{
  // calculate and store the values for the given channel
  byte channel = this->servoIndex;
  if( (channel < MAX_SERVOS) )   // ensure channel is valid
  {
    if( value < SERVO_MIN() )          // ensure pulse width is valid
      value = SERVO_MIN();
    else if( value > SERVO_MAX() )
      value = SERVO_MAX();

    value = value - TRIM_DURATION;
    value = usToTicks(value);  // convert to ticks after compensating for interrupt overhead - 12 Aug 2009

    uint8_t oldSREG = SREG;
    cli();
    servos[channel].ticks = value;
    SREG = oldSREG;
  }
} 

从以上代码可以看出,Servo.write(degree)和Servo.writeMicroseconds(value)是差不多一样的,只不过Servo.write(degree)把输入进去的角度转换成脉冲宽度,传入调用的Servo.writeMicroseconds(value)而已,主要问题出在了void Servo::writeMicroseconds(int value)中调用的一个函数cli()上,不少有经验的开发者经常用到,关闭中断 意思是设置了之后,外部中断和内部中断都被屏蔽了,不会去处理了,然后把脉宽值转为Ticks值,从而设置定时器。
我们已知舵机库可以定义任何一个引脚为输出口。以下的代码反应出是通过digitalWrite来实现的


static inline void handle_interrupts(timer16_Sequence_t timer, volatile uint16_t *TCNTn, volatile uint16_t* OCRnA)
{
  if( Channel[timer] < 0 )
    *TCNTn = 0; // channel set to -1 indicated that refresh interval completed so reset the timer
  else{
    if( SERVO_INDEX(timer,Channel[timer]) < ServoCount && SERVO(timer,Channel[timer]).Pin.isActive == true )
      digitalWrite( SERVO(timer,Channel[timer]).Pin.nbr,LOW); // pulse this channel low if activated
  }

  Channel[timer]++;    // increment to the next channel
  if( SERVO_INDEX(timer,Channel[timer]) < ServoCount && Channel[timer] < SERVOS_PER_TIMER) {
    *OCRnA = *TCNTn + SERVO(timer,Channel[timer]).ticks;
    if(SERVO(timer,Channel[timer]).Pin.isActive == true)     // check if activated
      digitalWrite( SERVO(timer,Channel[timer]).Pin.nbr,HIGH); // its an active channel so pulse it high
  }
  else {
    // finished all channels so wait for the refresh period to expire before starting over
    if( ((unsigned)*TCNTn) + 4 < usToTicks(REFRESH_INTERVAL) )  // allow a few ticks to ensure the next OCR1A not missed
      *OCRnA = (unsigned int)usToTicks(REFRESH_INTERVAL);
    else
      *OCRnA = *TCNTn + 4;  // at least REFRESH_INTERVAL has elapsed
    Channel[timer] = -1; // this will get incremented at the end of the refresh period to start again at the first channel
  }
}

再看下面的代码:C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino\wiring_pulse.c

unsigned long pulseIn(uint8_t pin, uint8_t state, unsigned long timeout)
{
	// cache the port and bit of the pin in order to speed up the
	// pulse width measuring loop and achieve finer resolution.  calling
	// digitalRead() instead yields much coarser resolution.
	uint8_t bit = digitalPinToBitMask(pin);
	uint8_t port = digitalPinToPort(pin);
	uint8_t stateMask = (state ? bit : 0);

	// convert the timeout from microseconds to a number of times through
	// the initial loop; it takes approximately 16 clock cycles per iteration
	unsigned long maxloops = microsecondsToClockCycles(timeout)/16;

	unsigned long width = countPulseASM(portInputRegister(port), bit, stateMask, maxloops);

	// prevent clockCyclesToMicroseconds to return bogus values if countPulseASM timed out
	if (width)
		return clockCyclesToMicroseconds(width * 16 + 16);
	else
		return 0;
}

这个函数里并没有用到定时器中断,或者什么其他的中断,其核心在unsigned long width = countPulseASM(portInputRegister(port), bit, stateMask, maxloops);这句话上
我们可以打开同目录的wiring_pulse.S,这里面是一个汇编的代码,不过它有对应的C注释,可以看一下

 * unsigned long pulseInSimpl(volatile uint8_t *port, uint8_t bit, uint8_t stateMask, unsigned long maxloops)
 * {
 *     unsigned long width = 0;
 *     // wait for any previous pulse to end
 *     while ((*port & bit) == stateMask)
 *         if (--maxloops == 0)
 *             return 0;
 *
 *     // wait for the pulse to start
 *     while ((*port & bit) != stateMask)
 *         if (--maxloops == 0)
 *             return 0;
 *
 *     // wait for the pulse to stop
 *     while ((*port & bit) == stateMask) {
 *         if (++width == maxloops)
 *             return 0;
 *     }
 *     return width;
 * }

当执行到pulsIn()的时候,先把这个引脚上的高电平先过掉,再开始读值,这里用的不是中断的方式,是进程阻塞式的,而Servo的定时器溢出时会中断,r然后去改变舵机引脚的电平,期间就过去了几十个时钟周期。虽然有时候也不多就30个us这样,但是对于某些条件下是相当的要命。

三、解决办法

解决办法还是有的

1.关中断

/* Measures the length (in microseconds) of a pulse on the pin; state is HIGH
 * or LOW, the type of pulse to measure.  Works on pulses from 2-3 microseconds
 * to 3 minutes in length, but must be called at least a few dozen microseconds
 * before the start of the pulse.
 *
 * This function performs better with short pulses in noInterrupt() context
 */

从最后那句话中可以看到,作者建议没中断的状态使用这个函数。我们可以noInterrupt()把中断都关了,再pulseIn()。

2.手写PWM

我再网上找的解决方案https://www.cnblogs.com/sjsxk/p/5832406.html
这位老哥用手写的办法解决舵机控制

//这个函数的精确度一般,但是能用

int servopin = 7;    //定义舵机接口数字接口7 ,接舵机信号线,这个IO口随便定义

void servopulse(int angle)//定义一个脉冲函数,这个函数的频率是50hz的,20000us=20ms=1/50hz

{
  int pulsewidth=(angle*11)+500;  //将角度转化为500-2480的脉宽值
  digitalWrite(servopin,HIGH);    //将舵机接口电平至高,反过来也是可以的
  delayMicroseconds(pulsewidth);  //延时脉宽值的微秒数
  digitalWrite(servopin,LOW);     //将舵机接口电平至低
  delayMicroseconds(20000-pulsewidth);
}

void setup()

{
  pinMode(servopin,OUTPUT);//设定舵机接口为输出接口
}
void loop()
{
    //把值的范围映射到0到165左
 for( int angle = 0;angle<180;angle+=10){
    for(int i=0;i<50;i++)  //发送50个脉冲
  {
       servopulse(angle);   //引用脉冲函数
  }  
  delay(1000);  
  } 
}

这个老哥是用这个方法来避开舵机库与PWM函数的冲突,同样也可以运用到这上面

3.第三方库

Arduino的资源是相当多的,我用的解决方法是安装了一个第三方库Servo_Hardware_PWM.h 这个库只用用于Arduino mega2560。该库用了mega2560的定时器3,4,5及其相对应的引脚进行使用,库里直接寄存器操作,省去了大量的时钟周期。示例代码如下:

/* Servo Sweep
 Created by Daniel Duller, 12. January, 2019.
 Changed by Daniel Duller, 11. October, 2019.
 This example code is in the public domain.
*/

#include <Servo_Hardware_PWM.h>

Servo myServo1; //creating a servo object (any custom name could be used)
Servo myServo2;
Servo myServo3;
Servo myServo4;
Servo myServo5;
Servo myServo6;

unsigned int valueMicros = 0; //variable that contains the microseconds
int valueDegrees = 0; //variable that contains the degrees

void setup() {
  myServo1.attach(2); //attaches the servo to pin 2
  myServo2.attach(3);
  myServo3.attach(7);
  myServo4.attach(8);
  myServo5.attach(44);
  myServo6.attach(45);
}

void loop() {
  //option 1 - using microseconds and the writeMicroseconds-function:
  for (valueMicros = 500; valueMicros < 2500; valueMicros++){ //goes from 500us to 2500us (0° to 180°)
    myServo1.writeMicroseconds(valueMicros);	//writes the value of valueMicros to the servo
    myServo2.writeMicroseconds(valueMicros);
    myServo3.writeMicroseconds(valueMicros);
    myServo4.writeMicroseconds(valueMicros);
    myServo5.writeMicroseconds(valueMicros);
    myServo6.writeMicroseconds(valueMicros);
    delay(1);
  }

  //option 2 - using degrees and the write-function:
  for (valueDegrees = 180; valueDegrees > 0; valueDegrees--){ //goes from 180° to 0° (2500us to 500us)
    myServo1.write(valueDegrees);	//writes the value of valueDegrees to the servo
    myServo2.write(valueDegrees);
    myServo3.write(valueDegrees);
    myServo4.write(valueDegrees);
    myServo5.write(valueDegrees);
    myServo6.write(valueDegrees);
    delay(10);
  }
}

感谢阅读———maxkz

  • 8
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值