如果你需要提下列问题,请自觉阅读本贴,如果读完以后你还要贴代码,偶就没办法了。
1.I2C程序怎么调
2.为什么我的程序不对,然后贴一堆代码上来
3.为什么我这样写对,那样写不对
首先建立一个观点:程序是搞
电子技术里面最最最最简单的东西,因为他最最最说得清楚,你这样写了,处理器就会这样做,如果没有按照你的想法来做,那一定是你没写对。
话说硬件设计还需要很多数据库似的知识支撑,高频还需要黑色艺术细胞,学写程序除了背点语句,掌握一些基本技巧外加做好规划之外,真的不需要啥东西,会说话就会写程序。
现在言归正传,前面说了,程序就是你想处理器做什么。在你动手之前,应该先找张白纸,写下你想干什么,然后画出流程图(贴代码问程序的请扪心自问,是否画了流程图)。再明确程序的模块划分,每个模块的输入、输出变量和占用资源。
模块划分就是把任务拆分成互不相关的部分,比如软主机I2C程序可以拆分成发生启动位、发送停止位、发生一个字节、接收一个字节、发送应答位、发送非应答位、接收一个字节、读取应答位等。
其次是要想方设法从最基本的地方开始调试,来这里贴代码的都是一贴一大篇,要知道我们都不敢这样写代码,何况是自称新手?程序要写一块调一块,细到什么程度?真是菜鸟,就细到写完发送启动位就送出来拿示波器检查,检查边沿、电平和时间参数是否跟自己设计的值一样。
模块有大有小,取决于站在哪个层次看问题和写程序的熟练程度。如果是从一个使用PCF8563的数字钟系统来说,I2C通讯整个可以看成一个模块。另外如果调试技术够成熟,写完整个从器件选址应答过程再看也不迟,甚至连示波器都不需要。
【未完待续】
感谢大家强烈围观,特别还有各位老鸟的围观式鼓励!
上次说到,程序要分模块规划,这是把程序化大为小的基础,特别是对初学者,否则会感觉老虎吃天,无从下口。
这里补充说一下,模块和函数是不同的概念。模块是从程序功能的角度来划分的,函数是为了给程序减肥,把多个地方需要使用的部分写成函数。有时候一个模块刚好就是一个函数,比如lcd_display(*p),也可能一个函数什么模块也不是,单纯是为了减少代码量,比如
FUNC_PORTA_INPUT:
BSF STATUS,RP0
MOVLW 0XFF
MOVWF PORTA
BCF STATUS,RP0
RETURN
就没有任何模块的意义,只是因为PIC改端口方向很费事,多处需要调用,所以顺便写成函数。再顺便提醒一句——宏不省代码。
现在是初学者最关心的部分,如何调试程序了。我们推荐按照函数或者模块的方式来调试,有的初学者说,我的程序没写完,怎么调?我们说——从你能看到现象的地方开始调起。
比如有人写了个秒表,
char
time;
while(1)
{
func_time(time++); //计时
func_disp(time); //显示
}
现在程序不对了,怎么调呢?
while(1)
{
time=0;
func_disp(time); //显示
}
先把显示调对了,不就可以观察到程序是否按设计运行了么。这里做了一个假设,直接把time赋值为0,有时候一个固定值不够,还要多试两个,最极限情况,就是写个循环来显示全部可能值。
这一关过了,显示就基本正常了,如果程序还不对,那肯定是计时部分程序出了问题。
好了,这次先到这里。下次我们再来讲讲如何跟踪不听话的程序以及这些程序是为啥不听话的。
【未完待续】
我们先打断一下原来的计划,插播一条刚刚收到的消息。这几天问C语言怎么学的人好像有点多,我们可以这样说——会C语言和会写程序是不一样的,会写程序和会搞开发也是不一样的。
语言有很多种,汇编、C、java,甚至中文都是语言,语言是用来表达事情的,编程可以用自然语言么?可以啊!
如果(肚子饿了)
就:吃饭
否则(继续玩)
是不是感觉跟if语句很像,如果编译器可以认识这个语句,那这几行就可以当计算机语言了。学习C语言,实际是学习如何用计算机看得懂的语言来表达事情罢了。所以会C语言不见得会写程序,人家用汇编一样可以表达呢。
所以我们得到一个结论——学C语言不如学如何去发现问题解决问题,当你有了解决问题的能力,随便找一种语言(前提是好用,例如用汇编写数据库就是自虐)来描述就可以了。
最后再说下开发,搞电子,前期是爱好,后期是吃饭,说白了,用最古老的51
单片机,写个龌龊的程序,卖出去每年赚10万和用最ARM,操作系统,java写一个卖不掉的产品,你选哪边?
好了,希望各位纠结C语言语法或者在那里背51汇编指令的同学能够清醒了,偶从不记指令,要用的时候翻书就是了。
今天会再次更新,继续上次的内容。
2011-8-21
睡觉的同学可以醒了!今天我们讲实用技术——如何让不听话的程序听话以及为什么程序不听话。
前面已经说了,程序是搞电子技术里面最最最最简单的东西,这里再加上一条,所有的处理器(只要不是多内核技术的)在同一时间都只能处理1条指令。对这些程序来说,无论你是原生态的裸奔还是花哨的操作系统,程序只能在2个地方被执行——中断里面和中断外面,ARM的各种模式也可以看做中断。
再让我们来回忆一下,只要你按照自己的想法写了程序,写对了,入口条件满足了,程序就应该按照你的想法去执行,当他不听话的时候,只要检查这些条件是不是成立了就可以了。
比如
If(条件A满足;条件B满足;条件C不满足)
{
做D事件;
}
如果这个判断没有照你的预想执行,那么你可以这样检查
首先屏蔽判断语句
//If(条件A满足;条件B满足;条件C不满足)
//{
做D事件;
//}
如果这样写,D事件都没有被执行,那么OK,不关判断语句的事,你先得检查“做D事件”这个语句为啥没正确执行了,这个是基础。
【小结】找程序问题的时候应该从简到繁,排除干扰从核心找起。
现在“做D事件”已经正确了,来找if语句本身的问题
串口发送条件A;
串口发送条件B;
串口发送条件C;
If(条件A满足;条件B满足;条件C不满足)
{
做D事件;
}
这是一种跟踪调试的方法,把单片机串口连接大PC上,用串口助手看下发送出来的条件ABC就知道自己有没有犯啥低级错误了。
【小结】善于用输出通道对程序运行情况进行跟踪,一般来说程序执行不正常都是因为自己编写或者逻辑上犯了低级错误,跟踪一下,就可以发现了。
【注意】有的人(特别是某部分“高手”)会说这不就是仿真器的断点功能么,搞得那么复杂。为啥不用仿真器,我后面再说。初学者请跟着我的思路来做,我可以让你在5年以后在这些“仿真高手”面前笑傲江湖。
如果这里输出都是正确的,那怎么办呢?没关系,咱进来再发发看。
If(条件A满足;条件B满足;条件C不满足)
{
串口发送条件A;
串口发送条件B;
串口发送条件C;
做D事件;
}
如果在这里发现条件ABC出现了错误,那么就要关心下是不是中断的问题了。
【结论】在直线程序前后发生数据莫名其妙改变的,一般都是中断所致。
比如一个经常犯的错误是:
串口中断服务程序
{
从串口缓冲区取数据;
}
串口中断使能;
While(!串口收到数据中断标志);
从串口缓冲区取数据;
因为在串口中断服务程序里面已经读了缓冲区后,硬件已经清除了“串口收到数据中断标志”所以外面的while循环自然等不到这个标志置位了,表现就是程序死机。
程序debug内容太多了,这节课讲不完,先课间休息哈,下次再来。
2011-9-7
今天我们接着讲Debug
一般来说,程序的BUG可以分为两类:逻辑错误和低级错误。逻辑错误是指设计本来就存在错误,比如
if (楼主是火星人)
then
明显这是不可能发生的事,避免逻辑错误主要靠遵守基本的设计规范和画流程图检查。常见的基本设计规范包括
a.动手前先做好程序规划,包括需求分析和画流程图
b.中断内外都在使用的临界资源需注意保护
c.中断内使用了的资源要现场保护和现场恢复
d.PIC单片机retlw查表长度不能超过255
e.变量上电要赋值
f.模块只能操作自己需要的资源(端口、变量位)
g.模块化清晰
h.不要乱GOTO,不管是汇编还是C
至于低级错误,比如常见的
a.单片机没有上电(大家不要笑,真实发生,而且还不是菜鸟所为)
b.没有焊晶体
c.编辑器开的一个工程,编译器开的另一个工程(常见于把工程到处复制备份又不改名字的人)
d.熔丝没有设置
e.汇编程序最后一行没有回车
f.51单片机acall和lcall出现跳飞
这里举例说明下低级错误
比如toshiba 900L系列汇编,必须以回车符结束,结果我们有这样两个文件
[main文件]
.......
#include a.inc
funb:
nopa
nopa
nopa
.......
[a.inc文件]
func:
nopb
nopb
return ;这行没有敲回车符
结果编译器不认识这个return,在执行了nopb语句以后直接执行了nopa,给人的现象就是调用了func函数,却执行了一个无厘头相关的另一个函数funb。
于此类似的还有PIC单片机汇编的标号问题
lab:
nup
由于PIC的IDE环境MPLAB允许编号不带冒号,所以被敲错的nop指令将被认作标号,并仅在编译输出一个警告(注意不是错误),而我们大部分人是没有习惯去看编译器的警告信息的,这行指令就这样被放过了。
这些错误都可以通过习惯加以纠正,比如我习惯在汇编文件开头结尾加分隔线
;---------------------------------------------------- ;我是华丽分隔线!!!!
这样即使认掉一行,也是注释行,木有关系。
另外就是要注意看编译器输出的警告信息,不能看到hex文件生成就万事大吉。
我们这里再声明一次“一般来说,问题越奇怪,错误越低级”
先不要看原因,我们来看现象:我们有一个51的汇编程序,keil编译器,在程序临近编写完毕时,发现在程序开始,初始化中间加一个nop程序就要飞,加2-7个nop也要飞,加8个nop就没有飞,此时总中断是没有开的。单片机外设里没有能把程序弄飞的东东。
奇怪吧!
;------------------我是华丽的隐藏分界线,斑竹木删偶哟,怕怕-----------------
原因:本来51汇编是没有call这条指令的,但是keil为了方便懒人,加了这条伪指令。在编译的时候,按理应该自动匹配为acall和lcall,但是这个keil偏偏没有匹配,程序总长度又恰好临界于acall的边界,所以一个nop就可以“让程序飞”
我们是这样找到这个问题的:
首先总中断没有开,那么没有其他程序可以干扰这里;其次外设里没有能够弄死单片机的;那么能飞的情况就剩下乱飞乱跳这一项了,51的查表不会弄死,而且初始化也不需要查表,所以怀疑到call上,查看了指令表后发现这是伪指令(这点确实疏忽了,认为所有单片机都有call指令,如果及早发现,就没有这个问题出现了),看反汇编,发现确实没有正确匹配acall和lcall。
有同学举手:我们不会看反汇编
答:程序校验会不会?先用call编译了以后烧进去,再把一个绝对应该lcall的call改成lcall,编译,校验,看一样不一样就OK啦。
我们遇到的最变态的问题是Toshiba 900L上出的,现象是for循环跑死,出不来。
for (i=0;i<100;i++)
{
_asm_func();
}
_asm_func();是一个用汇编编写的函数,猜猜死机的原因是啥来着?900L编译器为了加速程序运行,将自动分配寄存器变量并且不打招呼,i变量因为在for内,被判断为“频繁使用”,故分配到了xiz(900L内核的一个32位寄存器)内,而_asm_func();函数内,正好用了这个寄存器,所以for循环的退出条件始终得不到满足,自然死机咯。
这个问题属于比较难找,也比较变态的,最后是通过看反汇编和测试for循环分配寄存器变量的规则找出来的,严格说来属于编译器设计的不完善。
好了,今天的课讲完了,给同学们留个课后作业:单片机程序错误从时间和空间上看有没有规律,如何运用这些规律来找程序错误呢?
【未完待续】
11-9-21
我们现在先来看看上次的课后作业有什么效果。为什么说单片机程序错误从时间和空间上看又规律呢?因为从本质上看,单片机只有一个处理器(多核的暂时木讨论),程序始终要得到CPU才能被执行,这样程序本质上也就分为2类——顺序执行(按书写者的安排运行),和另一类——外部条件触发,例如中断触发或者使用操作系统的模式,模块之间由事件驱动。
应该说顺序执行的程序是最容易找到问题的,因为程序的来龙去脉很清楚,只要在入口和出口进行跟踪就可以找到“什么条件没有正确”,对于有中断或者操作系统(严格来说是多任务系统)来说就没那么幸运了,因为中断部分的执行事件可能完全和设计者所想的相反,就像上面中断标志被抢先清除的例子,但看等待中断标志这一段程序是完全没有错得,除非Debuger能“回过神来”,想到还有个“看不见”的中断在跑,否则打死也想不出来谁清了中断标志。
了解了程序这两个特性以后,就可以用他们来帮助我们Debug,当遇到程序出现问题无从找起时,可以试探性关闭中断,例如
disable IE
func_err
{
}
enable IE
其中开关IE的语句是后面加上去的测试语句,怀疑func_err的执行条件被中断破坏,那么就禁止中断在这里面发生,如果条件依然被破坏,那么绝大部分可能性与中断无关了。
再比如前面举的51单片机汇编编译器的问题,我们在无厘头的情况下可以去找规律比如
init
main
这样执行是正确的
init
thing_a
main
就出现错误,那么索性把thing_a换成nop,如果这样还出现错误,那么可能是跟时间相关,就是main被执行的时间(因为有的中断还是可能在特定时间发生的,假如因为单片机复位的原因,每次都是在相同的地方执行中断,还是可能造成这种现象哟)
那么我们开始加2个NOP,3个NOP,4个NOP,看程序是否正确,当我们加到8个NOP的时候,发现程序对了,然而这很无厘头,因为在main之前,初始化应该是想做多久就做多久的,所以才有了后面去找编译器问题的想法。
好了,我们总结一下今天的内容:程序的执行有顺序和中断两种,包括OS的并发进程在内,找程序BUG的重要步骤之一是确定程序的执行条件是被前面的顺序流程破坏,还是被中断破坏。
3