|
EDA365欢迎您!
您需要 登录 才可以下载或查看,没有帐号?注册
x
(1)开场白:: k7 S' B4 L$ `$ d+ e
. a! y% M& N3 v! [: ~+ u( p
实战中,串口通讯不可能一次通讯只发送或接受一个字节,大部分的项目都是一次发送或者接受一堆的数据。我们还要在这一堆数据里提取关键字,提取有用的数 据。这节我将要介绍我最得意的,最可靠的串口通讯的基本编程结构,学会了这招,用在什么单片机上,用在什么串口通讯项目上都管用。串口通讯表面看起来简 单,实际蕴含着玄机,但是不用怕,我会把这些玄机都公布出来。; m3 f( H) ?3 v* M; t Q# _& R5 P
(2)功能需求:
0 e4 w. A, n4 D3 u( f 任意时刻,从电脑“串口调试助手”上位机收到的一堆数据中,只要中间包含了十六进制的“Eb 00 55”,那么就往上位机发送“eb 00 aa”表示确认,同时蜂鸣器叫一声。
( y2 D( B6 @8 B3 `& A (3)硬件原理:; }% J( S0 E* Y) X1 n- s
把单片机串口通讯的那两个引脚经过一个MAX3232之后直接跟电脑的9针串口通讯。我发现很多朋友会选MAX232这个芯片,而我本人更加推荐用 MAX3232。因为MAX232只支持5V,不是宽压的,而MAX3232不但支持5V,还支持3V。每个人的记忆力都很宝贵,用232串口我只选 MAX3232,不管它是用5V工作还是3V工作。就像74系列的芯片,我的心中只有你(74HC)没有它(74LS),一样的道理,74HC是宽 压,74LS不是宽压。
6 p; y# a$ d" P: S% C (4)源码适合的单片机 IC18f4520,晶振为22.1184MHz,波特率1152008 p7 a7 W/ j! s: |6 X/ g
(5)源代码讲解如下:
) [1 v3 H9 D2 R- p' q) ^" i #include<pic18.h> //包含芯片相关头文件3 _4 o( C- j# T/ H$ x8 @ Y) h
- I5 [, G$ E! u
//补充说明:吴坚鸿程序风格是这样的,凡是输出IO后缀都是_dr,凡是输入的//IO后缀都//是_sr- E; \2 Z* X' w" W7 J
#define beep_dr LATA2 //蜂鸣器输出% n. w# S; n2 H6 L' ^6 Q, F
8 W0 d& s8 b, {; N! u
//补充说明:吴坚鸿程序风格是这样的,凡是做延时计数阀值的常量1 f& I |6 @; A/ I% w: a
//前缀都用cnt_表示。
% ?& m* U& M7 [6 a; T #define cnt_voice_time 150 //蜂鸣器响的声音长短的延时阀值# Q$ p3 R u o4 u
#define cnt_send 300 //确保接收缓冲区没有继续接收数据,是变量
, k; P" `6 V) B! U+ ~( J //send_cnt的溢出阀值
% p+ f8 L5 M3 W9 t8 K# n% f. E# T# X8 `* u# Y
Void usart_service(); //串口通讯服务程序,放在main函数里8 x/ K6 ]/ @1 s. ^. v8 ^6 k# N" [9 f
unsigned char asy_recieve(); //把串口缓冲区的数据一个个提取出来
5 S, L1 \+ J, c! @ F! n8 c+ ~6 m void eusart_send(unsigned char t_data); //串口发送一个字节的数据; R$ P/ N5 Y; l Q
Void Buf_clear(); //把余下的缓冲区清零0 I+ D) q# A9 O0 k5 e% W, r, m! s$ b
void Delay11(unsigned int MS); //延时函数
$ n9 c4 l8 H( n; w- @% l
d; N; Y$ q- `3 g
: t5 b. h' _6 y+ Z2 } //补充说明:吴坚鸿程序风格是这样的,凡是计数器延时的变量, J9 L* M0 `7 g* {7 B- x
//后缀都用_cnt表示。5 f o' o; \2 F% l( z7 E
unsigned int voice_time_cnt; //蜂鸣器响的声音长短的计数延时
4 {" N4 I6 ^' L- V unsigned int send_cnt=0; //一串数据从上位机发过来的时候,他们每个字节之间//的延时间隔很短,如果他们的延时间隔一旦超过了这个send_cnt变量的延时,那么就////认为他们的一串数据已经发送完毕
6 ]# U# v, q: K9 P7 d2 q
7 E& m5 C, w5 Z7 P; Q' J //补充说明:吴坚鸿程序风格是这样的,凡是涉及统计数量的变量# }$ g2 _0 Y" [5 V* z
//后缀都用_total表示。8 f7 l1 j* f8 ?$ }* i7 c
unsigned int RCREG_total; //统计串口缓冲区已经收了多少个数据
% y# i" e. b1 G, X3 a+ S% P unsigned int RCREG_read_total; //统计已经从串口缓冲区读出了多少个数据
! X6 C% C' |+ h8 g3 G$ K
\4 o$ H# C( d5 i" j //补充说明:吴坚鸿程序风格是这样的,凡是用来更新的标识变量,比如液晶刷屏,或者有新接收的串口数据更新等等,后缀一律用_update表示
! b9 Y3 v. K; c2 R4 Y Unsigned char send_update=0; //一旦有数据从上位机发送过来,就会引发串口接收中////断,在串口中断里,我把send_update=1表示目 前正在接收数据,警告单片机先不要//猴急,等串口中断把所有从上位机连续发送过来的一堆数据接收完,再处理。那么什么///时候才知道发送的数据已经发 送完毕了呢?用send_cnt识别。因为在串口中断里,我///每次都会把send_cnt=0,而在main函数里,一旦发现 send_update==1,send_cnt就//会开始自加,当它超过某个数值的时候,就会自动把send_update=0,表示目前已经没// 有数据发送了。而如果有数据不断从上位机传来,send_cnt永远也不会超过某个数值,//因为每个中断它都被清零,这个原理跟看门口狗喂狗的原理很 像。, }( r: c* c. E' Q! ?- o! Z
# ?! D5 F: z: j6 R //补充说明:吴坚鸿程序风格是这样的,凡是用来接收数据的缓冲区数组后缀都用_buf表//示
7 A% L% t% V4 b4 _+ [! B, N R Unsigned char RCREG_buf[50]; //串口接收缓冲区,读者可以根据实际项目设置大小: _0 x+ J) F! V9 K1 x/ R) S. i7 b+ m) E
Unsigned char RCREG_buf_temp[50]; //临时处理串口数据的缓冲区,可以不用那么大) Q7 Y# m/ \: I
% {' G3 L1 Z. A+ Y9 T( z* I F4 o9 O
//补充说明:吴坚鸿程序风格是这样的,凡是自锁变量名, 后缀都用_lock表示。+ G; g E/ y6 W
Unsigned char send_lock=0;
3 R5 s: @# w1 T' D1 H) a C0 m$ s' O) _1 E: `$ q
//主程序
, o: C* h" e' k& Q3 G P main()
- ~3 E7 c }/ D& E( s5 Z {
6 w8 l- H! o+ z: x* W ADCON0=0x00;
! I1 s" F# ~% C% I: ^4 I- a( U ADCON1=0x0f; //全部为数字信号
& ~6 S9 i2 Z# A ADCON2=0xa1; //右对齐
. [! e0 {: b0 h! U' p9 q; c RBPU=0; //上拉电阻
R6 [/ k! z5 _. ?4 ?) E SSPEN=0; //决定RA5不作为串口
! F1 y* C: L: t8 c& |
% w4 ]2 G1 r: M8 R5 j0 r! S TRISA2=0; //蜂鸣器输出
, q: P( q4 p C2 ~; R+ S) P' @, ~, }) K5 H2 ~3 H$ U3 D/ d
, J% G) u/ g/ k2 _ BRG16=0; //设置串口通信寄存器+ V+ O3 W) m+ t! u
BRGH=0;
3 M- Y5 H' G* q6 s+ i SPBRGH=0x00;
6 Q/ j* \9 D- F/ R SPBRG=0x02; //22.1184MHz晶振,115200波特率# r. q m) r# G8 ^7 y2 c
SYNC=0;
2 n# u: q* b$ L SPEN=1;
$ W+ ]$ f# o6 Y* M$ n TX9=0;
( Q, ^5 {) ^: X2 e* m TXEN=1;7 W4 I9 N& n; V2 g; j
TXIF=1;3 P& j! {( |, L# l2 C z6 B+ u
RX9=0;
; {6 m4 R8 p2 }; v CREN=1;
: h* `! @3 f: A) w S6 w RCIE=1;
, A$ n, o6 o% G( D5 ^ PEIE=1;* D$ v2 x0 l9 S1 b! V! L: o
GIE=1; l2 j9 l; G- i2 q6 n
9 W5 R8 v! C& k. j. n$ ~8 E
8 e& v! j" V7 @
T1CON=0x24; //定时器中断配置
: {5 v; P( h7 p7 U) A/ p, t1 W; A TMR1H=0xFE;
% O; R% }, d4 k: y* b0 n/ ~ b TMR1L=0xEF;2 |0 v: B: S8 f0 }! P! ?+ g7 z
TMR1IF=0;
2 I7 @' C0 a0 _/ s) T3 m TMR1IE=1;! g3 m7 {5 z3 g% r
TMR1ON=1;
4 i. W$ d3 ~- _$ z' R3 ? TMR1IE=1;
8 |$ w+ H& I9 U: h7 J //补充说明,以上的内容为寄存器配置,每种不同的单片机会有点差异,
8 H# I- [# g. l' v, r4 [ e9 \2 r //大家不用过度关注以上寄存器的配置,只要知道有这么一回事即可
- v3 W. R3 M6 O" ?
4 O. k, Z% V, Q$ K7 C beep_dr=0; //关蜂鸣器,上电初始化IO
7 {# g) h, G. a- ?1 l, D( S
" z3 ?( ?& r! h" q+ V$ B while(1) / G, \8 A- b. `5 | ?
{
8 Y4 `! y M7 Y' r CLRWDT(); //喂看门狗,大家不用过度关注此行
8 x* @* ]! p; V _ usart_service(); //串口通讯服务
7 P/ i2 ^. f `. K; ~ }+ x8 i0 V& n# E6 Y
) ?7 q2 |3 t+ I2 N2 U& K
}' c2 ^# A/ m, z* f# }, w
4 W' u4 S, s+ x- B //中断! }( o4 e- P Z; q3 R, L
void interrupt timer1rbint(void)
$ u, h( S8 S9 F# b& W5 M& `! e3 s {
) m$ F8 @& ]" x2 B7 }! z
x; k8 P$ D, S% `. e4 M) _ if(RCIE==1&&RCIF==1) //串口中断,一次只能接受一个字节
' M# i( s8 @ C. l: T c* ~ {* J1 [# q3 e6 n0 x2 F) L7 N9 M2 d
RCIE=0;/ ]# m+ N R* ?) s7 h6 S% N) T
RCIF=0;* x" Y& u& h4 V$ n1 B
2 V0 m e, G4 }! p5 c* s4 ~
3 d% s( p+ ?5 d! H ++RCREG_total; //以下代码是鸿哥在所有串口项目中用到的标准代码
p. ~$ |4 {& B9 t( s if(RCREG_total>50) //超过缓冲区2 ^1 q l! d7 V" h, {8 N! q
{
, S+ z0 P, }8 Z2 a1 F: ^ t1 R ? RCREG_total=50;
9 D6 e: u( }. v, [, k5 Z }
' c% Q4 ~( p- |& [/ \& \7 T6 w* D* \ RCREG_buf[RCREG_total-1]=RCREG; //依次把上位机来的数据存入数组缓冲区
2 F9 Q: V- l' X( b; T& H send_update=1; //通知单片机目前正在接收数据* N$ g; W* v( J. p0 N4 _& h
send_cnt=0; //及时喂狗,虽然main函数那边不断在累加,但是只要串口的数//据还没发送完毕,那么它永远也长不大,因为每个中断都被清零,很可怜。' z3 @- R- D. x$ U- Y
+ q S( X: ~ @ RCIE=1;
) r$ M0 O2 a- W9 j
6 [9 @: [5 N$ w% N3 | }" z' f, t Y! v9 M" I$ s
: o& x: }. Y+ U! S! Y% j if(TMR1IE==1&&TMR1IF==1) //定时中断
4 i0 h: \/ X# O W- U! g {
1 f& T# u9 o# u' I7 O. Z3 z
! L1 ~0 j. \4 k! Y TMR1IF=0; //定时中断标志位关闭
L) t/ l1 {. x$ N TMR1ON=0; //定时中断开关关闭+ u% |! {: }9 y. U+ }
+ D S% V. S$ q if(voice_time_cnt) //控制蜂鸣器声音的长短5 s# d: U) `( p
{* ?" s' @, P7 g- V, G) J8 j3 Z/ @
beep_dr=1; //蜂鸣器响& T- w! [0 z$ G
--voice_time_cnt; //蜂鸣器响的声音长短的计数延时
. [! Q O3 _( R: D4 z9 B }
F& a) D- S; b: o8 Q: i else # _ `5 e8 O9 ~; b
{0 f, v1 w* z. W
Asm(“nop”); //添加此行空指令为了使else的内容跟if的内容对称,意义////不大9 T& v: ^7 [" A2 R* e
beep_dr=0; //蜂鸣器停止5 W T# S6 } E% D- \
}2 q3 {" Y# U, w$ S3 b
7 J2 ~6 ^; u9 B- o: \8 i) s TMR1H=0xFe; //重新设置定时时间间隔
4 K- _8 x% g, b2 y TMR1L=0x00;/ _+ b1 ?& ]" {/ {* m
TMR1ON=1; //定时中断开关打开# {3 Q8 |% O- q6 s0 S4 G
}5 Q& C9 A0 ^' J2 e
}
3 Y- d R9 a. y' n/ L7 j1 |+ R) \3 M7 G
5 F5 A8 f7 k6 M w4 r void usart_service() //串口服务程序,在main函数里
: j! L2 ~7 a( }1 s$ b {
3 i* @1 Q5 e0 Y, c2 _
3 T2 r; d6 J j* K if(send_update==1) //说明目前串口正在接收数据,不要读缓冲区数据
% |- ?9 n! [% P6 N' d {
" [2 y4 f; G$ |( P" Z D send_lock=1; //开自锁标志4 K* V) g6 }* g2 _. o: A9 C) a
++send_cnt; //只要有数据接收,send_cnt每次都被串口中断清零' ?3 L3 H; [; k7 ~
if(send_cnt>cnt_send) //延时一段时间,确认缓冲区没有继续接受数据
) N7 z1 ?) p& y {
" ~# R1 H& N- f. G4 A: N" G3 E1 S send_cnt=0;
3 ^" K, j& |) O send_update=0; , @1 L7 d% M2 x* a3 S6 y; n
}
' _* \6 {; c" U K) F }: o0 F1 P- h. r+ G8 o* y$ I" E
Else //说明当前没有继续接收数据了5 g8 n8 S9 o) e4 f
{5 J; ?- Z) x1 O% n/ Y
if(send_lock==1) //在数据已经接收完毕,并且还没有处理过数据的情况下
- b* o1 [$ S$ b! g! T {5 k' G2 W5 c/ I+ y% e& Y6 r& k
send_lock=0; //处理一次就锁起来,不用每次都进来,除非有新接收的数据
2 Y8 l. z {1 I1 `# ?. ?4 E, q while(RCREG_read_total<RCREG_total) //说明还没有把缓冲区的数据读取完
% [( Q2 B( u" j. X( {7 N9 s( b3 ` {
5 W. U1 G) F# H$ \% B1 [3 _ CLRWDT();
/ b2 o# y% c8 f. e2 ~1 g RCREG_buf_temp[0]= RCREG_buf_temp[1]; //数据移动,方便截取关键字, N, ^- Z- u7 |$ j0 ^1 b7 U/ _
RCREG_buf_temp[1]= RCREG_buf_temp[2];
7 y. |2 D6 K3 F: _ RCREG_buf_temp[2]=asy_recieve();
) v0 l) R! T; P J7 x8 A% @1 p7 c! N if(RCREG_buf_temp[0]==0xeb&& RCREG_buf_temp[1]==0x00&& RCREG_buf_temp[2]==0x55) //数据头”eb 00 55”判断5 Q* N s+ M1 W. J# \6 S
{6 T1 U7 x; t; z; n/ P0 b3 h. g
RCREG_buf_temp[0]=0; //把临时处理数据清零,方便下次接收
, z, f: i9 l6 | RCREG_buf_temp[1]=0;
$ k* @; ]" u; u5 T RCREG_buf_temp[2]=0;
; m& z1 P- l9 E* h eusart_send(0x00); //串口多发送一个填充的无效字节,因为由于硬件的原因,第一个字节很容易丢失
( ^$ j2 m. h' e q eusart_send(0xeb); //串口发送应答的数据
; V$ z9 N; w+ e8 g% C g eusart_send(0x00); //串口发送应答的数据
( Q5 q2 L2 A/ n% E/ ^3 d: g eusart_send(0xaa); //串口发送应答的数据& F# V& e; ?! l; `6 P
voice_time_cnt= cnt_voice_time; //蜂鸣器响“滴”一声就停5 i7 i% G( B' w; c
break; //退出循环
/ C3 K& n5 h( }% C- n V }# F& Y f0 v/ ~
( y3 ^: W2 z8 e; l7 K6 \9 x
}" H& c8 E* P7 E. z' v% s" N
& T. i, g1 `( U8 p1 M! P# S: K0 |/ l
Buf_clear(); //把余下的缓冲区清零,方便下一堆数据接收与处理, f/ U7 x; {9 P5 I/ H' f2 T+ D
" ]& _3 ]; x% @3 O( j/ u+ Y
9 h/ k2 U/ D% y0 {+ {) B# k1 g0 v, z( ? }
* F m P1 {% ^5 {+ u- W& r6 g% v) ~" x- G3 `8 N
" ?. s+ I2 O, R* H }$ _( H( Z' L7 o$ U3 J) e! B. Z( A
4 z0 t/ h! M5 p; @
p* B! |& W, R8 Z% R! d0 ]
}
5 k5 |; a; ?( ?/ h4 n3 B) @
`3 a$ s% }5 v0 ?% G4 u9 V Void Buf_clear() //把余下的缓冲区清零- Z7 e9 p' p- ?8 h7 N8 i* c
{
+ E9 V0 O' t; f* g6 P Unsigned char buf_clear_temp;
: S; [0 Z* B& ^5 e while(RCREG_read_total<RCREG_total) //说明还没有把缓冲区的数据读取完* C& D% r8 }& r% O, I* Z' J, ]3 N
{3 }& s* T2 ?+ T! |7 T8 t# }
CLRWDT();: X. u4 j* z7 H
buf_clear_temp =asy_recieve();& d, v( V8 e9 K! |# c% @1 I
}" j3 @2 x+ ? }1 R |( n& _
/ A4 J; j& K8 T* }$ ~ }
) P/ B* k1 U& w' J1 x4 {. z unsigned char asy_recieve() //把串口缓冲区的数据一个个提取出来* C2 b6 T9 b2 P# W
{
( [/ M; |0 `" c: ~ unsigned char RCREG_dt;
0 j5 A3 p/ Q& g. i+ F
2 T) j% D, o6 O. ?2 j' ` ++RCREG_read_total; //已经读出了多少个数据
3 g0 \6 o2 n. [1 f, B RCREG_dt=RCREG_buf[RCREG_read_total -1];
' I) x, }* ?$ f, L
0 }) _* p, U; C# V: f if(RCREG_read_total >=RCREG_total) //只要把全部数据都读完,马上把缓冲区清零
7 M( [, R; H" A' l- G n; Y$ L {
- r- ]* r e) ~/ d2 N% U3 o RCREG_read_total =0;
3 P% f( a- H p& T RCREG_total=0;4 M! M) F M: k
" |; s3 z5 h: U2 t, t" [! B( O% d
}7 N* o+ k# x# M2 Z# n0 V
return RCREG_dt;5 l0 s" B8 O( O: w7 S9 [
M( n4 y/ I- G5 P6 D0 L- A9 M9 w
}1 ]% \5 ]2 `1 U# Y- e# s3 f
3 Y$ C+ ^3 p. Z, D7 P' N5 B m% }; P7 W4 [
void eusart_send(unsigned char t_data) //串口发送一个字节的数据
5 \8 e: c! K- d q; D* s, ^
7 Z* Y4 l7 d* M1 b) N/ E {
6 _9 }% t: b& p- u& o& M4 Q5 z& ` unsigned int error_delay;
: a" {+ g9 f# H0 \6 Y
7 v7 X0 [4 U3 G& J; Y- q TXREG=t_data; //发送数据
- r, T: I4 L# P g! N4 x8 _ p4 g; z. Q& }4 I# E
error_delay=0;//等待把数据发送完毕
& v) b ~' Y7 d while(1) //这里也可以省略,直接用延时替代。延时有两种,一种是delay的死延时,一种是计数延时方式。
" i. G8 Q; C9 R+ ?
; } j% l B. K1 J5 R- b! b1 a6 t0 x$ b: X9 f
CLRWDT();
, _9 |& s) \9 J- l1 S- I1 e if(TXIF==1) //等待把数据发送完毕
9 }4 U1 l7 q% z' A0 `% @ {
6 C; `8 g6 `0 D* V break;
# N7 v7 M9 |4 |$ o4 G& G6 ] }
, {' h1 V" ^3 M% q6 ^3 u2 g4 ^ Else
3 C. s" p$ l# [% ^4 r {
4 Z! e& a' h/ v- S7 V ++error_delay;! Z9 B1 R# J, B; Q+ j) n/ S: Z
if(error_delay>200) //超时也要退出,不能死等% A. B+ \1 ^6 n2 g l8 I* K3 {! ^+ V% l
{5 | f d% ~2 V2 B1 w6 I; }
break;
+ L0 D& H! Z7 M+ U' T& ] }
3 }1 R ^7 l! q/ G2 S- D }
0 r, G6 c h: l& f& B/ a! Y. A! ` }
5 a* c/ B1 {4 M% M- E+ H5 g2 b& M! b7 k: x0 U7 S% o
Delay11(1); //此处最玄机,要特别注意。每发送完一个字节,由于不同的项目,这里的延时间隔都不一样,读者根据实际情况来改。这里最容易出问题,必须要延时,尤其是连续发送一堆数据的时候。在一些实时要求很高的场合,读者也可以把4 X5 I, a& n& O
这个死延时改成计数延时的方式,有兴趣的自己动脑筋改。一般的项目这样子已经够用了。. a7 E1 s! E& b0 y! D. J, q
, b* ?1 ~- k% ~9 @+ o9 ^+ M2 y, H
}
0 F) k. U3 x5 H* Z/ Y1 _% V/ T2 j# {+ w% ^3 }7 A
//延时函数6 A7 Z% P m) p$ ~
void Delay11(unsigned int MS)
$ o* i; J& I9 p$ H {
- ]! B8 w/ d4 N2 z5 N) o) i7 } unsigned char us,usn;2 d/ a- c. ~' a+ K" l4 o. ^
while(MS!=0) //for 12M
, s% h& ^; b% _' H, }8 | {
/ _2 S; X! V# i' i4 i3 `1 Z CLRWDT();# L- o, i% L, p7 r; X
usn = 2;. q u* j0 K8 \7 j
while(usn!=0)& Q' j, v" c5 x8 y
{ CLRWDT();( \* R$ ]. `' P# H& |
us=0xf5;& X; V) t/ }9 z
while (us!=0){us--;};
2 [$ U% P5 a+ R6 ?, P- u+ | usn--; h O8 A0 \9 r0 { `
}8 C5 F7 q0 k! g7 W6 O
MS--;9 m0 C$ B$ n2 `, M# j4 l
}- Y' U' w. z/ T4 T) H
}9 j [: g2 A5 l6 F8 U9 S, a
& V. o" e$ x) V (6)小结:; R9 h. f' X8 }0 f
(a)发送一堆数据的时候,要特别注意每个字节之间的延时间隔。
4 L, U( q3 }$ o& A2 u (b)必须等待上位机的数据全部发送完毕,再去处理缓冲区的数据。
/ j6 I5 R1 T5 x. ~# V (c)无论是上位机还是单片机,在发送数据前最好多发送一个填充字节0x00,避免由硬件问题而引起第1个字节丢失。8 J1 o6 Y0 h' R% @5 w( v
(d)对接串口线的时候,要特别小心,尤其是接9针串口线的时候,经常容易接反出错。
) w% }) t$ M: w! ^% A; @0 O/ P (e)如果遇到很莫名其妙的问题,有可能232的转换芯片已经烧坏了,这种芯片很脆弱。
. ~3 `4 B4 q" F6 Y& ] (未完待续) |
|