Arduino UNO基于Timer2的舵机驱动库(精度比官方的高) 原博客格式更友好:http://www.straka.cn/blog/more-accurate-arduino-uno-timer2-servo-driver-library-than-official-one/ 事情是这样的,本来有个小车,想改装下,已经有的驱动板上引脚已经限定了用途和功能,最终的结果就是,如果我想用红外发射库,就无法同时使用舵机对小车进行调速,因为他们都用了 Timer1定时器,何况我还要同时在3、5引脚使用pwm。无奈之下只能寻找别的办法。 先在网上了解了下ARDUINO的定时器、中断、PWM、舵机控制,红外收发等相关知识。尤其是仔细阅读了AVR atmega328p,也就是ARDUINO UNO的芯片手册的定时器部分,其中有两点: 1. AT mega328p的定时器有3个,对应Arduino UNO板子, B. Timer1 对应 9、10引脚pwm, 16bit 由于红外接收发射库可以选择timer2或者timer1作为38khz载波发生定时器,(之所以不用timer0,因为timer0是用于delay这种延时函数的,所以如果被征用了会导致延时异常)。考虑到38khz频率相对较高,如果我在中断中做些其他的处理,容易导致其载波频率不可靠,所以还是选择改动舵机库,毕竟舵机的频率低,对时间准确性要求相对低,而我对pwm的准确性要求就更低了,所以考虑用定时器2同时作为pwm和舵机控制的定时器。 先看官方库代码: #define usToTicks(_us) (( clockCyclesPerMicrosecond()* _us) / 8) // converts microseconds to tick (assumes prescale of 8) // 12 Aug 2009 #define ticksToUs(_ticks) (( (unsigned)_ticks * 8)/ clockCyclesPerMicrosecond() ) // converts from ticks back to microseconds
#define TRIM_DURATION 2 // compensation ticks to trim adjust for digitalWrite delays // 12 August 2009
//#define NBR_TIMERS (MAX_SERVOS / SERVOS_PER_TIMER)
static servo_t servos[MAX_SERVOS]; // static array of servo structures static volatile int8_t Channel[_Nbr_16timers ]; // counter for the servo being pulsed for each timer (or -1 if refresh interval)
uint8_t ServoCount = 0; // the total number of attached servos
// convenience macros #define SERVO_INDEX_TO_TIMER(_servo_nbr) ((timer16_Sequence_t)(_servo_nbr / SERVOS_PER_TIMER)) // returns the timer controlling this servo #define SERVO_INDEX_TO_CHANNEL(_servo_nbr) (_servo_nbr % SERVOS_PER_TIMER) // returns the index of the servo on this timer #define SERVO_INDEX(_timer,_channel) ((_timer*SERVOS_PER_TIMER) + _channel) // macro to access servo index by timer and channel #define SERVO(_timer,_channel) (servos[SERVO_INDEX(_timer,_channel)]) // macro to access servo class by timer and channel
#define SERVO_MIN() (MIN_PULSE_WIDTH - this->min * 4) // minimum value in uS for this servo #define SERVO_MAX() (MAX_PULSE_WIDTH - this->max * 4) // maximum value in uS for this servo
/************ static functions common to all instances ***********************/
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 } } SIGNAL (TIMER1_COMPA_vect) { handle_interrupts(_timer1, &TCNT1, &OCR1A); }
static void initISR(timer16_Sequence_t timer) { ...... TCCR1A = 0; // normal counting mode TCCR1B = _BV(CS11); // set prescaler of 8 TCNT1 = 0; // clear the timer count TIFR1 |= _BV(OCF1A); // clear any pending interrupts; TIMSK1 |= _BV(OCIE1A) ; // enable the output compare interrupt ...... }
而定时器2是8bit的,要完成5k次的tick,光靠COMPA的中断是不够的,还需要纪录中断的次数,因而会复杂一些。 其实现方式为每个舵机对应一个对象,其成员包括该舵机在每个周期(本文中的周期会指两个概念,一个是舵机驱动要求的周期,即20ms,另一个是单片机系统的中断周期,即256ticks,也即指TIMER2_OVF_vect或TIMER2_COMPA_vect终端向量的处理周期,后文中如不加说明,特指后者,如果指前者会加上20ms以做区别)内需要触发的COMPA中断次数即counter和额外的tick次数即reminder。ISRCount始终标记了当前handle的舵机周期20ms内所经过的中断次数,当ISR等于counter,说明该舵机所需的中断周期数已经满足,那么还需要额外经过255-reminder次ticks,所以将TCNT2设置为reminder,则会在255-reminder时间后触发中断,完成高电平的脉冲,开始低电平脉冲后进入下一个舵机的控制阶段。 首先要说明单个定时器是如何对多个舵机进行控制的,由于前面的第二点,舵机控制周期为20ms,但是其中高电平最多占2.5ms,那么很容易想到,当一个舵机的高电平结束后就可以开始下一个舵机的高电平控制,那么所能控制的舵机数量就是20ms/2.5ms=8个,当然这里面要注意的是: 1. 舵机控制直接的切换是需要耗时的,所以如果要控制8个,舵机的高电平就不能达到2.5ms,会略少于2.5ms 2. 此外如果加入复杂的计算,也可以实现控制更多舵机的能力,就是让舵机的高电平直接有所重合,这个有兴趣的可以去试试啦。 例如,16M晶振,prescale为8,每us tick数为2次,如果一个舵机需要高电平1ms,那么就是2000次tick,2000/256= 7, 2000%256=208,所以当前一个舵机高电平结束,OCRA为0,第一次COMPA中断开始,将OCRA置为208,经过8次中断,时间恰好经过2000次TICK,即1ms,然后第八次中断中将OCRA置为0,再经过10次中断,在第19个中断周期中发生第11次中断,这个中断中将OCRA改成0,由于中断中对OCRA的修改在下一个中断周期中才生效,因而实际每个舵机是19个中断周期,那么实际每个舵机可以达到的最大高电平时间为19*256/2=2432us,那么经过8个舵机时间后,仍然达不到20ms,所以需要对整个周期20ms进行修正,20ms-2432us*8=544us,所以需要增加矫正544*2/256=4,544*2%256=64,即矫正4个中断周期,64个tick。 而对于pwm功能,定义pwm_t结构体,其中ctn为所需要经历的总的溢出中断次数,ocr为溢出中断比较值,即当溢出中断次数达到ocr次后输出低电平,不足输出高电平,cur为当前pwm引脚的计数,这么设置好处是对于不需要pwm分级为256级的pwm应用,可以将pwm分级变小,即ctn设小一些,如此以提高pwm频率,如果ctn设为255则pwm频率约为30.5Hz,如果ctn设为15,则pwm频率为488.3Hz,这个大家可以进行取舍。
,或者参考另外一篇博文#####。这里稍微说明下使用的模式是FastPWM,在TCNT2技术达到TOP位置0xFF和OCRA位置分别中断,选这个模式原因见前文所述。<span]代码去原博客看吧,论坛对代码不友好,太费劲了 static void initISR(){
servos[MAX_SERVOS].activated = false;
servos[MAX_SERVOS].cycles = PERIOD_REVISE_CYCLES;
servos[MAX_SERVOS].ticks = PERIOD_REVISE_TICKS;
COMPACtn = 0;
curChan = 0;
TIMSK2 = 0; // disable interrupts
TCCR2A = _BV(WGM21) | _BV(WGM20); // fast PWM mode, top 0xFF
TCCR2B = _BV(CS21); // prescaler 8
TCNT2 = 0;
TIFR2 = _BV(TOV2) | _BV(OCF2A);
TIMSK2 = _BV(TOIE2) | _BV(OCIE2A); //enable ovf & ocra interruption
inited = true;
}
PWM的中断处理,循环判断每个pwm通道是否需要切换电平或者重新计数。
ISR(TIMER2_OVF_vect)
{
for(uint8_t i=0;i<pwmCount;i++){
pwms[i].cur++;
if(pwms[i].cur <= pwms[i].ocr){
digitalWrite(pwms[i].pin, HIGH);
}else {
digitalWrite(pwms[i].pin, LOW);
}
if(pwms[i].cur == pwms[i].ctn){
pwms[i].cur = 0;
}
}
}
舵机驱动的中断处理,详细解释见前文。
ISR(TIMER2_COMPA_vect){
++COMPACtn;
if(COMPACtn == 1){
if(servos[curChan].activated){
digitalWrite( servos[curChan].pin, HIGH);
}
OCR2A = servos[curChan].ticks;
}else if(COMPACtn == servos[curChan].cycles + 1){
if(servos[curChan].activated){
digitalWrite(servos[curChan].pin, LOW);
}
}else if(curChan == MAX_SERVOS && COMPACtn >= PERIOD_REVISE_CYCLES){
curChan = 0;
OCR2A = 0;
COMPACtn = 0;
}else if(COMPACtn > CYCLES_PER_SERVO){
++curChan;
OCR2A = 0;
COMPACtn = 0;
}else if(COMPACtn > servos[curChan].cycles + 1){
if(servos[curChan].activated){
digitalWrite(servos[curChan].pin, LOW);
}
}
}
舵机的设置
void Timer2Servo::write(uint16_t value){
if(value < MIN_PULSE_WIDTH){
if(value > 180) value = 180;
value = map(value, 0, 180, min_, max_);
}
this->writeMicroseconds(value);
}
void Timer2Servo::writeMicroseconds(uint16_t value){
if(servoChan_ >= MAX_SERVOS){
return;
}
if(value < MIN_PULSE_WIDTH){
value = MIN_PULSE_WIDTH;
}
if(value > MAX_PULSE_WIDTH){
value = MAX_PULSE_WIDTH;
}
servos[servoChan_].cycles = value * TICKS_PER_MICROSECOND / TICKS_PER_CYCLE;
servos[servoChan_].ticks = (value * TICKS_PER_MICROSECOND) % TICKS_PER_CYCLE;
}
以上就是不考虑20ms周期精度,不考虑偶尔出现的因前一个中断处理未结束而后一个中断时间又到了导致后一个中断错过了,从而造成的高低电平脉冲时间不准确,此外上述的库无法对某个舵机输出2.5ms的高电平,也就是通常指的180°,因为最大的可设置毫秒数是2430,即使官方可设置毫秒数也不过544~2400,而我的前一个版本已经可以达到500~2430。那么接下来我们对这个进行修正。
首先我们修正可以达到的脉冲时间。由于前文所述,如果要控制8个舵机,最大只能达到2432us的脉冲,那么为了能达到2500us的脉冲时间,我们只能牺牲一个舵机的控制能力,虽然通常的应用场景,一个timer2控制7个舵机也是足够了。如果最大舵机数量设为8,如果给每个舵机多分配一个中断周期,即20个中断周期,那么最大可以达到2560us,已经可以满足,此时修正CYCLES数为16,但还没完,富余的16个修正中断周期有点多,我们再给每个舵机加一个中断周期,这样还需要修正9个中断周期,后文会解释为什么每个舵机还需要一个中断周期。
在修正完中断周期基本后,为了能更准确的达到20ms,需要在所有舵机包括虚拟舵机的中断周期结束后修正TICKS,为了能实现这个功能,需要在COMPACtn>PERIOD_REVISE_CYCLES满足后调整TCNT2。
另外需要解决的是,当某个舵机的脉冲接近128的整数倍,即脉冲ticks总数接近256的整数倍,也即所需要设置的ticks数接近0或者255,那么很容易导致某个COMPA中断被跳过,进而导致脉冲时间不准或者周期不准。为了修正这个问题,我们将舵机驱动对象重新定义:
typedef struct{
uint8_t pin=0;
volatile uint8_t cycles=0;
volatile uint8_t startTicks=0;
volatile uint8_t endTicks=0;
bool activated=false;
}servo_t;
即将原本单个ticks成员改为两个startTicks和endTicks,这样如果原本的ticks值离0或者255很近,则将整个舵机的脉冲在这个舵机的处理周期内进行偏移,这样每次的COMPA中断位置就不会离0或者255很近,也就很难miss。具体做法见代码:
void Timer2Servo::writeMicroseconds(uint16_t value){
if(servoChan_ >= MAX_SERVOS){
return;
}
if(value < MIN_PULSE_WIDTH){
value = MIN_PULSE_WIDTH;
}
if(value > MAX_PULSE_WIDTH){
value = MAX_PULSE_WIDTH;
}
value = value * TICKS_PER_MICROSECOND - TRIM_PULSE_TICK;
servos[servoChan_].cycles = value / TICKS_PER_CYCLE;
uint8_t ticks = value % TICKS_PER_CYCLE;
if(ticks>=256-2*TRIM_TICKS){
servos[servoChan_].cycles++;
servos[servoChan_].startTicks=3*TRIM_TICKS;
servos[servoChan_].endTicks=ticks+3*TRIM_TICKS;
}else{
servos[servoChan_].startTicks=TRIM_TICKS;
servos[servoChan_].endTicks=ticks+TRIM_TICKS;
}
}
而COMPA的中断处理函数也将变成:
// Handle compare A register to provider servo driver
ISR(TIMER2_COMPA_vect){
#ifdef __DEBUG
++compa_times;
#endif
++COMPACtn;
if(COMPACtn == 1){
OCR2A = servos[curChan].endTicks;
if(servos[curChan].activated){
digitalWrite( servos[curChan].pin, HIGH);
}
}else if(curChan >= MAX_SERVOS && COMPACtn > PERIOD_REVISE_CYCLES){
// also trim to adjust period, not too close to 255 encase miss the next
// interruption. TCNT2_TRIM + PERIOD_REVISE_TICKS is the actual revise.
TCNT2 = 255 - TCNT2_TRIM;
COMPACtn = 0;
curChan = 0;
OCR2A = servos[0].startTicks;
}
if(curChan < MAX_SERVOS && COMPACtn > servos[curChan].cycles){
// a bit larger than 0 to ensure not miss the next interruption
OCR2A = TRIM_TICKS;
if(servos[curChan].activated){
digitalWrite(servos[curChan].pin, LOW);
}
}
if(curChan < MAX_SERVOS && COMPACtn > CYCLES_PER_SERVO){
++curChan;
OCR2A = servos[curChan].startTicks;
COMPACtn = 0;
}
}
pwm的控制因为前述对TCNT2改动导致中断周期变化,因而PWM周期会有轻微抖动,好在抖动幅度不大,在30.6到30.7之间变化,所以精度影响小于0.5%,还可以接受吧,哈哈。
另外需要改进的是,由于digitalWrite的延时较大,如果pwm所有通道都在第1个周期拉高,那么第一个周期的处理时间会比较久,容易导致COMPA中断被miss,所以将pwm各通道的起始拉高周期错开,第i个通道在第i个周期拉高,所以pwm的结构体也发生变化:
typedef struct{
uint8_t pin=0;
volatile uint8_t start=0;
volatile uint8_t end=0;
}pwm_t;
这样牺牲了前一版本的频率可定制的灵活性,保证了精度。
Pwm的实现简单很多,这里不细说,看代码:
void Timer2Pwm::write(uint8_t pwm){
// uint8_t oldSREG = SREG; // Reverse these codes for future use.
// cli();
pwms[pwmChan_].start = pwmChan_;
pwms[pwmChan_].end = pwm + pwmChan_;
// SREG = oldSREG;
}
// Handle overflow interrupt to provide pwm
ISR(TIMER2_OVF_vect)
{
curPwm++;
for(uint8_t i=0;i<pwmCount;i++){
if(curPwm == pwms[i].start){
digitalWrite(pwms[i].pin, HIGH);
}else if(curPwm == pwms[i].end){
digitalWrite(pwms[i].pin, LOW);
}
}
}
原博客格式更友好:http://www.straka.cn/blog/more-accurate-arduino-uno-timer2-servo-driver-library-than-official-one/ 最后要做的就是上逻辑分析仪看下输出的实际情况,然后做些数值上的修正,主要是digitalWrite会有一定的延时导致的,实际写的时候还需要仔细思考下分支判断的边界和顺序,这个只有动手写一遍才能体会,过于细节不详述。 附上几种方案实测对比: 测试程序为循环设置舵机脉冲时间,多个舵机脉冲时间相差5us,循环步进30us,代码地址: https://github.com/atp798/Timer2ServoPwm/tree/master/examples/ServosAndPwms 官方库:
未修正的版本:
修正后的高精度版本:
粗略统计,官方的版本,周期误差稳定15us左右,脉冲误差1~2us,平均1us左右,未修正版本周期误差4~20us左右,不稳定,脉冲误差1~2us左右,平均1us,但是较容易出现偶然的脉冲误差整个周期即128us的情况。修正后版本的周期误差稳定小于1us,脉冲误差稳定在0.5~2us,平均小于1us。 最后修正版代码见: https://github.com/atp798/Timer2ServoPwm 参考资料: Topic: ServoTimer2 - drives up to 8 servos: https://forum.arduino.cc/index.php/topic,21975.0.html https://github.com/nabontra/ServoTimer2 G哥撸Arduino之:深入理解PWM输出: https://www.arduino.cn/forum.html?mod=viewthread&tid=80668 关于如何修改ATMEGA328P的PWM频率: https://www.arduino.cn/thread-83019-1-1.html 舵机常见问题原理分析及解决办法 https://blog.csdn.net/fang_chuan/article/details/51557069 舵机控制原理是什么_舵机的控制方法 http://m.**/article/687067.html 原博客格式更友好:http://www.straka.cn/blog/more-accurate-arduino-uno-timer2-servo-driver-library-than-official-one/
|