|
EDA365欢迎您!
您需要 登录 才可以下载或查看,没有帐号?注册
x
(1)开场白:1 ~; d$ ^, T6 b3 e
$ q1 Y/ v! |+ I3 r& U
实战中,串口通讯不可能一次通讯只发送或接受一个字节,大部分的项目都是一次发送或者接受一堆的数据。我们还要在这一堆数据里提取关键字,提取有用的数 据。这节我将要介绍我最得意的,最可靠的串口通讯的基本编程结构,学会了这招,用在什么单片机上,用在什么串口通讯项目上都管用。串口通讯表面看起来简 单,实际蕴含着玄机,但是不用怕,我会把这些玄机都公布出来。- d' ]% v) i% l0 ^
(2)功能需求:. v I" t4 t9 ?+ S3 @8 ^
任意时刻,从电脑“串口调试助手”上位机收到的一堆数据中,只要中间包含了十六进制的“Eb 00 55”,那么就往上位机发送“eb 00 aa”表示确认,同时蜂鸣器叫一声。
* d2 s5 w9 O4 m9 Y0 I (3)硬件原理:6 j9 s; z) d% A i' f
把单片机串口通讯的那两个引脚经过一个MAX3232之后直接跟电脑的9针串口通讯。我发现很多朋友会选MAX232这个芯片,而我本人更加推荐用 MAX3232。因为MAX232只支持5V,不是宽压的,而MAX3232不但支持5V,还支持3V。每个人的记忆力都很宝贵,用232串口我只选 MAX3232,不管它是用5V工作还是3V工作。就像74系列的芯片,我的心中只有你(74HC)没有它(74LS),一样的道理,74HC是宽 压,74LS不是宽压。" O! H9 e8 C) W+ k5 w
(4)源码适合的单片机 IC18f4520,晶振为22.1184MHz,波特率115200$ H" P% a- f0 N5 F8 {; q
(5)源代码讲解如下:, r& U( m2 @0 k9 j, Q
#include<pic18.h> //包含芯片相关头文件- a" v. O8 {/ e; J! I/ @ c) s( m
8 t3 K; \8 r6 M* D9 j. f- E, @
//补充说明:吴坚鸿程序风格是这样的,凡是输出IO后缀都是_dr,凡是输入的//IO后缀都//是_sr
j: y4 j* @ P3 V #define beep_dr LATA2 //蜂鸣器输出
7 k1 c4 S9 l& U8 u6 `2 P, E y
% Z6 N& _; e% F! b //补充说明:吴坚鸿程序风格是这样的,凡是做延时计数阀值的常量
K6 o" S1 _1 Q* `" C P //前缀都用cnt_表示。6 ~: i$ Q9 m q% |3 _
#define cnt_voice_time 150 //蜂鸣器响的声音长短的延时阀值
, j4 G8 s: V8 N# V #define cnt_send 300 //确保接收缓冲区没有继续接收数据,是变量
* ^. I, O( W% y' K! w, I //send_cnt的溢出阀值2 ^0 g: W) {3 Q: F+ X( F- n. P
6 @% P8 A5 H1 l! ~, j
Void usart_service(); //串口通讯服务程序,放在main函数里
) U4 v7 @+ S' l unsigned char asy_recieve(); //把串口缓冲区的数据一个个提取出来
& a$ C8 L3 y& S. n5 i0 } void eusart_send(unsigned char t_data); //串口发送一个字节的数据
* `2 B' k) O9 A" P5 x7 ?& Y1 R Void Buf_clear(); //把余下的缓冲区清零
$ W6 V J* s; L3 ] t void Delay11(unsigned int MS); //延时函数9 O& R$ p- r9 B: z" U' f
2 v9 {( k( T9 r2 a/ R( @( \/ t, S6 z; o/ W# S7 `; L6 R
//补充说明:吴坚鸿程序风格是这样的,凡是计数器延时的变量- \, H- Q/ D u: Y4 H8 U
//后缀都用_cnt表示。
' C9 }2 T- n H1 f/ l" c unsigned int voice_time_cnt; //蜂鸣器响的声音长短的计数延时
4 D/ A* D! q3 U" u* z unsigned int send_cnt=0; //一串数据从上位机发过来的时候,他们每个字节之间//的延时间隔很短,如果他们的延时间隔一旦超过了这个send_cnt变量的延时,那么就////认为他们的一串数据已经发送完毕) f5 U6 N. E, \+ z: d
2 Z* w+ }) J3 R3 C* {5 _, o //补充说明:吴坚鸿程序风格是这样的,凡是涉及统计数量的变量
$ f0 H9 u3 [$ u1 F( s! X' i$ n" b //后缀都用_total表示。$ A" D }, A z
unsigned int RCREG_total; //统计串口缓冲区已经收了多少个数据
) F4 B, S& Y4 f6 P3 M unsigned int RCREG_read_total; //统计已经从串口缓冲区读出了多少个数据
6 M$ {/ A6 P( `/ O- F* o& ^4 g4 r& i# j$ P! Z8 P" G6 f6 X0 u
//补充说明:吴坚鸿程序风格是这样的,凡是用来更新的标识变量,比如液晶刷屏,或者有新接收的串口数据更新等等,后缀一律用_update表示6 |+ a1 v$ T3 X* \% Z; r
Unsigned char send_update=0; //一旦有数据从上位机发送过来,就会引发串口接收中////断,在串口中断里,我把send_update=1表示目 前正在接收数据,警告单片机先不要//猴急,等串口中断把所有从上位机连续发送过来的一堆数据接收完,再处理。那么什么///时候才知道发送的数据已经发 送完毕了呢?用send_cnt识别。因为在串口中断里,我///每次都会把send_cnt=0,而在main函数里,一旦发现 send_update==1,send_cnt就//会开始自加,当它超过某个数值的时候,就会自动把send_update=0,表示目前已经没// 有数据发送了。而如果有数据不断从上位机传来,send_cnt永远也不会超过某个数值,//因为每个中断它都被清零,这个原理跟看门口狗喂狗的原理很 像。" X$ V$ T: h% d
/ h# W! B5 Q- {# y _; J) ~& s //补充说明:吴坚鸿程序风格是这样的,凡是用来接收数据的缓冲区数组后缀都用_buf表//示
4 y! E7 ]; D$ |- a+ m Unsigned char RCREG_buf[50]; //串口接收缓冲区,读者可以根据实际项目设置大小7 Y! I9 R. k4 A, ?! \, r
Unsigned char RCREG_buf_temp[50]; //临时处理串口数据的缓冲区,可以不用那么大
# ?* A( u5 q% `2 R8 k' g, z8 g- y- T! q: u
//补充说明:吴坚鸿程序风格是这样的,凡是自锁变量名, 后缀都用_lock表示。
- n) t' O1 X% U1 f Unsigned char send_lock=0;
) x+ L$ _ b+ a8 L/ J4 Y" Q8 z* d% ~* K3 b0 x; Z* R
//主程序
/ H; ?4 O2 i4 H5 J2 U* l main()
3 j# x( [5 [+ ` {" l" i7 H {, ]' _6 _- Z% s N& M' U
ADCON0=0x00;
3 [& F8 Y7 B0 K% g# P3 m ADCON1=0x0f; //全部为数字信号8 x+ N4 W; x7 t
ADCON2=0xa1; //右对齐
( z- L! p+ \( K$ M' V RBPU=0; //上拉电阻
1 h8 ]# N1 I0 Z SSPEN=0; //决定RA5不作为串口
. [9 s' N' D5 O1 E: V/ F" b9 v) v! ^
" h8 R* T. G9 u4 i+ ?- i TRISA2=0; //蜂鸣器输出/ P- P+ I8 i/ x. c
7 E- Y% s r: }) q$ v* }
0 [# {8 x5 j7 a3 ~, y: D
BRG16=0; //设置串口通信寄存器
J- a& \1 `' U/ }" b/ a# g BRGH=0;) D7 E m6 v9 u2 P
SPBRGH=0x00;3 r5 b/ w# J3 e
SPBRG=0x02; //22.1184MHz晶振,115200波特率! \* Z, d4 {5 @& L% Q
SYNC=0;
2 ]* G$ R4 [0 p0 z; \& [# d% s SPEN=1;
5 @) O0 u! k9 K& D2 D+ o TX9=0;
- c0 s( s/ G# m) ? TXEN=1;6 l! I1 c+ N$ v4 q& O
TXIF=1;( G: v& Q" N9 c; S- f
RX9=0;4 T* Q) c3 ~$ p+ X/ e2 G
CREN=1;
3 w7 d" T" L$ F* I! c3 E RCIE=1;: w' U' d. P4 D6 H
PEIE=1;, F, z/ l: y" r7 K1 b3 J' G) ?. l
GIE=1;
1 y4 |% W0 [8 g% p5 n: A* w# V I2 h, X
% H8 D p0 |# X O/ w! @: x; m
T1CON=0x24; //定时器中断配置
$ w; g; }* g+ d& G& u TMR1H=0xFE;! T+ U) P) C; j6 C# c- C
TMR1L=0xEF;
% G$ V$ k& A, D' K0 Q; J( ^! q" D TMR1IF=0;
& I$ D: H: _8 Z5 J TMR1IE=1;" x% S+ M1 ~ Z1 A. j9 p
TMR1ON=1;3 D& r0 J$ t0 Q# M' S
TMR1IE=1;
4 G% f/ P4 ~$ @+ S6 n //补充说明,以上的内容为寄存器配置,每种不同的单片机会有点差异,
7 L: y3 E3 E2 e //大家不用过度关注以上寄存器的配置,只要知道有这么一回事即可2 M$ s8 u |" _# s& E
V$ e- a, t( G2 |( w6 E- { beep_dr=0; //关蜂鸣器,上电初始化IO
' }: Q/ o0 ?0 g2 H3 g& }) u' f# P( _4 c" b' [3 k
while(1) ' l9 N% R( x- w l4 N- f( K) S
{
" s2 [/ A% f, r. O CLRWDT(); //喂看门狗,大家不用过度关注此行( ?: N; W+ z% t! T) e! m
usart_service(); //串口通讯服务+ c2 [- K' [) ]; e6 q
}2 p! c* G1 ?( @. r; s
* [+ t7 h5 z3 z9 ~/ a6 S. g
}
0 V) J0 \& N _0 } 2 k0 |+ Z3 [& a" `- x
//中断( Z, s6 z6 O$ f( k- ]- R7 b
void interrupt timer1rbint(void), U/ a/ ?' a1 H* z" a: }( K% Q4 u) g! o
{
}, L- D3 j2 ? @. C, G
- v |6 ?; ?" t0 E) ^. x s if(RCIE==1&&RCIF==1) //串口中断,一次只能接受一个字节
4 J9 {2 T" [4 n, s* x {
; H' ?- t2 [/ g RCIE=0;
" Y5 d/ D% [/ m) s+ B- y! c RCIF=0;
+ N: G1 u/ C4 U# H+ R0 {, ^+ |; F1 o$ Q) a. N
+ ~6 \9 F6 G/ |" k
++RCREG_total; //以下代码是鸿哥在所有串口项目中用到的标准代码
7 N8 {$ b# W- L) _- D6 o* Y if(RCREG_total>50) //超过缓冲区& @/ \$ x, X1 S7 i5 P: B3 E, V6 ~# S
{1 U* D- q/ }6 u; u( I: u
RCREG_total=50;
% [) e# P& F9 N4 f" \6 L }5 E, ~4 \- Z5 D7 g
RCREG_buf[RCREG_total-1]=RCREG; //依次把上位机来的数据存入数组缓冲区
1 O4 {& `. L3 p5 b% B% _ send_update=1; //通知单片机目前正在接收数据
; u5 I/ Q3 U' [$ J. s$ i$ i) @, _ send_cnt=0; //及时喂狗,虽然main函数那边不断在累加,但是只要串口的数//据还没发送完毕,那么它永远也长不大,因为每个中断都被清零,很可怜。9 P' z( x5 P; P- z, b- s
3 p0 C4 p' \8 I& W' M( l3 }! u; h
RCIE=1; O/ ^3 A2 @' w. j
& w- H) H$ m7 D0 G7 ~ }
( W' \" f- z* ?# |/ W' ^
) ^' L6 a! i, R+ @, Q if(TMR1IE==1&&TMR1IF==1) //定时中断1 L9 v; T( v+ h" _1 h" Q- }; ~
{
2 W( C! Z" V# [8 L
( Q4 R! X/ n" n" i& E6 O: H TMR1IF=0; //定时中断标志位关闭
( Q4 s) I4 p) e- E7 Z, c1 k) Q TMR1ON=0; //定时中断开关关闭0 v9 _% |( W, ]/ Q( M% l
3 [- x4 q7 t6 i: Z8 C$ \5 j
if(voice_time_cnt) //控制蜂鸣器声音的长短
6 z9 Z [0 j* i7 i4 i {# ?' L# X; ]3 I# ~* N
beep_dr=1; //蜂鸣器响# I9 F" V$ U/ `1 {7 z! @
--voice_time_cnt; //蜂鸣器响的声音长短的计数延时
! ]/ M. g! P5 g1 s }
; _! }4 v$ f& L else . x# {/ X* b: @$ |1 ?# N# {! `
{
/ P0 K' E2 L0 E: ]5 O5 W) U Asm(“nop”); //添加此行空指令为了使else的内容跟if的内容对称,意义////不大# z3 T% @" q% O
beep_dr=0; //蜂鸣器停止
r. ^9 T2 Z g$ ^) H* }- ~0 j0 N/ i& b }. `: S) F/ @7 _8 ~5 Z
6 x" D" n+ Z8 s# { TMR1H=0xFe; //重新设置定时时间间隔
Y9 s1 c# N p- K TMR1L=0x00;
8 ]2 T2 F( S: h, Z+ e' R( Q( F3 t$ K TMR1ON=1; //定时中断开关打开
, t0 G! o% |2 T4 P; p( m; V5 X6 m }
9 h* N: I5 Q* f: l }& y' o" d3 C0 S2 T, o# Y) i' v
" b G* n5 F- q9 f) c H* G, F
; I- e2 P n7 w5 | void usart_service() //串口服务程序,在main函数里/ t4 s$ G! h. \8 R" J
{; O2 ^, _3 G1 n' i6 E. Y
, |% @( E' r, G if(send_update==1) //说明目前串口正在接收数据,不要读缓冲区数据! @! \+ k0 l3 N/ B7 h/ y2 S
{( ~" R2 a0 q% [3 Z- w
send_lock=1; //开自锁标志
( V$ s/ D' u k: Y$ c# R7 A4 y/ l ++send_cnt; //只要有数据接收,send_cnt每次都被串口中断清零
6 D2 E, R9 C0 Q& a3 v2 v: R& D+ \" ^$ v if(send_cnt>cnt_send) //延时一段时间,确认缓冲区没有继续接受数据2 x7 z8 R w/ |; @
{
, R: a, U5 P/ ~ send_cnt=0;8 l% A: H. @5 c1 v9 u" ~
send_update=0; . {7 @( n2 ^: s0 u" N% N/ A
}$ N* S z7 r$ A0 F6 U" J$ X) z( M
}
2 z; T7 H& i$ P- L- K# t4 E Else //说明当前没有继续接收数据了! }0 Q6 R) T2 _4 {' _+ u
{
9 c7 {3 i i S% F: I9 `! G$ Z if(send_lock==1) //在数据已经接收完毕,并且还没有处理过数据的情况下' f4 Q& w( A* K Q& v
{7 x- L8 j9 S$ t
send_lock=0; //处理一次就锁起来,不用每次都进来,除非有新接收的数据( {* ^' T7 }, g# F
while(RCREG_read_total<RCREG_total) //说明还没有把缓冲区的数据读取完" p7 M! m2 t; b7 B; f0 u+ X
{5 S$ f1 q9 {% J; e2 P2 g0 F
CLRWDT();
2 ~- E9 P4 l: ~ RCREG_buf_temp[0]= RCREG_buf_temp[1]; //数据移动,方便截取关键字
9 v2 C" C0 m' y4 ~ RCREG_buf_temp[1]= RCREG_buf_temp[2];( \" P& I8 d2 g. o
RCREG_buf_temp[2]=asy_recieve();
" `" g/ W1 ?! \9 o7 ^- b! g2 } if(RCREG_buf_temp[0]==0xeb&& RCREG_buf_temp[1]==0x00&& RCREG_buf_temp[2]==0x55) //数据头”eb 00 55”判断 W5 t1 h' W3 F" @4 J
{
& F& D2 G( \ R RCREG_buf_temp[0]=0; //把临时处理数据清零,方便下次接收3 H0 a5 J! d3 J3 k3 `( d
RCREG_buf_temp[1]=0;
; W# |2 z; I; n! m RCREG_buf_temp[2]=0; }+ S- K* S: v' Y$ }7 [
eusart_send(0x00); //串口多发送一个填充的无效字节,因为由于硬件的原因,第一个字节很容易丢失8 i2 d4 [6 l3 [3 n/ |
eusart_send(0xeb); //串口发送应答的数据
4 j: ^9 o) }; ?8 O2 m7 o eusart_send(0x00); //串口发送应答的数据, S3 F! L- ~' o8 D1 B4 E% Q1 g
eusart_send(0xaa); //串口发送应答的数据
: B' F$ r' {# C voice_time_cnt= cnt_voice_time; //蜂鸣器响“滴”一声就停
" F6 t; c. ?- Q; }& P+ {# g p$ Z break; //退出循环6 r* O7 `4 |% U$ O7 T
}
5 x" O, }0 q: Q5 y , a+ S' W) ^ w7 R0 q$ a
}$ O. `( N# Y d @
3 m/ o0 V' B5 v+ L2 l- N! D* u4 M Buf_clear(); //把余下的缓冲区清零,方便下一堆数据接收与处理' [5 O' Q' G* X0 p- _% H# E
) w/ G8 w# z0 z0 t* d3 c# c) F
& \* H6 I( m$ F' I; E+ M9 s }: E) e* L+ q) c9 z2 L# w
# c( @0 J; Y" Q1 t; e w- U( e
6 H2 B0 k0 Y0 U. {% x* ^" C }" z; U! n6 H9 S& t" R
f7 Z, I) F5 U% G3 y* K2 b" x; u
) h! `* o1 d" I" r; o }
, r; f7 x' Y, ^. J, E: F2 y7 D: K7 B) d; U
Void Buf_clear() //把余下的缓冲区清零- N& Q. O3 o, R
{
" }8 B" c! f% V* m1 Q* g2 e Unsigned char buf_clear_temp;: `% i: s! s+ t( J I
while(RCREG_read_total<RCREG_total) //说明还没有把缓冲区的数据读取完
7 }4 l5 B- c4 q. Z+ @- z {! p' y; T& e/ X/ x
CLRWDT();
. z1 L/ q9 w+ m! B5 m8 ~% ? buf_clear_temp =asy_recieve();, A( ?8 {+ I/ {2 z# Q$ ]* G6 x c
}
. w2 H9 s1 K6 p" P6 Y P3 ]8 t$ g; d5 P) Q/ @
}: P: ~$ d3 P5 i2 A
unsigned char asy_recieve() //把串口缓冲区的数据一个个提取出来& ^ p- Q# r7 A9 n
{
, c# m3 a* J3 V+ f* D unsigned char RCREG_dt;/ E$ [3 D- y3 j8 o
6 O E; }% w) J7 W; R
++RCREG_read_total; //已经读出了多少个数据
+ Y2 @( `7 ?# h: M! n4 y RCREG_dt=RCREG_buf[RCREG_read_total -1];2 F2 X$ q- R. N! ?8 B& ?( U
( B7 r) r d' I. J if(RCREG_read_total >=RCREG_total) //只要把全部数据都读完,马上把缓冲区清零
4 M$ v) Q" a0 o' h {# A+ ^' A* B! E
RCREG_read_total =0;0 ~3 m3 {9 s s/ B; Q, L6 C8 E9 D9 r8 O
RCREG_total=0;0 g) T6 T1 o5 p; P, W
" Q% t. D R# L! s: M: I }
6 K H' [/ Z8 |% C8 p7 t return RCREG_dt;
" o" |5 x1 n7 }9 E
9 M8 O; b# n$ s }
! U$ F/ X) t5 i5 _; [+ T+ l' u0 y( {
# d5 d+ o$ [, ~) T' [6 K+ O# d* G; e* X& s4 j
void eusart_send(unsigned char t_data) //串口发送一个字节的数据+ {$ g* S/ ?; ]/ @% X" L
4 G5 y* P$ F" j; M$ s- N ]: s! j {! o+ m! [+ H `) ?8 g8 h
unsigned int error_delay;6 t/ | V6 F6 V% I
; r2 u3 L$ n; z
TXREG=t_data; //发送数据
* @/ q6 _% j6 d+ J1 S# y& r3 x1 ]% Z0 h& i. ~ I
error_delay=0;//等待把数据发送完毕) ~! N. j+ v+ B/ w4 f
while(1) //这里也可以省略,直接用延时替代。延时有两种,一种是delay的死延时,一种是计数延时方式。, t W% q; M; f ^7 _3 j' _- F2 G" }
& r. m: E% n/ `3 `$ h( ?
: N, b" Y2 a1 v W# O CLRWDT();
, q7 K( L* L, b/ z) H8 ]9 j( f if(TXIF==1) //等待把数据发送完毕
w2 Y4 u( Y) j: s* k {4 g; K8 r- }4 U& [) q' o
break;
( E8 D: ]- E- V, Q6 ? { }" \% \/ \% N% f7 C
Else $ I' V0 s& j$ p3 t
{
" f% l3 h4 E0 A( m ++error_delay;! m8 R6 I- B( ~" A4 p b. z* R' N
if(error_delay>200) //超时也要退出,不能死等
/ N1 B6 ^& M4 a7 c {
1 q7 O5 ?: I# ~0 _% Q, w& B5 [! D7 _ break;2 a0 X. r. K# U7 Q
}% ^' x; B1 Y/ H, z" a% R5 I
}: m1 O7 h8 [7 x
}
5 c# f0 x6 q- B" H# z) F1 g6 m! m& T2 |7 K% q
Delay11(1); //此处最玄机,要特别注意。每发送完一个字节,由于不同的项目,这里的延时间隔都不一样,读者根据实际情况来改。这里最容易出问题,必须要延时,尤其是连续发送一堆数据的时候。在一些实时要求很高的场合,读者也可以把
3 ?2 Z/ l+ b* [4 y! j/ x0 `1 J 这个死延时改成计数延时的方式,有兴趣的自己动脑筋改。一般的项目这样子已经够用了。# h5 _3 d: }3 t% v8 p8 B3 W9 B
Y9 i* t% i& e! T/ \6 ], B
}/ K/ M! D4 K7 y& r) {
: b7 n* p# |( w, s9 | //延时函数
, L) O+ \! \5 q* Z) e" V U+ J void Delay11(unsigned int MS)2 ]8 M7 w( E3 t( n) s1 ]7 c2 r/ R
{. S' X7 ~+ X; K1 c
unsigned char us,usn;' k" J! g7 c. J, K: X" b9 g! o9 T
while(MS!=0) //for 12M
) a! O( n8 i B4 Z { p$ M$ F1 T) Q( T1 ^0 f' `
CLRWDT();# s1 K" ]1 y7 i! n
usn = 2;
/ T6 I7 m5 ~2 {6 m2 F. ? while(usn!=0)" i' w' A% [+ C/ R3 z2 X( _. R
{ CLRWDT();
# ~1 E* `5 f3 @7 P us=0xf5;
" ?/ `, V0 Z* m8 e) V! H! \ while (us!=0){us--;};3 s" q+ J9 K" C5 m% |5 V3 a
usn--;
2 s9 c$ S8 F/ R$ V }! U( }: k" B, ~
MS--;2 Y9 ?7 v: [* d, B* E: D
}) i& X0 [1 I" E3 i" o) r
}- c& ~1 H: t0 `; @ N
# P' C7 T1 w6 n1 J
(6)小结:
1 X8 n, o% \/ o0 g h (a)发送一堆数据的时候,要特别注意每个字节之间的延时间隔。
2 H0 v( C8 {/ ? (b)必须等待上位机的数据全部发送完毕,再去处理缓冲区的数据。4 i- v& z ~( p9 g `* E
(c)无论是上位机还是单片机,在发送数据前最好多发送一个填充字节0x00,避免由硬件问题而引起第1个字节丢失。2 w( ^- p0 G9 ?- I+ r* Z8 b1 |, V
(d)对接串口线的时候,要特别小心,尤其是接9针串口线的时候,经常容易接反出错。/ v: w& k7 X- \& ?" q
(e)如果遇到很莫名其妙的问题,有可能232的转换芯片已经烧坏了,这种芯片很脆弱。) ]- A0 b* O) O, a% Q/ D
(未完待续) |
|