针对avr atmega328p,利用硬件定时器构建实时多任务的一种设计方法

中山野鬼 发布于 2015/09/08 05:36
阅读 1K+
收藏 0

今天说事,咱就不“哈”了。哈。和开源啥关系,怎么说,系统的原型堆件是用的arduino,自然就来砸 @红薯 的场子了。哈。

帖子的起因是最近正在用avr atmega328p做个小机器人玩具。涉及多个机电控制的同步控制。

先看一个小例子:我们假设一个舵机,其参数如下:

pwm的周期是20ms,0-180度的脉宽为0.5ms - 2.5ms,带负载下舵机最快输出转速大于60度/秒,(正常舵机无负载下,可以有60度/0.2sec的速度)。


现在给定一个角度,45度,则可算出其脉宽为:
 0.5 + 45 /180 * (2.5 - 0.5) = 1ms
对应的pwm波则是在20ms的一个周期内,先拉高1ms,随后拉低,保持19ms。
一种简单的利用定时器的做法则可以利用16位定时器来实现。这里有两个值,TCNT1,OCR1A。 对于16MHz的主频可以预分频8,形成每0.5us一次计数。我们通过溢出中断确定一个周期,通过比较中断确定脉宽。则可以有如下设计:
20ms * 1000 / 0.5 = 40000
1ms * 1000 / 0.5 = 2000
也即,按照2MHz的频率,计数4万次为一个周期,计数2000次则为脉宽。因此我们可设定
TCNT1 = (1ul << 16) - 40000ul
OCR1A = (1ul << 16) - 40000ul + 2000ul
这样,TCNT1累加2000次后,触发比较中断,在对应的中断响应函数中,我们向输出脚写0,而在初始化时,输出脚写1,当TCNT1继续累加达到4万次后,触发溢出中断,在对应的中断响应函数中,我们将输出脚写1,对应重新设置
TCNT1 = (1ul << 16) - 40000ul

这样就没问题了。 

对应任意给定的角度,则可有如下对应关系
#define SETA_PWMWIDTH(angle) do{\
OCR1A = (1ul << 16) - 40000ul + ((angle) * 2000 * 2/ 180 + 500 * 2);\
}while (0)
// 1000 * 2 -> 1ms * 1000 / 0.5
// 2000 * 2 -> 2ms * 1000 / 0.5 
则由此可得到一个设定角度的宏
#define SERVO_2_ANGLE(angle) do{\
uint8_t _tmpangle = max(180,min((angle),0));\
SETA_PWMWIDTH(_tmpangle);\
}while(0)



那么我们有两个舵机,好办,另一个舵机则为
#define SETB_PWMWIDTH(angle) do{\
OCR1B = (1ul << 16) - 40000ul + ((angle) * 2000 * 2/ 180 + 500 * 2);\
}while (0)
#define SERVOB_2_ANGLE(angle) do{\
uint8_t _tmpangle = min(180,max((angle),0));\
SETB_PWMWIDTH(_tmpangle);\
}while(0)
我们要设置3个舵机或者更多,或者我们还有另外一个需要高精度定时器的模块,上面简单的方法就没有的玩了。
上述方法,实际是一种软控制,因为pwm的波形是靠代码控制引脚输出为1还是0来实现的。不是说超过2个就没有办法实现,但是有些方式,是一种不过大脑的设计。例如如下的思路:
1、按照8分频设置计数器
2、设置一个数组,每个单元里存放对应舵机脉宽持续的时间(换算成次数),并排序
3、选择数组中最小值作为当前的OCR1A的值,使得到这个点触发一次比较中断,处理对应引脚拉低。
4、比较中断中,将数组中下一个值给予OCR1A,退出中断。
上述做法大体的思想是,假设舵机A需要在TCNT1 被初始化为 (1ul << 16) - 40000ul后的第3000次时拉低,而舵机B需要在TCNT1 被初始化后的3200次拉低对应引脚。那么我们首先发生的是针对舵机A的比较中断,此时TCNT1到达了
(1ul << 16) - 40000ul + 3000
而处理完对应引脚在退出前,将OCR1A改成(1ul << 16) - 40000ul + 3200,则上述中断发生后的200 * 8 个 系统时钟后,会再次触发比较中断。于是很high 很high的依次比较下去。
为什么说这种设计方式不过大脑呢,一个很简单的道理,如果数组里面相邻的两个值只相差1,那么第二次中断比较,应该在第一次中断发生之后8个系统时钟时发生(8分频),代码处理的过来吗?参见atmega328p的6.7.1,一次中断至少4个cycle。其他事不干了?call一次函数需要4个周期,ret一次函数需要4个周期,此处且不谈 是否有些操作可以放在ret之后执行,等指令级的优化策略(avr很久没搞了准确说10年了,可能没有这种优化,以前做dsp优化时是可以的)。
对应上述问题,则存在两种调整做法。一种是提高计数器累加1次所需要的系统时钟,就是增加分频数,例如,256。这样做的问题也存在,就是控制精度不够。针对16MHz主频,256分频,则每16us一次计数。而如果舵机每转1度(实际上不需要这么高的精度,此处仅是抬杠举例),脉宽变化是 2ms / 180 = 11.1us
另一种方式则更可行,我们预分频不变。但对数组之间的数值做调整。

