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这样,但是对于某些条件下是相当的要命。
解决办法还是有的
/* 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()。
我再网上找的解决方案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函数的冲突,同样也可以运用到这上面
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 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