|
|
- 前言 -
⼤家,好久不⻅。
此⽂旨在于对2019年我发表在51黑论坛的贴⼦《请⼯程化,定制化你的单⽚机代码》http://www.51hei.com/bbs/dpj-162218-1.html 进⾏更新,升级和完善。
虽说现在回头⼀看,那篇贴⼦在⼀些细节确实有⼀些问题,不过最⼤的问题是——它不是⼀个完整的⽂章,我匆忙的把⼀些编程要点塞进了帖⼦⾥,然后就⼀直没有更新了。
如今⼀晃就是6年,机缘巧合下,我突然⼜找到了那篇贴⼦。在不知不觉之间,帖⼦已经被置顶,有了这么多的评论和阅读量,这令我甚是惭愧。
感谢⼤家的⽀持。
我近期刚好有时间,那就把帖⼦重写⼀下,并更完⽂章吧。
- 第⼀章:设计⼀个时钟基线 -
在⼀些初级MCU编程⼊⻔书中,经常能够⻅到类似
或者
- while(K1 == 0); //等待⽤户释放键盘上的K1按键
复制代码 这样的代码,表⾯上看没有什么问题,但是本质上跟下⾯的逻辑差不多:
“已知⼩明同学从教室跑去⼩卖部买糖再回来,⼤概需要1分钟,所以,当我们需要计时12⼩时的时候,让⼩明同学这样来回往返个720次,应该就差不多了。”
“你戳⼩明⼀下,他就会叫⼀声但是如果你⼀直戳着不松开,他就会由于脑⼦不够⽤,不但叫不出来,就算你跟他说话他也没法听进去。”
2
在这样的代码中,MCU的PC指针(这东⻄代表着程序当前在哪个代码⾥运⾏)就如同悲催的⼩明同学,被困在那两⾏代码中。也许是巨⼤的资源浪费,也许是⽆法对外部的信息变化做出反应,都是很糟糕的事情。
那么,要怎么样来编写单⽚机程序,才可以⼜实现这些逻辑功能,⼜释放PC指针呢这就是我们接下来要讨论的内容。
1.1 带有时基的主程序架构
正如“ 每个不得不跑去⼩卖部的⼩明,其实他去年买了个表,根本不⽤跑 ”的道理()。
即使是普通的51单⽚机,他也是有定时器的。
我们需要设置⼀个定时器,让定时器会告诉单⽚机:“好的,现在已经过了1ms了(或者10ms这都没关系)”单⽚机⼀拍脑袋:“啊,已经到了1ms了么,那我就把⼀些事情做⼀下吧”。就像下面这样子:
3
在这个框架下,我们的代码看起来会像下⾯这个样⼦:
- unsigned char flag1ms; //全局变量
-
- void main()
- {
- flag1ms = 0;
- mcuConfig(); //MCU的⼀些其他初始化,此处略过
- timer0Init(); //定时器0初始化,略过,需设置1ms定时周期
-
- while(1)
- {
- if(flag1ms)
- {
- flag1ms = 0;
- doAnything(); //单片机可以做任意事情
- }
- }
- }
-
- void timer0XINT() interrupt 1 //定时器T0中断
- {
- flag1ms = 1;
- }
复制代码 为了⽅便理解,这⾥的“⼀系列⼯作”在代码中只⽤了⼀个doAnything()来表⽰,实际应⽤中,往往会放好⼏个⼦程序,没什么问题。
在本程序中,单⽚机每隔1ms会将⼀系列⼯作执⾏⼀遍。从现在开始,你的⼦程序必须要摒弃那些“傻等⾏为”,重新设计你的程序逻辑。
1.2 ⼀个按键程序的例⼦
我们先拿⼀个⾮常恶⼼的按键函数来开⼑,如果没有时基,我们可能会写出这样的⼀个按键程序:
- /* 在没有时基的时候,代码是这样写的 */
- #define K1_PRESSING() ((P1&0x01)==0) //按键位于P1^0引脚
- void keyPress()
- {
- unsigned int key_press_time = 0;
- while(K1_PRESSING()) //等待松开
- {
- ++key_press_time;
- if(key_press_time==300) //300*10ms = 3s
- {
- //⻓按3s时要做的事情
- }
- delay10ms(); //delay计时
- }
- if((key_press_time < 300) && (key_press_time >= 2))
- {
- //短按的处理
- }
- }
复制代码 这个函数兼具delay()语句和while(KEY==0)语句,可以说是⼆毒俱全了。
如果是在新的代码架构中,它可以修改成这样的形式
- /* 本程序将在每个1ms时基中被调⽤ */
- #define JUST_INC(x) if(++x<=0) --x //x在不能溢出的前提下++
- #define K1_PRESSING() ((P1&0x01)==0) //按键位于P1^0引脚
- void keyPress()
- {
- static unsigned int key_press_time = 0; // ……请标为静态变量
- if(K1_PRESSING())
- {
- JUST_INC(key_press_time);//计量按键时间,并避免数据溢出
- if(key_press_time==3000)
- {
- //在此写下按键⻓按3s时要做的事情
- }
- }
- else
- {
- if((20<=key_press_time) && (key_press_time < 3000))
- {
- //20ms ~ 3s之间,视为短按,在此写下写短按的处理代码
- }
- key_press_time=0;
- }
- }
复制代码 功能是⼀样的,但实际⽤起来区别很⼤——旧程序跑1次⻓按功能,新程序已经跑了3000次,且后者不会⼀直占⽤PC指针。
新的程序有些细节,可以展开说⼀说:
①key_press_time现在是⼀个静态变量,static关键字可以让该变量在每次重新进⼊函数的时候不会重新赋值为0,⽽是保留上次退出函数时的值。
②“JUST_INC(key_press_time)”这⼀句,看起来⽤“++key_press_time”就能搞定,但是,谁也不能保证⽤户真的不会按按键超过65秒的啊万⼀他真的按了65576ms单⽚机还就真的以为⽤户“短按”了⼀次呢(65576-65536=40ms,属于短按范畴),下⾯那个短按程序段也会被执⾏现在这样写,哪怕你按100年也没关系了,反正单⽚机就每隔1ms进来看⼀次,K1这个按钮你想按多久就按多久,掉在范围内就处理,超出范围就⽆视。
③“if(key_press_time==3000)”这⾥的3000只是随便设置的⼀个3秒⻓按时间,如果需要做按键⻓短按功能,这⾥就是⻓按程序所放的位置也可以不⽤3000,⽤2000、50000都没事,别超过65534就⾏。
④“if((20<=key_press_time) && (key_press_time < 3000))”这⾥,前⾯的>=20是短按的消抖设计——再强的⼈类也不可能1秒按⼀个按键超过20次,也就是不可能⼩于50ms的时间——这⾥⽤了20ms相当于兼容“超级快男”来按按键了。后⾯<3000是不能和⻓按的时间冲突,因为3s我们已经⼈为的设置成⻓按时间节点了。
⑤这⾥的参数遵循乘法原则。举个例⼦,如果时基是10ms,那么这⾥的if(key_press_time==3000)就会代表30秒,如果想设置⻓按3秒的话,得把3000改成300。
1.3 关于时基的进阶设置
回到刚刚讨论的时钟基线处,这⾥有⼏个⼩问题。
(1)假如,1ms的周期不够把事情做完怎么办
——那就把时基设置⼤⼀些,⽐如5ms,10ms,20ms,还不够就该换芯⽚了,要不就是代码还有很⼤的其他问题,需要好好排查。
(2)我该怎么⼤致估算代码跑完“⼀系列⼯作”需要的时间,好让我设置更合适的时基
——好问题这⾥有⼀个简单且准确的⽅案任意找个闲置端⼝,⽐如P0^0,在⼲活之前先把这个端⼝拉⾼,⼲完活之后⻢上把它拉低,通过外接⽰波器或者逻辑分析仪,就能直观的测量出耗时了。
4
(3)只有1个时基不⽅便,我的按键只需要10ms采集1次,但是LCD显⽰我需要100ms才刷新⼀次。
——时基是可以扩展的!在1个时基的基础上,只要你有需要,完全可以扩展出N多个时基匹配你的N个⼦程序。
⽐如下⾯的代码,就在1ms的时基上分别扩展出了10ms和100ms的时基:
(4)我的各个函数应该放在哪个时基⾥⾯呢
——这要求我们要对⾃⼰的程序要有清楚的把握,以及⼀定的产品思维,以下是⼀⼰之⻅。
①⾸先,所有的函数都要写得简洁⼲净,不要有任何模块的delay()加起来超过0.2ms,⼦程序⾥⾯放⼏个nop倒是⽆伤⼤雅。
②按键,检测,通信这类的⼦程序放到10ms时基⾥。输出,显⽰这类的放100ms时基就OK了。
③1ms时基⾥⾯应该放什么呢可以什么也别放,空着就好。或者把主程序的基础时基换成10ms也可以,其实很少有东⻄需要刷新得这么快的。如果基础时基打算⽤10ms的话,可以将定时器的中断设置为每10ms触发(这样可以删掉flag1ms变量,此处不展开)也可以偷个懒,原本的程序框架改成这样即可
④如果有特别需要关照的部分,⽐如说步进电机的驱动啥的,请放到另⼀个定时器中断⾥(单⽚机基本都⾄少有俩定时器的,不⽤⽩不⽤),按你需要的来设置。
⑤定时器的中断触发时间建议不要少于0.5ms,不然进中断就太频繁了。
现在,我们的代码已经初具雏形,⼤家可以⾃⼰搭⼀下这个框架来体验⼀下。
(当当当,现在是中场休息时间,喝杯⽔吧)
5
|
评分
-
查看全部评分
|