我们假设针对256分频,相对原有8分频,则倍数是32,我们修改某些值(不得已下)并满足以下条件:

1、相邻两个数值差大于等于32

2、修改后的偏移量不会超过原先位置的16,也即不会形成误差累计。

该算法很简单如下:
uint8_t i;
	registor uint16_t m,M,Mo;
	for (i = 0 ; i < n - 1 ; i++){
		m = a[i];M = a[i+1];
		if (m + 32 < M){
			continue;
		}
		if (m + 16 >= M){
			a[i+1] = m;
			continue;
		}
		M = a[i + 1] = m + 32;
		for (j = i + 2 ;j < n ;j++){
			if (a[j] <= M){
				a[j] = M;
			}else{
			break;
			}
		}
		i = j - 2;
	}




(注意上面代码没有实测,本帖子所有代码都没实测过,只讲道理,不抓bug,哈)
上面的算法大体思想如下:
1、如果当前点和后续点之间的差值大于32,则不考虑他们的差异(此时保持原有精度)
2、如果当前两个点的差值小于16,进行修正后者等于前者,其认为两个点是相同的比较值,此时和第一种调整方法的效果相对等同。
3、如果当前两个点的差值介入16到32之间,则将后一个点的比较值增加,保证两点之间至少32个计数间隔。但此时可能会导致后续的一些点小于了调整后的点,例如
1,31,32
针对1,和31比较后,31会调整为33,此时比后一个32还要大。由此就是下面的内层循环(当然这里仍然是一次遍历),将i+1之后的点和i+1进行比较,如果比调整后的a[i+1]还要小,则对应调整的和a[i+1]一样大。有点可以放心,这种调整,不会出现误差累计放大,因为后续点相对调整偏移肯定不比a[i+1]大。
这种做法在精度和每次中断之间保持足够间隔的两种需求中做了点点折中,并且还是有不少概率,可以保证精度的。
按照道理,咱们的事情就算讨论完了。其实上面废话了大堆,只是引个头。上述做法是可以较好的针对舵机了,虽然存在多个比较,但总周期不变。也就是说,我们的TCNT1在每次溢出中断后,都会设置同样的值,假设各种舵机的周期都是20ms,哈。
好了,问题来了,如果我们需要其他的处理周期怎么办?例如频率为1kHz的事件,也即,每个1ms我们需要干点什么。基于相同的预分频,我们还可以用到OCR1B来折腾。但这样也架不住多个不同的周期/频率的事件。当然我们还可以将诸如1kHz的事件,算出20个事件,并将对应计数值放入上述数组中排列。
这里我们考虑另外两个问题。
1、上述8分频16位计数器的最大周期, 65536 * 0.5us = 32768us = 32.768ms,如果遇到低频事件,例如20Hz,每个周期50ms,上述计数器就玩不转了。可能增加如下代码也行:

uint8_t flag = 0;


	...
	if (flag == 0){

	}else{
	//do something ...
	}
	flag = !flag;




这样通过两次(或以上)周期的工作来应对小于约31Hz(1000 / 32.678)的事件。不过代码逻辑复杂度上去了。
2、中断发生,中断使能被清,中断响应函数结束是reti,会重新设置中断使能。简单说,响应过程中不会形成嵌套。这是好事,不混乱,也可能是坏事,会漏事情。
例如,如上数组,我们三个比较值依次间隔256个cycle的时间。第一次比较发生后,首先将第二次的比较值设置到OCR1A中,但调用的函数A实际超过512个cycle,在执行它时TCNT1还在向前走。等函数A返回时,再次发生中断,也即第二次设置OCR1A的比较成功,此时将第三个比较值放入OCR1A,但此时TCNT1已经走的超过了这次设置的OCR1A值,然后,就没有然后了,最后,就是溢出中断了。

上述两个问题,综合起来简单说就是,一个系统中间存在不同频率的事件,整在一起就是一个字“乱”。那么应对这个问题,就需要对处理对象-“事件”进行区分,以分别应对(这里其实是系统设计的思想)。

