完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
|
|
|
|
狂顶。。。。。。。{:1:}{:1:}{:1:}{:1:}{:1:}{:1:}{:1:}{:1:}{:1:}{:1:}{:1:}
|
|
|
|
|
|
{:12:}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
本帖最后由 jianhong_wu 于 2013-2-13 17:54 编辑
第十节:鸿哥多文件编程套路的”十二招式” (1) 开场白: 吴坚鸿虽然号称深圳同龄人中最牛的单片机工程师,但并不意味着我真的什么都已经做到最好了。比如,在上个月之前的六年时间里,我写的程序都是一个工程仅仅包含一个源文件,当代码很庞大的时候,我总是要依靠”查找”的功能跳到我想去的地方,不是很方面。其实早就听说C语言可以分多个文件来连接编译的,我曾经也尝试过,但是每次都因为重复声明或者重复定义而报错,编译不过去,后来我只好一个工程只用一个源文件。 而上个月,有一个项目要用到NEC的单片机,虽然NEC的单片机我以前从来没有玩过,但是凭着“只要会51单片机的就肯定会所有单片机”的信念,我把这项目接了下来,并且告诉客户,我最多用两天时间就可以上手。NEC的开发环境比一般的单片机多了一个可以自动生成硬件初始化代码的软件Applilet2,而生成的这些代码它是分成了好多个文件,比如定时器一个,看门狗一个,AD一个,串口通讯一个等等,逼得我不得不重新去研究多文件的编程技巧,经过一番研究与上机练习之后,我终于摸出了它的规律,解决了我多年来的一个心病。从上个星期开始,我又接了一个PIC的单片机项目,我利用多文件的编程方式顺利完成了。多文件的编程方式确实有它的优越性,它最大的好处就是比传统一个源文件的方式多增加了目录的功能,当你要查找的时候,只要点击其中你想要的文件即可。 现在我把我的多文件编程技巧分享给大家,我不讲原理,因为原理讲太多会乱。我只讲套路,大家不需要知道原理,只要依照鸿哥总结的这个套路来操作就行。最后我会把上回第九节的源代码改成多文件的编程方式展示给大家。 (2) 鸿哥多文件编程套路: 第一招:所有的文件必须成双成对出现。 用了XXX.c这个源文件文件,就必须要有XXX.h这个头文件跟它配对。比如main.c和main.h, interrupt.c和interrupt.h, initial.c和initial.h, run.c和run.h 等等。 第二招:函数必须包含函数定义与函数声明这两部分,不能仅有其一。 比如: Void run(); //此乃函数声明 Void run() //此乃函数定义 { Unsigned char temp; Temp=0; } 第三招:全局变量必须要定义。但全局变量不一定要声明,要看具体情况。 (a)当全局变量已经在另外的文件已经定义过了之后,而又要想在当前文件调用它,那么必须先声明此全局变量才能用。 比如: Void run(); //此乃函数声明 Extern unsigned char Run_step; //此乃全局变量声明,绝对不能在此赋初始值,而//且必须有Extern这个关键字,因为在别的文件已经定义过了 Void run() //此乃函数定义 { Unsigned char temp; Temp=0; Run_step=0x55; //调用全局变量 } (b)当全局变量没有在另外的文件定义过,而又要想在当前文件调用它,则必须先定义再使用,而这时不用声明全局变量,只要定义就够了。 比如: Void run(); //此乃函数声明 unsigned char key_sec=0; //此乃全局变量定义,因为在别的文件还没有定义////过,可以给它赋初始值 Void run() //此乃函数定义 { Unsigned char temp; Temp=0; key_sec=3; //调用全局变量 } 第四招:函数与全局变量的定义在整个工程中,不管有多少个文件,只能在一个地方出现一次,不能重复在两个不同的文件中出现。 第五招:全局变量的声明在整个工程中,可以重复在不同的多个头文件中出现。 第六招:函数与全局变量的所有定义必须放在扩展名为XXX.c的对应源文件中。 第七招:函数与全局变量的所有声明,以及类似”#define yy 10”这种宏定义,都必须放在扩展名为XXX.h的对应头文件中。 第八招:全局变量的所有声明前缀必须有extern这个关键字,而函数的所有声明都不用extern这个关键字。 第九招:全局变量的声明与全局变量的定义在外形上非常像,很容易出错。它们的区别是: (a) 全局变量的声明不能赋初始值,而全局变量的定义可以赋初始值。 (b) 全局变量的声明前缀必须有extern这个关键字,全局变量的定义不用extern这个关键字。 (c) 全局变量的声明必须放在.h的这个头文件中,而全局变量的定义必须放在.c的源文件中。 第十招:扩展名为XXX.h的对应头文件中,里面所有的内容都必须被 “#ifndef _AAA _ #define _AAA_”和 “#endif”这种语句包围着。 比如: (a)在run.h的头文件中 #ifndef _RUN_ #define _RUN_ #define beep_dr LATA1 Void run(); //此乃函数声明 Extern unsigned char Run_step; //此乃全局变量声明,绝对不能在此赋初始值,而//且必须有Extern这个关键字,因为在别的文件已经定义过了 //注意:run.h与run.c在我这里是看做”同一个文件”,run.c和delay.c才是属于不同的文//件 #endif //------------------------------------run.h头文件的结束分割线-------------------- (b)在run.c的源文件中 #include #include "run.h" //第二行必须的 Unsigned char delay_cnt=0; //全局变量定义,因为在别的文件还没定义过 Void run() //此乃函数定义 { Unsigned char temp; Temp=0; Run_step=0x55; //调用在别的源文件中已经定义过的全局变量 beep_dr=1; delay_cnt++;//调用在本源文件中已经定义过的全局变量 } 第十一招:扩展名为XXX.c的对应源文件中,第一行必须有“#include 比如,目前有两种文件run(run.h和run.c看做同一个文件)与delay(delay.h和delay.c看做同一个文件) (a)在delay.h头文件中: #ifndef _DELAY_ #define _DELAY_ #define beep_dr LATA1 void delay(unsigned int t); //函数声明 #endif (b)在delay.c源文件中: #include #include "delay.h" Unsigned char Run_step=2; //全局变量定义 void delay(unsigned int t) { unsigned int i,j; for(i=0;i } (c)在run.h的头文件中 #ifndef _RUN_ #define _RUN_ Void run(); //此乃函数声明 Extern unsigned char Run_step; //此乃全局变量声明,绝对不能在此赋初始值,而//且必须有Extern这个关键字,因为在别的文件已经定义过了 //注意:run.h与run.c在我这里是看做”同一个文件”,run.c和delay.c才是属于不同的文//件 #endif //------------------------------------run.h头文件的结束分割线-------------------- (d)在run.c的源文件中 #include #include "run.h" //第二行必须的 #include "delay.h" //第三行。由于在此源文件中调用了别的文件的函数与//beep_dr这个宏定义,所以必须在此包含人家的头文件 Unsigned char delay_cnt=0; //全局变量定义,因为在别的文件还没定义过 Void run() //此乃函数定义 { Unsigned char temp; Temp=0; Run_step=0x55; //调用在别的源文件中已经定义过的全局变量 beep_dr=1; //调用在别的文件里的宏定义常量,必须在第三行包含人家对////应的头文件 delay_cnt++;//调用在本源文件中已经定义过的全局变量 delay(10); //调用了别的文件里定义的函数,必须在第三行包含人家对应的//头文件 } 第十二招:对于函数与全局变量的声明,编译器都不分配内存空间。对于函数与全局变量的定义,编译器都分配内存空间。 (3)功能需求: 无论是单片机还是上位机,最好在固定协议前多发送一个填充的无效字节0x00,因为硬件原因,第一个字节往往容易丢失。 通讯协议:XX YY EB 00 55 其中后三位 EB 00 55就是我所说的数据尾,它的有效数据XX YY在数据尾的前面。 任意时刻,从电脑“串口调试助手”上位机收到的一堆数据中,只要此数据中包含关键字EB 00 55 ,并且此关键字前面两个字节的数据XX YY 分别为01 02,那么就往上位机发送“eb 00 aa”表示确认,同时蜂鸣器叫一声。否则,往上位机发送“eb 00 55”表示出错。 (4)硬件原理: 在电脑的串口处加一个232转485的通讯模块。然后把单片机串口通讯的那两个引脚经过一个MAX485芯片,直接与电脑处的232转485的通讯模块相连接,多增加一根单片机的IO口,用此IO连接MAX485的第2和第3引脚,实现流控的功能。当此IO处于低电平时为接收数据的状态,当此IO口处于高电平时为发送数据的状态。 (5)源码适合的单片机:PIC18f4520,晶振为22.1184MHz,波特率115200 (6)把第九节的源代码改成多文件的编程方式,源代码讲解如下: (a)在main.h文件中的内容: #ifndef _MAIN_ #define _MAIN_ void main(); #endif //---------------------------------------------- (b)在main.c文件中的内容: #include #include "main.h" #include "usart.h" #include "initial.h" void main() //注意,此处必须要有void,与main.h的函数声明一致 { initial(); while(1) { CLRWDT(); //喂看门狗,大家不用过度关注此行 usart_service(); //串口通讯服务 } } //--------------------------- (c)在initial.h文件中的内容: #ifndef _INITIAL_ #define _INITIAL_ void initial(); #endif //----------------------------------- (d)在initial.c文件中的内容: #include #include "initial.h" #include "usart.h" void initial() { ADCON0=0x00; ADCON1=0x0f; //全部为数字信号 ADCON2=0xa1; //右对齐 RBPU=0; //上拉电阻 SSPEN=0; //决定RA5不作为串口 TRISA2=0; //蜂鸣器输出 TRISC5=0; //485通讯的流控输出 BRG16=0; //设置串口通信寄存器 BRGH=0; SPBRGH=0x00; SPBRG=0x02; //22.1184MHz晶振,115200波特率 SYNC=0; SPEN=1; TX9=0; TXEN=1; TXIF=1; RX9=0; CREN=1; RCIE=1; PEIE=1; GIE=1; T1CON=0x24; //定时器中断配置 TMR1H=0xFE; TMR1L=0xEF; TMR1IF=0; TMR1IE=1; TMR1ON=1; TMR1IE=1; //补充说明,以上的内容为寄存器配置,每种不同的单片机会有点差异, //大家不用过度关注以上寄存器的配置,只要知道有这么一回事即可 beep_dr=0; //关蜂鸣器,上电初始化IO rede_dr=0; //流控IO,从机处于接收数据的状态 } //------------------------------------ (e)在delay.h文件中的内容: #ifndef _DELAY_ #define _DELAY_ void Delay11(unsigned int MS); //函数声明 #endif //-------------------------------- (f)在delay.c文件中的内容: #include #include "delay.h" //延时函数定义 void Delay11(unsigned int MS) { unsigned char us,usn; while(MS!=0) //for 12M { CLRWDT(); usn = 2; while(usn!=0) { CLRWDT(); us=0xf5; while (us!=0){us--;}; usn--; } MS--; } } //----------------------------------------- (g)在usart.h文件中的内容: #ifndef _USART_ #define _USART_ //补充说明:吴坚鸿程序风格是这样的,凡是输出IO后缀都是_dr,凡是输入的//IO后缀都//是_sr #define beep_dr LATA2 //蜂鸣器输出 #define rede_dr LATC5 //485通讯的流控 //补充说明:吴坚鸿程序风格是这样的,凡是做延时计数阀值的常量 //前缀都用cnt_表示。 #define cnt_voice_time 150 //蜂鸣器响的声音长短的延时阀值 #define cnt_send 300 //确保接收缓冲区没有继续接收数据,是变量 //send_cnt的溢出阀值 void usart_service(); //串口通讯服务程序,放在main函数里 unsigned char asy_recieve(); //把串口缓冲区的数据一个个提取出来 void eusart_send(unsigned char t_data); //串口发送一个字节的数据 void Buf_clear(); //把余下的缓冲区清零 #endif //---------------------------------------------- (h)在usart.c文件中的内容: #include #include "usart.h" #include "delay.h" //补充说明:吴坚鸿程序风格是这样的,凡是计数器延时的变量 //后缀都用_cnt表示。 unsigned int voice_time_cnt; //蜂鸣器响的声音长短的计数延时 unsigned int send_cnt=0; //一串数据从上位机发过来的时候,他们每个字节之间//的延时间隔很短,如果他们的延时间隔一旦超过了这个send_cnt变量的延时,那么就////认为他们的一串数据已经发送完毕 //补充说明:吴坚鸿程序风格是这样的,凡是涉及统计数量的变量 //后缀都用_total表示。 unsigned int RCREG_total; //统计串口缓冲区已经收了多少个数据 unsigned int RCREG_read_total; //统计已经从串口缓冲区读出了多少个数据 //补充说明:吴坚鸿程序风格是这样的,凡是用来更新的标识变量,比如液晶刷屏,或者有新接收的串口数据更新等等,后缀一律用_update表示 unsigned char send_update=0; //一旦有数据从上位机发送过来,就会引发串口接收中////断,在串口中断里,我把send_update=1表示目前正在接收数据,警告单片机先不要//猴急,等串口中断把所有从上位机连续发送过来的一堆数据接收完,再处理。那么什么///时候才知道发送的数据已经发送完毕了呢?用send_cnt识别。因为在串口中断里,我///每次都会把send_cnt=0,而在main函数里,一旦发现send_update==1,send_cnt就//会开始自加,当它超过某个数值的时候,就会自动把send_update=0,表示目前已经没//有数据发送了。而如果有数据不断从上位机传来,send_cnt永远也不会超过某个数值,//因为每个中断它都被清零,这个原理跟看门口狗喂狗的原理很像。 //补充说明:吴坚鸿程序风格是这样的,凡是用来接收数据的缓冲区数组后缀都用_buf表//示 unsigned char RCREG_buf[50]; //串口接收缓冲区,读者可以根据实际项目设置大小 unsigned char RCREG_buf_temp[50]; //临时处理串口数据的缓冲区,可以不用那么大 //补充说明:吴坚鸿程序风格是这样的,凡是自锁变量名, 后缀都用_lock表示。 unsigned char send_lock=0; //补充说明:吴坚鸿程序风格是这样的,凡是在main函数中用的中间变量,前缀m_,后//缀用_char或者_int表示类型 unsigned int m_int; //中间变量,只要是用在main函数里,谁都可以重复用。 void usart_service() //串口服务程序,在main函数里 { if(send_update==1) //说明目前串口正在接收数据,不要读缓冲区数据 { send_lock=1; //开自锁标志 ++send_cnt; //只要有数据接收,send_cnt每次都被串口中断清零 if(send_cnt>cnt_send) //延时一段时间,确认缓冲区没有继续接受数据 { send_cnt=0; send_update=0; } } else //说明当前没有继续接收数据了 { if(send_lock==1) //在数据已经接收完毕,并且还没有处理过数据的情况下 { send_lock=0; //处理一次就锁起来,不用每次都进来,除非有新接收的数据 m_int=0; //中间变量清零,用来统计处理了多少个刚刚接收的数据 while(RCREG_read_total CLRWDT(); RCREG_buf_temp[m_int]=asy_recieve(); if(m_int>=4) //说明接收了5个数据以上(2个有效数据加3个数据尾) { if(RCREG_buf_temp[m_int-2]==0xeb&& RCREG_buf_temp[m_int-1]==0x00&& RCREG_buf_temp[m_int]==0x55) //数据尾”eb 00 55”判断 { if(RCREG_buf_temp[m_int-4]==0x01&&RCREG_buf_temp[m_int-3]==0x02) //有效数据是否为01 02的判断 { eusart_send(0x00); //串口发送一个填充的无效字节0x00,避免第一个字节丢失而引起的问题 eusart_send(0xeb); //串口发送应答的数据 eusart_send(0x00); //串口发送应答的数据 eusart_send(0xaa); //串口发送应答的数据 voice_time_cnt= cnt_voice_time; //蜂鸣器响“滴”一声就停 } else { eusart_send(0x00); //串口发送一个填充的无效字节0x00,避免第一个字节丢失而引起的问题 eusart_send(0xeb); //串口发送应答的数据 eusart_send(0x00); //串口发送应答的数据 eusart_send(0x55); //串口发送应答的数据 } break; //跳出循环 } } m_int++; //中间变量加1, } Buf_clear(); //把余下的缓冲区清零,方便下一堆数据接收与处理 } } } void Buf_clear() //把余下的缓冲区清零 { unsigned char buf_clear_temp; while(RCREG_read_total CLRWDT(); buf_clear_temp =asy_recieve(); } } unsigned char asy_recieve() //把串口缓冲区的数据一个个提取出来 { unsigned char RCREG_dt; ++RCREG_read_total; //已经读出了多少个数据 RCREG_dt=RCREG_buf[RCREG_read_total -1]; if(RCREG_read_total >=RCREG_total) //只要把全部数据都读完,马上把缓冲区清零 { RCREG_read_total =0; RCREG_total=0; } return RCREG_dt; } void eusart_send(unsigned char t_data) //串口发送一个字节的数据 { unsigned int error_delay; rede_dr=1; //流控IO,切换至发送数据的状态,准备发送数据, //以下几个空延时是必要的。 asm("nop"); asm("nop"); asm("nop"); asm("nop"); TXREG=t_data; //发送数据 error_delay=0;//等待把数据发送完毕 while(1) { CLRWDT(); if(TXIF==1) //等待把数据发送完毕 { break; } else { ++error_delay; if(error_delay>200) //超时也要退出,不能死等 { break; } } } Delay11(1); //此处最玄机,要特别注意。每发送完一个字节,由于不同的项目,这//里的延时间隔都不一样,读者根据实际情况来改。这里最容易出问题,必须要延时,尤其是连续发送一堆数据的时候。读者也可以改成计数延时的方式。 rede_dr=0; //流控IO,发送完数据之后,务必马上把从机切换回接收数据的状态,把总//线释放 } //-------------------------------------------------------------------------------------- (i)在interrupt.h文件中的内容: #ifndef _INTERRUPT_ #define _INTERRUPT_ extern unsigned int voice_time_cnt; //声明用到的外部变量 extern unsigned int RCREG_total; extern unsigned char send_update; extern unsigned int send_cnt; extern unsigned char RCREG_buf[50]; #endif //--------------------------------------------------------- (j)在interrupt.c文件中的内容: #include #include "interrupt.h" #include "usart.h" //用到usart的beep_dr //中断 void interrupt timer1rbint(void) { if(RCIE==1&&RCIF==1) //串口中断,一次只能接受一个字节 { RCIE=0; RCIF=0; ++RCREG_total; //以下代码是鸿哥的在所有串口项目中用到的标准代码 if(RCREG_total>50) //超过缓冲区 { RCREG_total=50; } RCREG_buf[RCREG_total-1]=RCREG; //依次把上位机来的数据存入数组缓冲区 send_update=1; //通知单片机目前正在接收数据 send_cnt=0; //及时喂狗,虽然main函数那边不断在累加,但是只要串口的数//据还没发送完毕,那么它永远也长不大,因为每个中断都被清零,很可怜。 RCIE=1; } if(TMR1IE==1&&TMR1IF==1) //定时中断 { TMR1IF=0; //定时中断标志位关闭 TMR1ON=0; //定时中断开关关闭 if(voice_time_cnt) //控制蜂鸣器声音的长短 { beep_dr=1; //蜂鸣器响 --voice_time_cnt; //蜂鸣器响的声音长短的计数延时 } else { asm("nop"); //添加此行空指令为了使else的内容跟if的内容对称,意义////不大 beep_dr=0; //蜂鸣器停止 } TMR1H=0xFe; //重新设置定时时间间隔 TMR1L=0x00; TMR1ON=1; //定时中断开关打开 } } (7)小结: 在开发环境的source files里添加所有.c的源文件,在header files里添加所有.h的头文件。养成用多文件编程的习惯,程序会更加清晰,精彩。 (未完待续,下节更精彩,不要走开哦) |
|
|
|
|
|
只有小组成员才能发言,加入小组>>
求解外围电路实现的是4脚给持续低电平复位并正常工作,高电平不工作的原因
2076 浏览 1 评论
3486 浏览 3 评论
PIC1946程序有一个变量在运行过程中恢复初始值其他变量保持不变
2329 浏览 2 评论
2754 浏览 0 评论
PIC16F1825的RC5引脚,在主程序中操作无效,在中断中可以改变是为什么?
4012 浏览 5 评论
956浏览 0评论
用XC8编译PIC18F25K80时提示下面Error,求怎么解决这个问题
6343浏览 0评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-11-23 16:27 , Processed in 0.979436 second(s), Total 91, Slave 81 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号