完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
顶起来
|
|
|
|
|
|
本帖最后由 jianhong_wu 于 2013-7-10 11:01 编辑
第六节:单串口通讯之如何接住别人丢过来的一堆数据 (1) 开场白: 实战中,串口通讯不可能一次通讯只发送或接受一个字节,大部分的项目都是一次发送或者接受一堆的数据。我们还要在这一堆数据里提取关键字,提取有用的数据。这节我将要介绍我最得意的,最可靠的串口通讯的基本编程结构,学会了这招,用在什么单片机上,用在什么串口通讯项目上都管用。串口通讯表面看起来简单,实际蕴含着玄机,但是不用怕,我会把这些玄机都公布出来。 (2) 功能需求: 任意时刻,从电脑“串口调试助手”上位机收到的一堆数据中,只要中间包含了十六进制的“Eb 00 55”,那么就往上位机发送“eb 00 aa”表示确认,同时蜂鸣器叫一声。 (3) 硬件原理: 把单片机串口通讯的那两个引脚经过一个MAX3232之后直接跟电脑的9针串口通讯。我发现很多朋友会选MAX232这个芯片,而我本人更加推荐用MAX3232。因为MAX232只支持5V,不是宽压的,而MAX3232不但支持5V,还支持3V。每个人的记忆力都很宝贵,用232串口我只选MAX3232,不管它是用5V工作还是3V工作。就像74系列的芯片,我的心中只有你(74HC)没有它(74LS),一样的道理,74HC是宽压,74LS不是宽压。 (4)源码适合的单片机:PIC18f4520,晶振为22.1184MHz,波特率115200 (5)源代码讲解如下: #include //补充说明:吴坚鸿程序风格是这样的,凡是输出IO后缀都是_dr,凡是输入的//IO后缀都//是_sr #define beep_dr LATA2 //蜂鸣器输出 //补充说明:吴坚鸿程序风格是这样的,凡是做延时计数阀值的常量 //前缀都用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(); //把余下的缓冲区清零 void Delay11(unsigned int MS); //延时函数 //补充说明:吴坚鸿程序风格是这样的,凡是计数器延时的变量 //后缀都用_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() { ADCON0=0x00; ADCON1=0x0f; //全部为数字信号 ADCON2=0xa1; //右对齐 RBPU=0; //上拉电阻 SSPEN=0; //决定RA5不作为串口 TRISA2=0; //蜂鸣器输出 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 while(1) { CLRWDT(); //喂看门狗,大家不用过度关注此行 usart_service(); //串口通讯服务 } } //中断 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; //定时中断开关打开 } } 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; //处理一次就锁起来,不用每次都进来,除非有新接收的数据 while(RCREG_read_total CLRWDT(); RCREG_buf_temp[0]= RCREG_buf_temp[1]; //数据移动,方便截取关键字 RCREG_buf_temp[1]= RCREG_buf_temp[2]; RCREG_buf_temp[2]=asy_recieve(); if(RCREG_buf_temp[0]==0xeb&& RCREG_buf_temp[1]==0x00&& RCREG_buf_temp[2]==0x55) //数据头”eb 00 55”判断 { RCREG_buf_temp[0]=0; //把临时处理数据清零,方便下次接收 RCREG_buf_temp[1]=0; RCREG_buf_temp[2]=0; eusart_send(0x00); //串口多发送一个填充的无效字节,因为由于硬件的原因,第一个字节很容易丢失 eusart_send(0xeb); //串口发送应答的数据 eusart_send(0x00); //串口发送应答的数据 eusart_send(0xaa); //串口发送应答的数据 voice_time_cnt= cnt_voice_time; //蜂鸣器响“滴”一声就停 break; //退出循环 } } Buf_clear(); //把余下的缓冲区的下标清零,方便下一堆数据接收与处理 } } } Void Buf_clear() //把余下的缓冲区的下标清零 { RCREG_read_total =0; RCREG_total=0; } 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; TXREG=t_data; //发送数据 error_delay=0;//等待把数据发送完毕 while(1) //这里也可以省略,直接用延时替代。延时有两种,一种是delay的死延时,一种是计数延时方式。 CLRWDT(); if(TXIF==1) //等待把数据发送完毕 { break; } Else { ++error_delay; if(error_delay>200) //超时也要退出,不能死等 { break; } } } Delay11(1); //此处最玄机,要特别注意。每发送完一个字节,由于不同的项目,这里的延时间隔都不一样,读者根据实际情况来改。这里最容易出问题,必须要延时,尤其是连续发送一堆数据的时候。在一些实时要求很高的场合,读者也可以把 这个死延时改成计数延时的方式,有兴趣的自己动脑筋改。一般的项目这样子已经够用了。 } //延时函数 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--; } } (6)小结: (a)发送一堆数据的时候,要特别注意每个字节之间的延时间隔。 (b)必须等待上位机的数据全部发送完毕,再去处理缓冲区的数据。 (c)无论是上位机还是单片机,在发送数据前最好多发送一个填充字节0x00,避免由硬件问题而引起第1个字节丢失。 (d)对接串口线的时候,要特别小心,尤其是接9针串口线的时候,经常容易接反出错。 (e)如果遇到很莫名其妙的问题,有可能232的转换芯片已经烧坏了,这种芯片很脆弱。 (未完待续) |
|
|
|
|
|
鸿哥又更新啦~顶
|
|
|
|
|
|
本帖最后由 zbfisthebest 于 2012-12-2 21:53 编辑
鸿哥,最近我在捣鼓BOOTLOADER,现有一事相求 ATmega64的单片机,它的页地址是PC[14:7] 但是问题来了m_PCPAGE = 0时可以成功写入起始页的FLASH即第0页,但是m_PCPAGE = 0x80时却不能成功写入第1页,而m_PCPAGE = 0x180时却能写入第1页,而m_PCPAGE = 0x280与m_PCPAGE = 0x380同理。难道ATMEGA64L的单片机,相邻页地址之间是“加二”才能找到下一页?明明应该是“加一”呀 以下是功能程序: loadpage();//将值取出,存入RAM以便写入FLASH //m_PCPAGE = (m_page - 1) << Z_ADD_SHIFT; //PCPAGE即PC[14:7], m_PCPAGE = 0;//当前要写的页地址,即PC[14:7] m_PCPAGE_H = m_PCPAGE >> 8; //Z指针指向,此两位即为PC m_PCPAGE_L = m_PCPAGE; loadflash(); //将值写入FLASH的程序,功能正常,可以正确写入一页FLASH void loadpage(void) { u16 i = 0; for (i = 0; i < PRO_PAGE_SIZE; i++)m_buf = simbuf; m_page += 1; } //将SRAM中的程序载入flash void loadflash(void) { //页擦除first erase page u8 low = 0, hi = 0; u16 i = 0; m_SPM = BIT(PGERS) | BIT(SPMEN); asm ("lds R31, %m_PCPAGE_H"); asm ("lds R30, %m_PCPAGE_L"); do_SPM(); //将数据从RAM 转移到Flash 页缓冲区second write page buffer for (i = 0; i < PRO_PAGE_SIZE; i += 2) { low = m_buf; hi = m_buf[i + 1]; m_SPM = BIT(SPMEN); asm ("mov R0, %low"); asm ("mov R1, %hi"); asm ("mov R30, %i"); asm ("ldi R31, 0"); do_SPM(); } //execute Page Write last write page m_SPM = BIT(PGWRT) | BIT(SPMEN); asm ("lds R31, %m_PCPAGE_H"); asm ("lds R30, %m_PCPAGE_L"); do_SPM(); //重新使能RWW 区enable RWW section,使能之后可以读取FLASH中的RWW区数据进行校验 m_SPM = BIT(RWWSRE) | BIT(SPMEN); do_SPM(); } void do_SPM(void) { while (SPMCSR & BIT(SPMEN)) ;//手册中将SPMCR 改为SPMCSR SPMCSR = m_SPM; _SPM(); } |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ATmega64L的DATASHEET上是说一页是512个字节。 你这么一说也倒提醒了我…… 64单片机BOOT LOADER可以写入的地址为0-0x7FFF 页地址的寄存器有8位,也即可以寻址0x100个页,每页有0x100个字节,这样的话可以写入总长0x10000个字节的FLASH,但是可以写入的地址为0-0x7FFF只有0x8000个,0x10000/2=0x8000 所以页地址每“加二”才能寻址到下一页,这样是正确的。。。 不知道说啥好,到底是我看DATASHEET没仔细呢,还是DATASHEET里面一些东西也不会完全提到。。。 |
|
|
|
|
|
|
|
|
|
|
|
|
|
只有小组成员才能发言,加入小组>>
求解外围电路实现的是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 12:58 , Processed in 0.860782 second(s), Total 87, Slave 78 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号