就谈一个舵机,我们可能有三种事件。

1、舵机本身保持一个角度,需要20ms周期,在0.5-2.5之间存在高电平的脉宽输出事件。
2、我们希望舵机按照一个速度转到另一个角度,则可能需要将转角差值等分(至少等分吧)在多个周期中,例如现在希望按照每秒10度的速度运转2秒,则我们构造一个10Hz的角度修正事件,每次设置一次舵机的目标角度,也即增加1度。
3、无论是遥控,或者红外巡线,或者视频识别,总要定期的获取当前执行单元对应的执行目标-也即,“执行任务”检测事件。
上述三种事件你会发现他们实际存在于 系统的不同级中。这里可以暂且命名为:
驱动级,和器件工作特性有关。
功能级,和器件展开行为有关。
系统任务级,和模块之间协同有关。

上述三层事件对周期的要求实际上是不同的。

驱动级的,不严格就扯淡了。舵机的脉宽你随意拉长点试试。功能级相对好一点,受离散数值计算逻辑的约束,你也不可能做到分解功能形成的各个执行动作完全平滑

简单说,对各周期的误差容忍度,功能级的,还是相对大点点的。例如,每秒10度,你第100ms定义1度,第205ms定义2度,等等,外部感受不是太大。特别是对于开环反馈的,此处不谈高精度闭环自控。(这和德国人的严谨没啥关系,开环反馈下,总是存在不确定性的,误差是必然存在,片面追求高精度是个伪命题,如同时速100公里每小时的汽车,你让他严格控制1ms实际均跑指定的距离?可行吗?注意此处是说实际速度不是说转速,遇个坑就能把你的高精度设计坑死)。

而系统任务级和驱动级、功能级最大的差异在于,协同事件存在的不可测性。我怎么知道什么时候会有控制任务来?例如遥控方式,啥时转弯,啥时加速,这个控制任务是不可测的。由于这种不可测性,系统任务级的事件对时序的要求,就更好说了,当然因为更好说话了,通常也对应需要缓冲区来补偿一下因为不能及时响应而对对方幼小心灵的打击。
简单总结:驱动级的,我们需要高精度的时间控制。不求两个设备周期的初相位在同一时刻,但求每个设备自身的执行周期稳定。而功能级的是低精度的。系统任务级的则是可相对随意的。
在另一方面,驱动级的由于时间控制要求高,因此,执行逻辑应当可以丢失,功能级的则执行逻辑不应当丢失,但执行结果应当允许误差累计。换种说法,如果驱动级的执行逻辑,在指定周期内不能完成,要么后续逻辑可以不执行且不会影响器件,(下个周期时该器件的控制总是从固定的初始状态开始),要么代码上板子前就需要重改,而不能说大概率情况下ok,小概率情况下,可能会有额外延长执行时间。而功能级的执行逻辑适当允许小概率下,执行周期过长,此时系统通常体现在一个字上,“卡”。至于系统任务级,无所谓啦,忙就不折腾,不忙就定期折腾折腾,如果系统忙,不折腾,此时系统通常体现在两个字上,“死机”。
基于上述的系统分析,那么就可以考虑一下,如何针对avr atmega328p(反正就是针对单片机,哈)构建一种实时多任务的处理方法。这里不是说多进程,更不是多线程。这里没有操作系统,我们只是希望有多个任务时,则有对应的多个任务入口函数,大家能在指定的时间内跑上一轮,好保证下一次时间片到来时,再跑一轮。
进程、线程过于复杂,单片机也没有mmu,至少我是放弃在这种单片机上折腾rtos的想法。至于什么Protothread库,只能说是“逻辑上”看上去还像,实际上还是没有解决最根本的问题,因为定时仍然需要你自己折腾,它只适合在系统任务级上玩一玩。

这里给出一个大体思路。

针对驱动级的,我们采用上述数组的方式,利用中断来执行每个事件,但要保证每个时间点的事件(各种分支逻辑下)的总执行时间小于虚拟的时间片,如上面的利用8预分频32个计数的方式,则要小于256个cycle。由于单片机只有一个核,真正执行指令还是依次做的,如果两个事件是在一个比较中断下都需要执行,则需要考虑他们的时间总和是否小于256cycle,无论他们的初相位是否相同,都需要如此考虑。那么不能保证时咋办?如上说的,要么改代码逻辑、压缩指令,(假设预分频不能提高)要么换片子(这个谁建议换,谁保证不是干活的)所以还是该设计目标的好。

我们在驱动级的比较数组中,增加一个诸如1kHz的比较值,如上例,16MHz 8 预分频,假设TCNT1从0开始,则我们增加 2000,4000,。。。。 64000的比较计数值,当然第二次则初始值要做调整,保证在没有其他事件影响下,1kHz可以精准实现。

对应上述的响应动作,则有如下代码:

#define FUNC_MAX 8
uint8_t flag ;
uint8_t func_count[FUNC_MAX];
uint8_t func_cycle[FUNC_MAX];//1 cycle is 1ms
uint16_t millis = 0;
uint16_t last_millis = 0;
typedef uint8_t(*_MODULE_FUNC)(void);
_MODULE_FUNC pfunc[FUNC_MAX] = {.....};

void xxxx_interrupt(...){
	uint8_t i;
	uint8_t delta;
	millis++;
	delta = millis - last_millis;
	if (flag){
		return;
	}
	flag = 1;
	for (i = 0 ; i < FUNC_MAX ;i++){
		if (func_count[i] > delta){
			func_count[i] -= delta;
		}else{
		
			func_count[i] = func_cycle[i];
			sei();
			pfunc[i]();
			cli();
		}
	}
	
	cli();
	last_millis = millis;
	flag = 0;
}



上述xxxx_interrupt函数是针对1KHz的响应事件。所有功能级的任务都在这个函数中进行。

这里的flag是用于保证只有双层嵌套。简单说,当该函数执行过程中,已经过了1ms,新的1KHz中断又来,此时就直接return掉,等于放弃掉这次响应。当然在开中断前(循序嵌套)millis和delta是需要修正的。
打开中断后,后面的具体事件的入口函数中的代码就无所谓了。慢慢执行。而之前是判断各个功能级的任务是否达到指定时间。例如有的是针对100ms,有的是针对43ms的。并不是每次1KHz的响应中所有任务都执行。注意,舵机的周期是20ms,但对他的pwm波的形成仍然是驱动层的,功能层和驱动层不能通过事件频率的高低来判断,当然功能层我们也不给太高的频率。

如果当前计数器func_count[i] <= delta,时则重置func_count[i];。

诸如需要100ms,43ms才执行一次,对应的func_cycle中则存放100或43。

如果在整个执行期间,有驱动级的中断发生,发生就发生了,先让驱动层能安稳的工作,回头还会返回到这个函数中继续执行。这不是个事。唯一是个事的,是在执行期间,超过了1ms,此时这个函数会嵌套,这是个事,意味着如果有个功能级的事件就是1KHz的要求,则必然会有一次无法执行。咋办?要么优化代码,要么“卡”就“卡”咯,难不成换芯片,重新堆环境?当然1KHz的基准节拍可以调整到500Hz,也即2ms一个周期,这可以有32K个cycle,应该够玩了。
最后就是系统任务级。系统任务级最无聊的实现方式就是如下代码:
while (1){
		xxx();
		yyy();
		delay();
	}




更合理的做法如下:
while (1){
		swtich (status){
		case :
		 ....
		case :
		...
		default:
			delay(xx);
		}
	}




这里不得不说一句,这个delay(xx)不要搞定时器了。定时器里面的比较数值,优先给驱动层用吧。
一种简单的做法可以如下折腾:
void delay(uint8_t xx){
		while (xx--){
			for (i = 0 ; i < 2048; i++){
				nop();
				nop();
				nop();
				nop();
				nop();
				nop();
			}
		}
	}




我没有仔细核对for 循环在avr下的执行cycle数,假设是2个cycles,所以中间折腾6个,16MHz,中间循环2048次,就是所谓的1ms。给如的xx就是XX毫秒。
之所以说是“所谓”的,是因为这期间会被定时器中的中断打断,去忙驱动层的和功能层的事情。你问这个delay不精确咋办?所谓“智慧”其中一点就是讲究"平衡",系统任务层的,精确啥?有必要吗?大差不差,如果觉得时间长了,原先delay(15);的你delay(12);不就得了。
“啊这样我怎么用delay控制led灯每秒闪两次啊,怎么能大差不差呢?”

遇到这种问题我只能说,您老大,我败了,等于我这个帖子啥都没讲。

至于arduino的源码,我就不“呵呵”了。一则很多是c++写的,好吧我安心忙我的c,我不理解很多不是面向对象的的问题,为何用面向对象的语法,既然早就说过我压根不懂c++,就不扯淡了,二则,貌似不少开源的伙计很容不得别人说代码质量不行。哈。

加载中
0
子木007
子木007
鬼哥码了这么多字,辛苦了, 其他的只能路过了……
0
0
中山野鬼
中山野鬼

引用来自“管梨员”的评论

鬼哥码了这么多字,辛苦了, 其他的只能路过了……
哈,不辛苦,正好自己整整脑子。。
返回顶部
顶部