16.1 关于I2C
0 O% r$ p6 o4 J! A16.1.1 I2C协议
6 I; `% g+ ]! ?, `: a0 m3 x4 v" F8 i% s* X/ p$ \( g
I²C(Inter-Integrated Circuit),常读作“I方C”,它是一种多主从架构串行通信总线。在1980年由飞利浦公司设计,用于让主板、嵌入式系统或手机连接低速周边设备。如今在嵌入式领域是非常常见通信协议,常用于MPU/MCU与外部设备连接通信、数据传输。0 i- D! l: S% j* B
/ w {% B9 ?$ |1 o8 T) W6 yI²C由两条线组成,一条双向串行数据线SDA,一条串行时钟线SCL。每个连接到总线的设备都有一个独立的地址,主机可以通过该地址来访问不同设备。因为I²C协议比较简单,常常用GPIO来模拟I²C时序,这种方法称为模拟I²C。如果使用MCU的I²C控制器,设置好I²C控制器, I²C控制器就自动实现协议时序,这种方式称为硬件I²C。因为I²C设备的速率比较低,通常两种方式都可以,模拟I²C方便移植,硬件I²C工作效率相对较高。% ` f- X) h) ~& X! u
1 a( t) j, X8 |; C2 M* k关于I²C协议,通过下面例子进行一个形象的比喻方便大家理解,如图 16.1.1 所示,老师(MCU)将球(数据)传给众多学生中的一个(众多外设设备中的一个)。. N1 ?% c Z& Q* B; O% n6 r
# [: F" {, R- r6 ?) k
$ K) i* V7 b- ?/ y! U图 16.1.1 I²C协议比喻 ! m0 V" c3 \ e Z8 g7 k
7 k6 D/ s/ _3 @# m; F; g. v# ` |: _
0 O3 N% d* e3 F. l0 _( H首先老师将球踢给某学生,即主机发送数据给从机,步骤如下: ^. \' Q2 x( ^0 v, P
+ l' X, u% L* b8 e) w3 b$ E1) 老师:开始了(start);" M9 A7 X3 Y+ m' o
; m/ o/ p+ M" T9 @: |
2) 老师:A!我要发球给你!(地址/方向);
( [" p% c9 m/ ~0 _: ]( j: |$ U: v8 l- K" h" ~- K f- s" u
3) 学生A:到!(回应);7 T" v* r% O1 Z# `1 O
: F, Q4 H; \0 E* X3 M4) 老师把球发出去(传输);
0 r6 |" D9 t7 ~6 ?$ M
/ s- L/ e: n8 a$ t' E. Q: k5) A收到球之后,应该告诉老师一声(回应);7 ]- X9 d* J8 B
, C/ I& u3 |0 u2 s! i6) 老师:结束(停止);) V7 ?6 @9 J8 h
) I9 t4 K( _/ T, c: C
7 V2 G. N0 h5 w7 w7 a" C U% g
接着老师让学生把球传给自己,即从机发送数据给主机,步骤如下:- Q }) w( p4 q% r+ a' [+ X- E3 ~
/ a* k& q/ \ ?4 j- P1) 老师:开始了(start);
' j; u) ?5 n9 T3 e4 V6 M! q+ F3 ?+ i) B) U0 o. ?! y3 _
2) 老师:B!把球发给我!(地址/方向);
6 I* b8 `; e+ `7 ~
- Y/ o9 o, ^2 _5 M6 Q3) 学生B:到!
t9 y# u3 a: U# i( B# X k" u8 m f3 A" E1 ]
4) B把球发给老师(传输);
1 k, p3 R4 K9 {& p" Q
( o5 ?$ M5 ]6 P. d' ?* ~5) 老师收到球之后,给B说一声,表示收到球了(回应);1 P0 w2 X! p5 l7 \ ?& x
+ M) I$ ~" N) W7 s: b, V; N
6) 老师:结束(停止)。% @) p2 R$ V. h7 M
0 n5 [6 F) c' r/ K4 w: u
! o+ a( l3 b* @0 W* T0 @ S9 F W; C
. @" _/ i( M3 T/ q4 j+ e; T% ?" u. i从上面的例子可知,都是老师(主机)主导传球,按照规范的流程(通信协议),以保证传球的准确性,收发球的流程总结如下:; P6 p" }' E7 {' s9 J! {7 O
. {5 g: ]& F5 E" B① 老师说开始了,表示开始信号(start);( T& y+ X/ @/ J4 z8 e
) E* n" o. n+ X0 a4 d% o9 |② 老师提醒某个学生要发球,表示发送地址和方向(address/read/write);$ S. i, \! S- |1 U0 p0 a. I; _
# H' h! R5 ]4 h* D/ q3 r9 l③ 该学生回应老师(ack);8 Y, Z; r; k: Z9 ~
4 J& I5 \# v" x! j9 Z9 T7 v0 y
④ 老师发球/接球,表示数据的传输;
. `; p6 X( B5 Q; ?7 H0 [
1 e8 m" y- z9 u# m' y4 k+ \⑤ 收到球要回应:回应信号(ACK);
! p. M. h0 n* Z6 q
+ D/ V( n5 Y0 ]0 {0 V& f: I: [⑥ 老师说结束,表示IIC传输结束(P)。& u) U( x, S) g2 F% g" e
8 J( X$ ^1 h" `6 ~) o9 s
2 K e& q! \- C0 V- E
4 X+ V0 t! P, s( h( k- I( u以上就是I²C的传输协议,如果是软件模拟I²C,需要依次实现每个步骤。因此,还需要知道每一步的具体细节,比如什么时候的数据有效,开始信号怎么表示。
/ H7 P3 C3 S3 D: z! D5 d. J+ t# X" A0 n, E( r: D% n5 g# e7 q
; `# z! ^, v A+ Z1 ~
( L( r5 E: m: r! Z( c
数据有效性+ M) R- j( ]0 O/ h, d1 y* }
8 l$ m% s/ {. G2 j& P9 Q9 rI²C由两条线组成,一条双向串行数据线SDA,一条串行时钟线SCL。SDA线上的数据必须在时钟的高电平周期保持稳定,数据线的高或低电平状态只有在 SCL 线的时钟信号是低电平时才能改变。换言之,SCL为高电平时表示有效数据,SDA为高电平表示“1”,低电平表示“0”;SCL为低电平时表示无效数据,此时SDA会进行电平切换,为下次数据表示做准备。数据有效性示意图如图 16.1.2 所示。3 R6 K. C7 I" @/ H
% ?6 M" {/ c" X% X
3 k' X4 Y3 A, y6 }4 Z# G/ g7 q
9 U% \+ F; s% F: p$ q3 l% `图 16.1.2 数据有效性 " |4 ?/ j' H0 X& \
: y* C+ b9 G4 _# w1 \
$ y5 b! S! x9 R+ d" r; K% Z1 D开始信号和结束信号
& `3 m1 j1 l" [7 g$ G! h+ F
# {7 @$ s ^1 m2 W4 e0 w$ v1 AI²C起始信号(S):当SCL高电平时,SDA由高电平向低电平转换;( o( N; i( y1 j* D4 a
5 H, z- w, l, T( p, G
I²C停止信号(P):当SCL高电平时,SDA由低电平向高电平转换;
1 m- Y3 V3 U# ?2 c, k# |" }! e# ?' U, b% R
+ y$ M( t, Q2 z9 x& A3 u0 E
) A0 d; b0 x7 D# U. j& }; ]图 16.1.3 开始信号和结束信号 . i& }0 ?& W& N' o3 ~- P
- S/ G6 M0 u9 v1 G& O3 }3 z) W
! Z3 Y, D6 Q/ Z+ A7 y" D应答信号
' p( o, a+ g i8 ^9 v0 o, v
) @) L7 b6 T4 ~1 v3 [3 UI²C每次传输的8位数据,每次传输后需要从机反馈一个应答位,以确认从机是否正常接收了数据。当主机发送了8位数据后,会再产生一个时钟,此时主机放开SDA的控制,读取SDA电平,在上拉电阻的影响下,此时SDA默认为高,必须从机拉低,以确认收到数据。! H) \3 a% t" y8 D' _! i# B, N: c2 B
2 e- d6 Q6 }/ k$ j
* H. d8 s9 x" b6 z3 G8 [- p
5 B' u/ _( @9 h" E图 16.1.4 数据传输格式和应答信号
/ s9 n8 s/ b0 P0 y& k. h
- V+ m/ j3 U7 P; l! Y0 Z
9 c O; X7 G/ @- I完整传输流程 " P7 s, V. q0 U2 V
6 W, p2 N9 i: f; _ i$ T% |
I²C完整传输流程如下:
& V7 [% S; v/ R0 C; V+ w2 d+ z8 P2 i+ g' d3 M. S% [
① SDA和SCL开始都为高,然后主机将SDA拉低,表示开始信号;+ k# i% a+ F7 R# g0 x& Y7 [' z
/ c4 s; T- D$ w& {② 在接下来的8个时间周期里,主机控制SDA的高低,发送从机地址。其中第8位如果为0,表示接下来是写操作,即主机传输数据给从机;如果为1,表示接下来是读操作,即从机传输数据给主机;另外,数据传输是从最高位到最低位,因此传输方式为MSB(Most Significant Bit)。1 t+ A( r h/ a
. I& i7 I C/ F& t5 n③ 总线中对应从机地址的设备,发出应答信号;, m1 l. t: ?8 a$ ~2 V
" s3 W+ G0 J# q6 I2 }/ D* W
④ 在接下来的8个时间周期里,如果是写操作,则主机控制SDA的高低;如果是读操作,则从机控制SDA的高低;
f; s% L7 @/ x8 E: }6 i. t; e" B Y+ r u s
⑤ 每次传输完成,接收数据的设备,都发出应答信号;1 K! a. E) G! R1 R+ Q
/ b5 V+ j0 N/ H& W, q: X& A, }
⑥ 最后,在SCL为高时,主机由低拉高SDA,表示停止信号,整个传输结束;. j+ B2 N5 y A( `; c3 @
& r1 Q. i1 Z- d
( B! z+ M+ J& h8 d; @
2 R5 n; @- v$ c# F0 y% q
! f+ G8 Z0 c) E9 T图 16.1.5 I2C传输时序 / T W9 W- i! a
* i* O! n6 ^+ M
! m# h F: V8 O$ z( m) N- U
16.1.2 EEPROM介绍% @. o6 O+ e Y
8 [1 o( Y& B# @) P- e$ x
EEPROM的全称是“电可擦除可编程只读存储器”,即Electrically Erasable Programmable Read-Only Memory。通常用于存放用户配置信息数据,比如在开发板首次运行时,需要屏幕校准,校准后的配置信息就可以保存在EEPROM里,开发板断电后配置信息不丢失,下次启动,开发板自动读取EEPROM的校准配置信息,就不需要重新校准。+ \+ Y, T* ], ~ i$ Z+ {
/ h! L. M% Q! T) e& i' j# z3 l3 }/ \
EEPROM和Flash的本质上是一样的,Flash包括MCU内部的Flash和外部扩展的Flash,本开发板就有一个SPI接口的外部Flash(W25Q64),在后面SPI接口再讲解。从功能上,Flash通常存放运行代码,运行过程中不会修改,而EEPROM存放用户数据,可能会反复修改。从结构上,Flash按扇区操作,EEPROM通常按字节操作。两者区别这里不再过多赘述,读者理解EEPROM在嵌入式中扮演的角色即可。7 ]% k4 ^* V3 J2 L
2 F9 L* w$ D1 `" t% K7 p9 B1 u' {
' u; J" h6 p' ~% o Q( e1 X4 @2 f! S5 S" Q4 v
结构组成7 ^( k7 |7 v/ r* R2 |
4 k6 S& w- n1 n# KEEPROM类型众多,其中比较常见是AT24Cxx系列,从命名上看,AT24Cxx中xx的单位是K Bit,如AT24C08,其存储容量为8K Bit。本开发板上的EEPROM型号为AT24C02,其存储容量为2K Bit,2*1024=2048 Bit。
% `! _, Q4 c. O6 p" w$ W) g/ M. u3 S6 }8 G4 ]/ I* A5 |, \
对于AT24C01/02,每页大小为8 Byte,对于AT24C04/08/16,每页大小为16 Byte。如图 16.1.6 所示,AT24C02由32页(Page)组成,每一页由8个字节(Byte)组成,每个Byte由8位(Bit)组成,Bit为最小存储单位,存放1个0或1。
: f% |0 R1 h- y
) J; h7 t( Q& o, K4 o
5 q/ y, G# y5 g- _+ o5 }
* K% A5 Y# h. [5 }9 F. K图 16.1.6 AT24C02结构示意图
3 O% B' X2 |3 Q8 j3 R4 ]8 Q \; }( A8 j3 x0 V; t5 B5 _
% W- d2 ?8 |7 h+ ]
设备地址 % L$ A3 O+ j/ o8 `2 l h
, h% w' [8 ^$ _1 j& CI²C设备都会有一个设备地址,不同容量的AT24C02,设备地址定义会有所差异,由芯片数据手册《AT24Cxx.pdf》可知,如图 16.1.7 所示。
" H( |# T2 t2 i# M" ?3 y- f# `6 n. L+ I2 J: d8 N. O' r; H
& n( A! u. e4 {1 E) M4 V
9 y/ [+ f6 N; b- {" B3 r图 16.1.7 AT24Cxx设备地址定义 $ G$ s# [# }: ^7 b- _6 u# u
8 v* z" w" ^0 L5 v
/ }1 _0 S/ P# P) u) c3 q
AT24C02的容量为2K,对应上图中的第一行,高四位固定为“1010”,中间三位由A2、A1、A0引脚的电平决定,比如A2~0引脚全接地,则值为“000”,最后的最低位为读写位,0代表写命令,1代表读命令。
" A1 Y9 X7 ]+ i. Q I% W+ B* {5 k
4 v- J. M) d! ~9 x6 d f/ mA2、A1、A0引脚电平需要由原理图决定,假设全接电源地,则如果需要向AT24C02写数据,则发送地址“1010 0000”,如果需要向AT24C02读数据,则发送地址“1010 0001”。1 q) k G, I- s0 j6 o" ~4 F
P5 X! J! d+ b; J( w
假设开发板有多个AT24C02挂在同一I²C总线上,通过这个规则,只需设计电路时,让A2、A1、A0引脚电平不同,即可区分两个AT24C02。, B( y+ y! Y8 L8 }( U5 W
6 T' M, F0 |# J; E# g/ @1 \2 C( f
对于容量再大一点的AT24Cxx系列,比如AT24C04,器件地址由A2、A1引脚决定,数据空间有P0决定。比如对AT24C04的0~2K空间操作,则P0为0,对2K~4K空间操作,则P0为1。
2 P% m+ [ R, Z' v8 P
1 G \+ D4 q2 s3 W0 n' Z( Q5 M( b4 u/ v/ z+ I4 W0 c/ p
# V5 s+ y. b9 i) a$ t0 {
写AT24Cxx
4 u* ~2 o- f% `, b3 Y
( O( D: b6 N1 h) y6 xAT24Cxx支持字节写模式和页写模式。字节写模式是一个地址一个数据的写;页写模式是连续写数据,一个地址多个数据的写,但是页写模式不能自动跨页,如果超出一页长度,超出的数据会覆盖原先写入的数据。/ U$ F4 \; G: z4 K: `7 `5 _
6 s! m" {2 f' y3 W; d
如图 16.1.8 所示,为AT24Cxx字节写模式的时序,在MCU发出开始信号(Start)后,发出8 Bit的设备地址信息(图中读写位为低电平,即写数据),待收到AT24Cxx应答信号后,再发出要写的数据地址,再次等待AT24Cxx应答,最后发出8 Bit数据写数据,待AT24Cxx应答后,发出停止信号(Stop),完成一次单字节写数据。8 `( ]9 b. \' G2 A
* n; P( O( g% `# u- M5 X. b( D5 U- S
+ D; h& z. T* i1 ~8 B# i! ^" X# _- }
图 16.1.8 AT24Cxx字节写模式时序
+ b2 s1 r0 W/ m9 cAT24C02容量为2K,因此数据地址范围为0x00~0xFF,即0~255,每个数据地址每次写1Byte,即8bit,也就刚好256*8=2048Bit。对于1K容量的产品,数据地址范围为0x00~0x7F,最高位不会用到,因此图中数据地址的最高位为“*”,意思是对于1K容量的产品,该位无需关心。) W- Z2 i1 F: y/ M6 e% ^6 F: T0 K
q5 w2 s9 W% V4 I7 A
3 c) L% n1 {7 Q" @0 B
, y& _9 t) {# O$ t! Y0 Z: [
图 16.1.9 单字节写模流程图 8 S' U0 H8 `4 ^& r
图 16.1.10 为AT24Cxx的页写模式时序,与字节写模式的差异在于,不是只发送1Byte数据,而是任意多个。需要注意,该模式不能跨页写,遇到跨页时,需要重新发送完整的时序。
9 x; `3 ~; n% Y7 H S3 ^5 Y
% E1 v( m% _8 u" x2 ]
/ V# k+ D$ R8 Z! K( { j" V" R0 r& } j
图 16.1.10 AT24Cxx页写模式时序 # R0 |" w+ I$ m& b& N* i
值得一提的是,《AT24Cxx.pdf》里提到每次写完之后,再到下次写之前,需要间隔5ms时间,以确保上次写操作在芯片内部完成,如图 16.1.11 所示。
; T5 p& P' {3 v' D# v; o, {, {* h$ F. G/ g. `+ k @
( J' w' |% `0 b) V- c: G5 I& l
9 n5 ^3 f7 o. y! x
图 16.1.11 AT24Cxx写间隔 & C4 A$ `2 N! \$ }- A: r1 R; r' t7 F( \, `
" [7 `, z4 R. K6 R+ |1 J, @, ]1 f% h4 l
读AT24Cxx
: P5 U8 M" S0 i: W9 w" |0 _- @1 c c, K+ Z- Z) N/ T
AT24Cxx支持当前地址读模式、随机地址读模式和顺序读模式。当前地址读模式就是在上一次读/写操作之后的最后位置,继续读出数据,比如上次读/写在地址n,接下来可以直接从n+1处读出数据;随机地址读模式是指定数据地址,然后读出数据;顺序读模式是连续读出多个数据。
9 q. X6 p1 u. _1 V& r2 j
/ G9 K, M" ?& N E& y: t% f在当前地址读模式下,无需发送数据地址,数据地址为上一次读/写操作之后的位置,时序如图 16.1.12 所示,注意在结尾,主机接收数据后,无需产生应答信号。/ ~+ o3 b3 S. P p5 u
( }$ x& g% R" E3 X8 b
: X& ?. d O" w9 y; [: H) v, P, i! s# f' D3 {! s" M
图 16.1.12 AT24Cxx当前地址读模式
' n6 T5 R7 A7 n w. j5 z+ w- F$ h在随机地址读模式下,需要先发送设备地址,待读的数据地址,接着再重新发出开始信号,设备地址,读出数据,时序如图 16.1.13 所示。
! ^4 D4 u: d! E' ^3 a7 R" O
& g" l1 s) ], c! Q* ]; Y v U6 P" W7 `% h9 D; l) ]& n
/ W( f: H1 s4 M: t, N. ~
图 16.1.13 AT24Cxx随机地址读模式 " E2 M& ~+ K5 r# D: Q% K. f
( r- i# \: Z+ |2 q/ g
0 R' C/ E% P; f* h1 L
在顺序读模式下,需要先从当前地址读模式或随机地址读模式启动,随后便可连续读多个数据,时序如图 16.1.14 所示+ f d% Q/ ^3 i9 b
2 q0 }# L6 w3 i: I; m
5 V1 G+ o0 }, K3 O
" K* g9 L1 E% n图 16.1.14 AT24Cxx顺序读模式 . N- ] }; M! r1 g/ b
' B% |4 H9 V; N s _) s; M
5 D) C0 Q1 N$ E. I
' O5 j) s# b5 d4 O3 h0 y: t1 [; |
16.2 硬件设计3 Z* w: {) w* X' I
如图 16.2.1 为开发板EEPROM部分的原理图,U6为AT24C02芯片,它的A0、A1、A2都接地,因此该设备地址为“1010 000X”,当读该设备时,X为1,写该设备时,X为0。/ R$ k$ Z9 i7 V7 M
; X2 e8 p" _# l; b) D2 m* N+ E9 `3 RU4的7脚为写保护引脚(Write Protect,WP),当该引脚为高,则禁止写AT24C02,这里直接拉低WP,任何时候都可直接写AT24C02。
: Y: {; X6 D# n) p8 \
7 m& o- W, o$ x. P- ?此外,I2C的两个脚SCL和SDA都进行了上拉处理,从而保证I2C总线空闲时,两根线都必须为高电平。如果没有上拉,在主机发送完数据后,放开SDA,此时SDA的电平状态不确定,可能为高,也可能为低,无法确定是从机拉低给出应答信号。: L5 k2 ~5 s2 `) O% H
& o4 |% d6 m2 M3 t/ X
结合原理图可知,PB6作为了I2C1的SCL,PB7作为了I2C1的SDA。& E* z! S- g+ g9 Y4 h# L
' C; r& _9 U8 B" ?) U2 ]- ^4 e5 E
, A l2 Z: t4 a$ D0 c: \2 Z8 u
2 {) F/ r0 t# _( [图 16.2.1 EEPROM模块原理图
2 w, J. K$ Q, v$ m0 G
/ g$ d& d9 a# k3 t1 K3 l" i! Z7 {+ n u8 _7 {9 G( S/ Y& Q6 E! t* W
16.3 软件设计
8 d( g* s r' f7 r# P16.3.1 软件设计思路- ]) \8 @% n9 z5 r; {: |+ L4 T; f
: V5 z3 q& P# A& M: n4 ?
实验目的:本实验通过GPIO模拟I2C总线时序,对EEPROM设备AT24C02进行读写操作。& v' a. P3 a1 z# Y* J; u
" h" d9 R) E; W6 o" \1) 引脚初始化:GPIO端口时钟使能、GPIO引脚设置为输入/输出模式(PB6、PB7);
7 v+ J9 \! V7 ?6 E1 V9 @
Y! } g/ g) h; f2 c" [( ?2) 封装I2C每个环节的时序函数:起始信号、响应信号、读写数据、停止信号;
5 X7 [3 o' ]5 |. r3 t v1 S+ D( h% h8 k9 R" h2 {" I
3) 使用I2C协议函数,实现对AT24C02的读写;4 ~6 j* `. x8 l4 d/ T# S, E
0 W0 e' t% y2 M- k4 m
4) 主函数,每按一次按键,写一次AT24C02,接着读出来验证是否和写的数据一致;
. {0 [5 P6 @3 s% m4 @2 E& ~$ S; p( h1 W/ r( {0 z
本实验配套代码位于“5_程序源码\8_通信—模拟I2C\”。- }# |7 M# o: |/ ?) X
. E. T, x4 u2 R7 _* k* J3 ^3 n& k6 y4 c& `0 z' P5 [8 A: r
8 m& r9 ^9 D% _( h3 R9 H
16.3.2 软件设计讲解2 a& U! Y6 {% s2 U8 D) [ J
" Q v3 X5 u- \- L' e
1) GPIO选择与接口定义2 |* I$ U p. g. J! U' O, p5 e6 D
; F& ?( i% T: E9 [, k0 d( c. d
首先定义SCL和SDA引脚,引脚的高低电平宏定义,如代码段 16.3.1 所示。3 G9 a5 l& v% k9 e6 g5 S
" c& J; n% f1 S# C' _3 Z, W/ V) g代码段 16.3.1 模拟I2C引脚相关定义(driver_i2c.h)
; x0 i% G9 q' {* b1 R% H- U5 g/ P6 a2 E& b
- /************************* I2C 硬件相关定义 *************************/. Q' y& r; J0 o) e# O+ G& ?- V+ f
* h, T- [6 h8 u# q) _- L' U( c- #define ACK (0)7 w6 W5 E# T* \8 c
9 _4 j: Q. m- c! i; v3 j+ O! O+ X- #define NACK (1)2 K. j8 S0 W ]# X1 u! _) L0 X
- 0 c0 [# z" U" z0 y, ?# K
' `6 C$ V1 d& C2 ?# J* l* \
/ ]; S h* p. n: y. _- #define SCL_PIN GPIO_PIN_6
5 Y) q' L- n, J1 F( o
" D+ v$ M' I& _9 Y- #define SCL_PORT GPIOB
1 U5 m8 q% X% R: j. K - 1 ^2 q0 R. L9 S9 \" l! ~8 ~
- #define SCL_PIN_CLK_EN() __HAL_RCC_GPIOB_CLK_ENABLE()
* R( e4 n* U8 _6 y - + q: g5 j" x% g) ]% o
2 G, i) E' S( {1 e- - l E% P8 t. H! g8 x
- #define SDA_PIN GPIO_PIN_7" k2 L- f& N, s U8 F
- . T0 l* @6 Q' w& I! ~+ V, R
- #define SDA_PORT GPIOB$ Z$ b- B8 Y2 P) L* E
4 ]6 v. R; y* d/ k- B* P- #define SDA_PIN_CLK_EN() __HAL_RCC_GPIOB_CLK_ENABLE()# P) q" _# x2 s& P- c
- 8 i( m+ }( ~' r! s
- ) L; `, i$ }% Z& t, {4 C! G
* D0 Y* \) L( }- #define SCL_H() HAL_GPIO_WritePin(SCL_PORT, SCL_PIN, GPIO_PIN_SET). {8 l% W8 m3 }' @" v, I& m
- ; h+ c8 u2 |* ^# G
- #define SCL_L() HAL_GPIO_WritePin(SCL_PORT, SCL_PIN, GPIO_PIN_RESET)
7 f8 i4 N/ P/ B# K
3 n, g5 D5 y2 s) q$ c- #define SCL_INPUT() HAL_GPIO_ReadPin(SCL_PORT, SCL_PIN)" \* \8 f& a1 H
- . b' F, m; ^5 [( G: f7 F
- i) e6 {' ~7 ~% \& b1 O& i' |# N: d
- - v9 T* B9 I5 f# K: L! R
- #define SDA_H() HAL_GPIO_WritePin(SDA_PORT, SDA_PIN, GPIO_PIN_SET) v8 R% Q7 @% \3 T) ?
- , n3 X" t% ~9 g- g
- #define SDA_L() HAL_GPIO_WritePin(SDA_PORT, SDA_PIN, GPIO_PIN_RESET)0 A* v5 M {" A N; [
- e# w U: D$ G8 ^$ R) `! I& o: Y- }& \- #define SDA_INPUT() HAL_GPIO_ReadPin(SDA_PORT, SDA_PIN)
复制代码
- Z2 ^% v8 G- @# {% R接着将两个GPIO引脚初始化,使能引脚时钟,先默认设置为输出模式。SCL引脚为时钟信号,始终为输出模式,SDA引脚为数据引脚,可能输出或者输入,因此还需要编写函数实现输入、输出的切换,如代码段 16.3.2 所示。0 R ]; q9 q) Y( o; {2 F0 c
# ]! K1 f8 D" j
代码段 16.3.2 I2C引脚初始化(driver_i2c.c)3 x& J5 C5 G( m5 a6 [
9 j, Z% ^) ~# r( V+ f0 W8 _) V4 K- /*8 E- @2 A9 k4 g( Z4 Q
- * 函数名:void I2C_Init(void)! r/ U+ W" _8 E
- * 输入参数:
! P7 @4 D8 ~/ M - * 输出参数:无$ {& q/ ?4 R! Z: e7 \
- * 返回值:无8 F+ ]* V% F& Z2 x
- * 函数作用:初始化模拟I2C的引脚为输出状态且SCL/SDA都初始为高电平% s2 X8 k- x; b$ e
- */
) v% d! `* n2 q A - void I2C_Init(void)$ l+ N# S* b1 ~* G* P7 V! V
- {3 u+ |5 _* j, r7 J2 u7 Y0 G
- GPIO_InitTypeDef GPIO_InitStruct = {0};
0 [% x- J4 W. ]) ` - ' i2 g% {! R, o9 P
- SCL_PIN_CLK_EN();
0 H2 }) ]3 S8 | - SDA_PIN_CLK_EN();1 W( D! g m& b& s' X
! D; M- b+ _5 g% ^0 p; Y- GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
$ l R" p# Q1 ^* g9 r# F - GPIO_InitStruct.Pull = GPIO_NOPULL;
5 R+ t" x6 |+ }! ^( C - GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;* o1 o7 @9 j, U
- 2 C9 e, ?/ m' R7 _/ g! y% S6 q
- GPIO_InitStruct.Pin = SCL_PIN;
" { _9 o- W3 p+ z5 J( b - HAL_GPIO_Init(SCL_PORT, &GPIO_InitStruct);
8 B$ c9 N( K3 v% ~ D
. B r. d. ]% |3 J7 Y& ~4 V- GPIO_InitStruct.Pin = SDA_PIN;
) f! V, d7 S2 g" t; b' ] - HAL_GPIO_Init(SDA_PORT, &GPIO_InitStruct);
0 y1 \# \" h \: B# e' l2 r$ K - ) h6 T# d% a/ R6 y& V/ z5 w0 f+ B
- SCL_H();
3 W$ ?1 ?5 }3 L# M" j - SDA_H();
# G; f( e9 U% `: a5 n9 s - }
+ O% q8 j8 j6 B! r! T - ; ]7 E- l8 D6 O8 E6 M3 q1 x' w" G
- /*+ O! F: W0 A4 X% i
- * 函数名:static void I2C_SDA_OUT(void)
8 K$ H8 v% }( k, H6 i - * 输入参数:
6 v* S5 @! w9 L: U3 |3 @$ B - * 输出参数:无
. i: P$ g1 Y" z* h- I - * 返回值:无" s8 c( j, S3 Y- o" ]1 w
- * 函数作用:配置SDA引脚为输出
! g1 d$ u5 v( d& B) W: P - */
% W8 |6 A4 u2 j q5 Z - static void I2C_SDA_OUT(void). F- e3 q3 B+ p6 \3 o
- {
0 Z/ a5 ~1 _$ D, P" Q - GPIO_InitTypeDef GPIO_InitStruct = {0};
0 J& a+ g9 A( y; ?6 }- Y
0 h: y+ d' S% o2 [& Q% H- GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;" D7 P, b4 t( _1 }
- GPIO_InitStruct.Pull = GPIO_PULLUP;( l; Z8 h8 E2 l* M" e c( P: |. j
- GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;" L! i, q8 {/ Z$ ~! L) O' c2 o' `0 L
; }9 M0 q& P+ O8 m* t6 Q4 ~- GPIO_InitStruct.Pin = SDA_PIN;9 \6 V& E: F* S
- HAL_GPIO_Init(SDA_PORT, &GPIO_InitStruct);5 Q* H" P4 @) W# j
- }
4 `0 l1 P) ?4 `1 q! c- R
* s8 T7 ]9 z5 T& `8 o! ~ t* i- /*1 |0 ~: H+ _# R' |+ z+ Z. l. H4 y; I( y
- * 函数名:static void I2C_SDA_IN(void)
' w$ F1 T' z0 { M! M) e5 Z8 A - * 输入参数:3 W! T. a9 N: b& F3 D
- * 输出参数:无
/ b, B1 ~. J& ^. r5 N1 l' e - * 返回值:无
8 l: J& l2 B& F% o; U, k - * 函数作用:配置SDA引脚为输入4 u9 i) v0 R4 ]- Z4 v, i4 T! ^
- */5 C+ K' }2 o5 H; a& d
- static void I2C_SDA_IN(void)
1 _1 W- ~0 j# O# Y - {
- S( ]" R" ?/ Z5 L" @ - GPIO_InitTypeDef GPIO_InitStruct = {0};* L6 d; z* r( Z: n, _
- a) q; |/ l" f# }5 }$ @
- GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
" S3 L4 j& q7 J - GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;3 Z: X) n0 z6 Z9 u+ f, H$ P
- - L% J4 E6 v4 m, V
- GPIO_InitStruct.Pin = SDA_PIN;
' T) ]4 L6 ?( P3 l - HAL_GPIO_Init(SDA_PORT, &GPIO_InitStruct);
4 ~% b2 R" B& q2 W( ~& f0 s/ i - }
复制代码
' N' N9 R4 {% S5 C7 x' |7 B Q# O1 z/ l! J
2) I2C时序函数
' k% `8 @* H& _9 F/ ^6 V( L6 e* {% \' H/ u3 R4 e
开始信号/结束信号
' g4 m |& x7 H" Z/ D. |1 @/ Q4 M9 a7 E# T( |
参考前面图 16.1.3 所示的开始信号和结束信号编写程序。对于开始信号,首先将SDA和SCL都拉高,随后SDA拉低,再SCL拉低。对于结束信号,首先拉低SDA,拉高SCL,再拉低SDA,代码如代码段 16.3.3 所示。
% i# B6 O9 t9 A+ @& k# {2 E/ Y( s$ `, V- y$ G* f
代码段 16.3.3 I2C开始信号和结束信号(driver_i2c.c)% H g$ ]' [# s' q6 R
0 H* P! A4 s% P9 J# q$ p. C- /*+ r* F6 c) ?: F b
- * 函数名:void I2C_Start(void)3 R0 V* t, w4 y
- * 输入参数:
! j# T# T1 z6 S1 R - * 输出参数:无/ i: @7 y/ `. _8 y. U9 b5 Z
- * 返回值:无
' T5 K9 I5 Q4 n }( s7 c0 Q. l2 [ - * 函数作用:I2C开始信号9 V3 ^, n. T( v2 J+ Q0 ~; R$ s9 I
- */7 r" `' w1 g* r u
- void I2C_Start(void)
; k5 K, m% y( A/ J3 J! E - {
/ w$ k, U. y7 e! N - I2C_SDA_OUT();
( f7 |( N# p/ I" R9 n" S
0 H: f- Z9 d! B" @: J- SCL_H();6 K, t) a- O& t L; k0 \* T! H
- I2C_Delay();3 _+ z" r5 E; q, Z j9 U" e; v
- 5 n( Q* ] x+ t0 s: O; h3 g
- SDA_H();6 o* Q! I# v3 U* l# {/ j' Z# j
- I2C_Delay();
! Q) @6 b4 @ f3 \! U1 l - 8 T$ ~$ D+ x* S
- SDA_L();
# r; B3 q5 w T! ~ - I2C_Delay();
! H) a+ `$ M! Q' I
3 o0 Z6 E4 P( k" g( i- SCL_L();2 Y) O: z6 k, N% ~4 `( a( e/ w
- I2C_Delay();
7 ?$ Z' n9 t5 g; G) O - }
8 i6 c5 F# Y0 T/ F, M
/ D% w7 B% \0 Z* g* P8 H- /*8 A( K+ M/ u/ g
- * 函数名:void I2C_Stop(void)
Q6 M# I I9 A - * 输入参数:/ W- L. |, z2 X: z6 w6 B+ ~
- * 输出参数:无
: S/ Y1 R8 s, v. i2 v - * 返回值:无3 l# X' o# L5 w6 @/ }0 |
- * 函数作用:I2C停止信号3 n( l8 _! X4 C3 s: X
- */
) O' w. G& B. s" V, g J, w - void I2C_Stop(void)
0 v7 d# N2 o7 x5 l - {
. c* K$ t& J1 R& \ - I2C_SDA_OUT();0 g# B- l/ y9 R, I8 i
- ' Y* n7 _% p' v5 T+ w9 Y
- SDA_L();
3 k q0 @( c. t* l$ m* K$ T9 _- N - I2C_Delay();
+ G$ ?2 J4 O' W - + q3 X5 i" U! }7 a7 B2 d0 o
- SCL_H();
4 n6 K3 ?* [+ N7 _$ E6 g - I2C_Delay();2 e6 i; e" j8 k G* F3 E% z
- 4 C3 j# A/ i; I6 d( G
- SDA_H();
1 o( S, W% K9 h7 }* z) ~; X - I2C_Delay();
. G ~3 c" F1 }- }% l - }
复制代码
: \5 c; F/ R4 ~; Z8 F W5 d, @: H$ P应答信号/非应答信号/等待应答信号; l3 ?! p5 i9 q6 j9 v" W
% n, ^0 X2 H, V1 Y参考前面图 16.1.4 所示,编译应答信号,如代码段 16.3.4 所示。
9 q8 \/ `. ~* n1 J
( M- Q: z4 T! h0 E) I: D( a代码段 16.3.4 应答/非应答/等待应答信号(driver_i2c.c)
4 N, X- J; I) L# q5 C- H" E7 t# y
- /*. |, }2 ^0 j9 _) g8 C
- * 函数名:void I2C_ACK(void)) S/ i& N/ ]2 u' O
- * 输入参数:
. G. k# J/ p3 X3 @ - * 输出参数:无
8 D/ d4 Z: R4 X# c {8 N+ b - * 返回值:无; `0 _, n0 y* Q" k
- * 函数作用:I2C发出应答信号0 p' Y4 P& B7 r4 M4 _
- */
0 ? X/ t H5 x) ^$ [ - void I2C_ACK(void)
: W4 I4 y* N5 z/ W4 ]) t - {
: M/ i$ x9 V9 Z; A: g* e - I2C_SDA_OUT(); [0 Z" l9 Y& J8 I2 y( g6 j% J
$ v* ~- C5 C: { N! t+ d2 O- SCL_L();7 f+ a) m4 v* H6 f1 G6 L( l
- I2C_Delay();
4 j" S4 o/ V# Y# H0 X' i
. r/ E) D! {" s" k1 ^! b! L- SDA_L();" w2 |: I# n4 S9 h) O
- I2C_Delay();
- J5 J# P$ W' I+ s: t" X
1 o- R6 ]$ S% C% K- SCL_H();4 Q F# r6 M; O% |+ W e3 z% X
- I2C_Delay();6 z8 Q1 a6 \8 q$ a+ ~* a
- & j( c8 ]' Y* z! T/ @
- SCL_L();; P9 }; O( F. @% g. U) P
- I2C_Delay();
1 N1 r; h7 t% I0 v3 Q/ C - }
k8 X" ~- R' x o6 J' M+ s - # c7 o9 b& d/ I: r/ V( O0 n
- /*
/ f5 k [% H6 @/ d0 N" a - * 函数名:void I2C_NACK(void)
' c# g8 G: n8 U, H# J) U - * 输入参数:
) U; Y3 B! F8 X: B3 k+ g - * 输出参数:无
% d' m0 s8 a# x+ P - * 返回值:无
# G5 T; h+ H8 @ G+ R' B - * 函数作用:I2C发出非应答信号+ ]( d d2 e. [* ~. Y2 J1 z& h
- */
1 R4 q2 A, z) s. J - void I2C_NACK(void), r+ W" u3 n; J; o
- {
$ C3 T; l) a1 i+ E8 F; a9 `" H - I2C_SDA_OUT();
$ j/ g0 M; u' r, Y$ C" J0 [9 f% o - 2 H; \* n0 c. Q9 \+ D
- SCL_L();
. V% {0 w8 n6 j: I+ J* q7 t - I2C_Delay();
! D+ q3 B. n3 Z - : ?5 x; q! a7 r
- SDA_H();
; w8 _! m+ N8 d ~8 E3 ?9 J! C - I2C_Delay(); `: ]$ I3 ?$ `
- . _5 I+ W+ K' w5 Z) L1 ^0 g8 M
- SCL_H();
+ g& Q4 l1 r- m - I2C_Delay();
* A4 U' M4 U9 J9 z/ `+ k3 ~; J1 A5 ~
% G$ i8 d2 ^0 L) U2 d( v- SCL_L();
' d7 N# @$ B* {* u - I2C_Delay();9 H A3 N3 @8 r, ]& u/ s
- }
- q5 T! O D+ F4 q/ A - ' G* C. z; W; u$ C+ f/ |* p
- /*9 L- ]. h( B" J
- * 函数名:uint8_t I2C_GetACK(void)
K6 Y- K( v, J) I! D* l( l - * 输入参数:# I# i Y$ |0 s- @
- * 输出参数:无+ {# a i( ~* o( f7 u4 d
- * 返回值:1无应答,0有应答
3 Y) ?5 a4 h h - * 函数作用:I2C等待从机的应答信号9 T/ a. {9 s5 }9 A. A$ C
- */6 a7 a0 k2 L7 g, h* |/ u$ z5 D) g
- uint8_t I2C_GetACK(void)
0 D. p7 C! ?" \0 R( q5 m; c - {7 g* ^. g! E" J2 {, ^% j. i
- uint8_t time = 0;
7 \: b0 y$ D8 @( ?. m: ? - I2C_SDA_IN();; z# F1 O% f* f- {" P. C6 f
- ' M8 e! G! Y$ Y4 P
- SCL_L();
: {# b- N/ D. _5 J - I2C_Delay();' {6 W6 B N4 J
- ! W G: v1 z) f( O! ?* v1 N
- SDA_H();
& D, E3 s; m% x: Y2 m - I2C_Delay();5 d1 Y3 b& b6 A! @9 F
- a# H0 _. m& ]6 ~; C* e- SCL_H();
9 h5 G5 F. u8 c) f% d: n) h - I2C_Delay();
. p) j& A1 O; m; g2 U0 ^
( q3 u5 K! C! i3 `! j+ A; z+ y2 c- while(SDA_INPUT())& C! ~' V; N6 w) W0 ^1 C3 D
- {) ^) p3 H3 U) b( b& n9 D; ?
- time++;; D$ {& R7 a( ~5 ]2 K
- if(time>250)) n( p+ U0 h# V( y
- {) Y. y' `/ P- j- I9 e
- SCL_L();
( I3 I" b/ }5 l5 D& v - return 1;
' V) u# l' j5 f6 V' \$ b' B - }+ I, n- b0 A1 M9 |
- }
% N* Q, k, g f" E! U8 I - SCL_L();
0 g: O8 w7 R+ g) L; N% n5 o- R8 U" a
+ P4 V) B' A" P8 c- return 0;
( S; G0 h- o! P6 R/ e% K7 y6 a% ^ - }
复制代码 % ]* `: g. b( J, W
8~23行:应答信号,在一个SDA时钟周期里,将SCL拉低;% t2 L& a' Z% {! e4 V- R( M- z8 _
- ~5 [. ?) t# a8 K! X! L5 Q( ^32~47行:非应答信号,在一个SDA时钟周期里,将SCL拉高;
3 ^' J; y. [# d2 q4 f- A' K% O# |; ^' |3 h. j
56~82行:等待应答信号,拉高SDA后放开SDA,读取SDA是否被拉低,如果拉低返回0,否则返回1;
9 f: U$ r9 B6 s4 V, }' G2 o2 T
7 V* t4 l6 O, j5 k$ D: a# n3 N. _/ O/ `3 g( u) x! _9 d1 G
9 ~0 v0 T; K4 k! W0 x' H发送/接收函数
) m2 o, K" l3 e4 L+ c B
) S8 F t; n$ R* l; z2 f* \最后还剩发送/接收函数,如代码段 16.3.5 所示。对于发送函数,控制SDA产生8个时钟周期,每个时钟周期里控制SDA高低电平发送1位数据。对于接收函数,控制SDA产生8个时钟周期,每个时钟周期里读取SDA高低电平接收1位数据。. }* ~8 }# Z( a. I" u1 p
' h. d2 R2 @+ |/ p" r代码段 16.3.5 发送/接收函数(driver_i2c.c)2 H/ p! m4 S: R0 d; S
* S+ q! B; z& l7 h1 o, R
% y! L4 m( Y0 k5 }' j. j
3 @, c+ }0 ]2 C, s, b- /*+ S8 ^( w6 E( Y8 _9 _/ k5 }4 X6 \$ d
- * 函数名:void I2C_SendByte(uint8_t data)
+ i# g% q; |% z7 _: x - * 输入参数:data->发送的数据
3 @0 a' b6 p, ^8 O - * 输出参数:无
1 W \ X( a t* l - * 返回值:无
# I" X- C, h! _- _! ?2 a O - * 函数作用:I2C发送一个字节, f& D! I4 S; k8 ?3 s# G
- */
4 I8 Y- A% d5 J5 D - void I2C_SendByte(uint8_t data)3 ^- W: @1 U' j* I
- {
& {' C# b8 R. P1 F5 {; L6 c7 P, h - uint8_t cnt = 0;
2 _0 L. F6 L5 u0 A+ C, G3 z$ F- f - 2 d* ^; `1 b- ~
- I2C_SDA_OUT();: M9 v% Z" ^8 o( S
+ e& r: e K2 i" e4 Z& s, s( z- for(cnt=0; cnt<8; cnt++)
8 S- Z' N6 V. ?7 s - {+ z- L/ l [* _
- SCL_L();2 U# r/ \; L, K v6 B3 A; }
- I2C_Delay();
6 ~; V* p& g3 k0 A" S$ A1 n - 9 w0 ~4 [8 G; \1 Q" c( l' e0 ?
- if(data & 0x80)7 [' O: j0 R( b8 R4 r+ Z
- {2 n; u/ _1 s, g) ?* M
- SDA_H();
0 Y3 j3 I. {3 { - }" a3 n) a8 j! W7 f3 r# W, h3 i
- else( s; @) T$ J s3 ], N
- {: z! ~5 ?& t4 j1 v( p* U
- SDA_L();
" {+ \: O. D1 j! S - }
1 t$ P/ r$ q8 W1 s' h7 q% R3 j - data = data<<1;9 D3 @$ l; y0 {/ i$ }
- SCL_H();5 x$ @: z3 b# H9 X" l* O0 `* C
- I2C_Delay();
7 ~# B o" j5 N9 U% a; p( m$ m5 h - }: M2 d# ?# R: F* j( u
0 `; p6 V$ u# A/ C! @) \- SCL_L();
1 ?" V2 F# z* J) P$ ~" l) ~ - I2C_Delay();( i0 B1 o v+ o$ u; Q
- I2C_GetACK();
* n t4 I1 e; m- `( g! u, | - }
- a% [9 G6 }/ d8 n3 u) \& \& |
O* H7 l: u7 J; f% V7 B" D- /*1 O+ s1 e& A1 _1 u; a* x
- * 函数名:uint8_t I2C_ReadByte(uint8_t ack)
1 L Y# z5 K2 c" K: B: O - * 输入参数:ack->发送的应答标志,1应答,0非应答
/ B, A W0 X* Q9 X0 b$ N$ | - * 输出参数:无
" ]3 c% J& Y- q3 ]7 ]# y. k - * 返回值:返回读到的字节" K0 E/ M/ K G, K; R+ N
- * 函数作用:I2C读出一个字节& L) ~% |( A$ ~3 N
- */
/ P9 k: I" p H: g0 m" r; J - uint8_t I2C_ReadByte(uint8_t ack)7 p) E: R% h9 q
- {
6 H$ ?$ N5 c3 E! v' k - uint8_t cnt;
4 _. U5 G' z$ i6 u" y - uint8_t data = 0xFF;
% O/ ?. m8 w' m, X# i
+ b) {3 P+ k( h3 q- SCL_L();
# e" Y+ R5 x+ o - I2C_Delay();% Y; Q% N7 b4 G0 E/ m" m& Q/ @- V3 \
8 i' X7 q& b% r; `- for(cnt=0; cnt<8; cnt++), k( b* U% m* ?
- {. r8 L- Y7 C: p1 U6 h9 |5 f
- SCL_H(); //SCL高(读取数据)1 g. ]4 q3 f& I( Q( F, f$ }- r" d( a
- I2C_Delay();* h) s. a1 |- w0 H" ?5 X
. Y) h8 g; s; X+ J. o- data <<= 1;: k) F3 v* H: X1 _# l, I( E1 X3 L5 f
- if(SDA_INPUT())
0 z7 t1 u6 |! P1 `6 Z) H% t - {
, ?# N3 ~9 I# z% N0 e& q- L% _6 h _ - data |= 0x01; //SDA高(数据为1)0 z' E q3 N, V0 ]& B
- }
% w* T1 z7 _& M0 D* q( m- e; _8 w - SCL_L(); |: K" ?+ |1 g/ A. L3 L5 g
- I2C_Delay();
/ E: g9 `8 v1 q% q; s7 O: `( i - }
# o+ i( ~/ H. p- P - //发送应答信号,为低代表应答,高代表非应答
9 b4 z# F0 ~2 a- S7 } - if(ack == 0)
$ T- {/ m9 l3 m, y5 |+ t$ w - {( h+ p4 T2 a) {& w
- I2C_ACK();4 D& W3 H4 z# K R
- }
/ Z& N( H; V; M- j+ ], v - else
; b- _2 u; ]# y. F9 t - {( h/ q. F9 n7 e$ R; f0 N R
- I2C_NACK();) A6 | e/ o' B
- }8 m+ Y* @/ h3 w7 e. }$ t
- return data; //返回数据
3 W; w' d5 a9 V6 S* u1 R9 o0 v - }
复制代码 + `- I. x3 X/ g. P7 f
14~31行:循环8次,每次循环:- `" x9 v! ]: D( H& k. L+ i5 ^
" n: |; v2 a8 r' H8 B/ C' i
16行:先拉低SCL;
. e T4 s$ q; y1 q# z# J( H0 v3 [. i, t. X5 X5 p
19~26行:将输入的数据data与0x08且运算,得到最高位的值,从而控制SDA输出对应的高、低电平;7 P. \' ?, E N6 i5 \6 i) Y
+ g) V% W2 C/ k9 m( w9 {
27行:将data左移一位,得到次高位;
5 I) g' z2 r+ K
* ^( B6 |1 V3 I* G+ B+ e9 h29行:拉高SCL,让SDA处于稳定期,从设备即可获取SDA的值;
5 Y% X- L# ~2 h1 |* W' X* F, E: b/ v9 `! ~" Y3 @) i9 t: Z8 q! h
35行:等待从设备的应答信号;1 y4 c1 b' f: a0 u
! C3 u. [! V- L5 s! t
53~65行:循环8次,每次循环:
( K% C3 P* ^$ _! U( K2 r/ D
% |+ S1 J |0 s7 x+ |9 Z/ I) L55行:先拉高SCL,此时认为从设备控制SDA电平,处于稳定期;3 S2 U8 k* I! t/ F: C( J
! G- k0 E5 \3 a
58行:将data左移1位,以确保收到数据按最高位在前存放;. P2 d" C5 c: i( X2 U3 d2 o; |+ J
+ k+ l2 x) ~7 t! |5 ]5 W+ u
59~62行:读取SDA电平,如果为高,保存到data当前最低位,否则data最低位默认为0;
" `3 e! i8 b7 R% W$ ?4 w" B, N* c5 Q" `. j; Z
63行:SCL拉低,此时从设备继续控制SDA电平变化. K' Y' `$ \) i6 D; w, c: w
1 o6 ^* k/ J/ l0 g( {, i# W
66~74行:根据传入的参数,决定是否发送应答信号;- j1 ?" R/ I+ }* j
, P+ U/ P6 E) d/ k# `) K4 V
" H: E* E$ ]6 m' M
1 {- A0 u/ y( Y n5 e6 F9 s4 D整个I2C协议函数中,经常用到“ I2C_Delay()”来实现SCL时钟周期。对于AT24Cxx,由其芯片手册可知,时钟脉冲宽度(Clock Pulse Width)需要大于5us,也就是SCL如果刚变为高电平,需要等待至少5us才能变为低电平,因此定义“ I2C_Delay()”为5us以上即可。
$ k. N# s/ Y/ }: T1 x/ e& |* z& H1 Z! |
- #define I2C_Delay() us_timer_delay(5) // Clock Pulse Width >5us
复制代码
4 e* j! f5 |7 G5 D7 R6 I$ l这里的“us_timer_delay()”可以由定时器提供,也可以使用循环提供,前者精度更高,效果更好。定时器的介绍在后面章节,本章不作分析,延时函数的两者方式如代码段 16.3.6 所示。
) U5 Q: w' E/ i) {" ~0 G8 r5 k6 o+ ~! R% I; J5 _7 D
代码段 16.3.6 延时函数的实现(driver_timer.c)5 e* x7 N3 s& u
" Y _3 ]1 J: ~
- #if 08 _& }, i$ Q5 K9 L9 n5 K; E
- /*& K, F) u% {* O( n% f$ L; G
- * 函数名:void us_timer_delay(uint16_t t)" U+ t s) U8 m8 M' I% N
- * 输入参数:t-延时时间us, n" e5 e0 q1 L8 K7 W$ h3 X( h
- * 输出参数:无
9 x, s& Z* W" t' L - * 返回值:无
, @4 K' u3 {5 G$ B0 x5 X6 g - * 函数作用:定时器实现的延时函数,延时时间为t us,为了缩短时间,函数体使用寄存器操作,用户可对照手册查看每个寄存器每一位的意义
. B- A8 [, N% c! Z! s8 }' u - */
/ o1 k7 @$ s6 C, _& @5 w - void us_timer_delay(uint16_t t): x" u+ _! C6 U* C( d0 E
- { n5 k I& C1 c
- uint16_t counter = 0;3 y1 y; w! i1 V. I' Q
- __HAL_TIM_SET_AUTORELOAD(&htim, t);+ z- c1 L- p0 D' ~
- __HAL_TIM_SET_COUNTER(&htim, counter);
/ Z/ q" L# r% v- h2 f - HAL_TIM_Base_Start(&htim);6 H; X# J8 X) d* t; \$ k4 y
- while(counter != t)
+ g* O" q# ]3 L2 e - {
A$ c- v$ v8 G - counter = __HAL_TIM_GET_COUNTER(&htim);0 C4 N+ L9 s$ l! B
- }
5 j: E$ K8 x9 _& Q6 b( d - HAL_TIM_Base_Stop(&htim);0 |. s) u& q0 ~7 D# x+ ]7 ]
- }' r- k/ | x# C# l P
- #else
/ i2 ]( T, S8 B4 M2 \6 C! L - /*
3 a9 p7 s4 j' Y& G) ?6 X' x- m - * 函数名:void us_timer_delay(uint16_t t)
) `8 w7 x- F8 U2 |5 C - * 输入参数:t-延时时间us H% y, Z* a: c/ R% R, Y
- * 输出参数:无# m3 S5 y* S0 K/ e$ {3 C5 w/ u
- * 返回值:无
6 m$ I4 i2 P% S; t: u - * 函数作用:延时粗略实现的延时函数,延时时间为t us
0 v$ A1 d! H1 o* a6 d4 @0 ] - */
8 R8 E9 v* r7 ]; }7 E! G' u - void us_timer_delay(uint16_t t)6 o! W/ O7 U' e0 Z2 _5 c
- {
4 m' h# N7 W t# y8 d - uint16_t counter = 0;4 f( ~2 g; V" X/ ~1 E
+ t& R( c9 O) N& ]$ t5 I- while(t--)
# ~' r, p' ?6 Z V3 ^# Z - {
8 l1 H& p. t/ N( S0 ?/ l - counter=10;/ i* x! p8 ]$ a: y" |7 \% y* k: a( G
- 4 _, J1 K! K, Z' e# M8 |* `
- while(counter--) ;- _0 {6 [( B# t) t' G
- }
9 D; T/ U8 O$ ^& O& n8 i$ P5 D# D% f - }
! }) C$ b. z& U" \% A - #endif
复制代码 1 S( C# @: L. t: p2 }, P7 Z7 E1 C
4 u$ P3 R: q1 W. t' f' v$ {
3) AT24C02读写函数
$ T* D6 U7 b- I9 {" Y
$ L* O$ ^# I n编写好I2C协议函数后,参考AT24C02手册编写读写数据函数,如代码段 16.3.7 所示。" x; u; M' c }; o y3 q
2 g" S9 H% n, S7 p( l' R
代码段 16.3.7 读写AT24C02一字节数据(driver_eeprom.c). ]' \7 H9 q+ ^0 J4 L4 t- \( P+ U
9 X, n$ \. Q0 e; w* B
- /*7 s0 ^' ^% H1 ^$ ?
- * 函数名:uint8_t EEPROM_WriteByte(uint16_t addr, uint8_t data)
4 C8 e8 b$ d4 V# X$ B; x5 D0 Y, i - * 输入参数:addr -> 写一个字节的EEPROM初始地址
) l5 X# x6 Y! y8 y) G8 Z- R5 b - * data -> 要写的数据) m! I5 T& H- o$ U
- * 输出参数:无
[* t3 K/ h! d9 T$ p5 D - * 返回值:无2 q: d$ v7 q. Y, R+ _2 [3 d1 w
- * 函数作用:EEPROM写一个字节. F- T" q i% S3 \+ T8 d5 w
- */- h {: ^) n' x- `. ]! e6 m# t5 X
- void EEPROM_WriteByte(uint16_t addr, uint8_t data)
* b& w: b: w% X3 ^2 ? - {
- E/ Q0 f( i/ K Q - /* 1. Start */6 t6 t; q- z$ \" b
- I2C_Start();
) o8 h- G# X& g - 8 v% w) x0 x, A+ i" z
- /* 2. Write Device Address */, z% G4 _: j+ v+ E4 P# ?4 a
- I2C_SendByte( EEPROM_DEV_ADDR | EEPROM_WR );3 v) G/ n: y6 u I
: o/ [* V2 b7 e' }& n- /* 3. Data Address */
; P- h" _1 N3 A% x3 O - if(EEPROM_WORD_ADDR_SIZE==0x08)
% W+ _/ S. u2 j! k% m. K2 O7 \ - {) d7 f) ?2 m6 a% |1 C; J$ t( S
- I2C_SendByte( (uint8_t)(addr & 0x00FF) );0 \. q4 @$ U! c& R. M) A' A
- }$ E& Q$ d; M% D0 c$ ?- g+ u
- else
5 e# U1 D) s3 l3 Y( J - {5 h5 H; c+ n8 q2 g% I7 w
- I2C_SendByte( (uint8_t)(addr>>8) );1 Q5 W3 x, K, a5 Q& X( q! d# j) O
- I2C_SendByte( (uint8_t)(addr & 0x00FF) );5 ~0 `9 j6 T- ^( D# u6 `
- }
' O9 h5 S. E% g; k( [! H
% C; p$ I j0 d% X I- /* 4. Write a byte */# k$ Z0 T) h t" n. O6 A
- I2C_SendByte(data);
. U* u( h0 b9 w" u6 g, K! S$ ]
! v3 C( @+ R* [- `+ D. S- /* 5. Stop */
. J: E1 O% F2 d - I2C_Stop();2 U+ d! W8 W" @
- }
1 _. q8 g" W" {2 H - & I' d7 u2 s- W6 F! O. s& O5 G) Z
- /*
2 i6 N! b; s. J; U - * 函数名:uint8_t EEPROM_ReadByte(uint16_t addr, uint8_t *pdata)2 p7 r, `7 x& n+ x; `* ^3 A
- * 输入参数:addr -> 读一个字节的EEPROM初始地址: z& D' E! ^) L9 [% o
- * data -> 要读的数据指针' G) W. n: f) L3 R4 Z1 z' D- P' R
- * 输出参数:无
8 u8 b/ K+ d" @2 m6 F - * 返回值:无
5 X, j* w4 m1 F( J5 x+ G - * 函数作用:EEPROM读一个字节
e' F# Y1 `8 w4 k, g; R6 ] - */' D' I5 |9 \- G' w% f
- void EEPROM_ReadByte(uint16_t addr, uint8_t *pdata)0 Z1 l; @ A9 r/ @. x- F3 p: {
- {
( N3 O& N5 O0 P7 u' z) K) l - /* 1. Start */
( b3 u/ c5 T0 \ - I2C_Start();( v- @9 V8 f* Y, A: c8 M# t8 Q
- ' ]8 _! J0 l5 K! N3 l
- /* 2. Write Device Address */
p' a, a; `& }$ @' K - I2C_SendByte( EEPROM_DEV_ADDR | EEPROM_WR );. s8 l( k' D6 e6 x
- , y9 S# h% E9 @. J4 J
- /* 3. Data Address */& k( [: Z: w3 a& x# |/ b
- if(EEPROM_WORD_ADDR_SIZE==0x08)
9 a8 A6 F$ y' ]$ |* ~) A - {
7 @& J l8 H+ z/ e# o" @7 x - I2C_SendByte( (uint8_t)(addr & 0x00FF) );
H# f7 w0 W# [8 r* ~- O/ ~' U - }3 o& @; {# J# K; J, S
- else
) P3 G: m: @1 a - {
+ i# p2 H6 ?* v - I2C_SendByte( (uint8_t)(addr>>8) );7 z3 b' ]# B% _* j
- I2C_SendByte( (uint8_t)(addr & 0x00FF) );
$ q o- \/ b# I2 @$ G _. i - }
5 x' |% N- t5 q+ R! c: \6 D: G. Y - ! m& E' G/ ]( |
- /* 4. Start Again */
5 q; L2 k% n$ }6 |9 N - I2C_Start();
?, a; \4 \% A# z
+ E$ b5 U# _+ ? K+ \0 N- J- /* 5. Write Device Address Read */( Y8 `: p. m* b2 S" l3 I2 b! n+ f( R
- I2C_SendByte( EEPROM_DEV_ADDR | EEPROM_RD );
7 z. l* |9 O6 y7 i/ d* g' I
& x5 e/ B; j& C, r; _" L- /* 6.Read a byte */$ |9 e. q- U, x/ V0 t$ f: z/ l
- *pdata = I2C_ReadByte(NACK);
r4 Y& d# w; h0 C- L8 [4 a& l - ; n) w+ q" P0 `
- /* 7. Stop */% ~8 \- f* ]3 {$ p
- I2C_Stop();
$ f( r/ I7 b f7 ?! ?$ p- v7 h - }
复制代码
4 O5 Q0 |& v/ c1 i参加前面图 16.2.1 和图 16.1.13 所示的介绍时序,编写AT24C02一字节读写程序。
, ]1 |& M& S2 C/ f0 f! a' y7 g. A' N1 n3 j& w" ?1 C! A$ Z
9~33行:写AT24C02一字节数据;
$ f: M5 y }( M6 f% F* N: s: a+ M
12行:发送I2C开始信号;
0 R7 b% \" B' H U
$ U" e9 z- q& z15行:发送AT24C02的设备地址,最后一位表示写操作;" H5 L- t# {1 Y+ S' l7 i
$ G# z) e' |# u( o- V18~26行:根据EEPROM型号,调用不同的数据地址长度设置函数(AT24C01/02为8位,AT24C04/08/16为16位);5 I3 \1 y. r3 L9 T
" l4 F7 B6 \# ^! }! S$ u29行:发送数据;
6 E- }+ h) X O' n$ l2 K1 f- y. n$ _+ ?; d% W
32行:发送I2C停止信号;
6 E) V3 Z$ M" O$ Z3 s
9 Y4 A% u1 s7 y7 J' F, l% d) J! J43~73行:读AT24C02一字节数据;
u- D7 J( q% V" ^7 K3 a
0 e* m/ A, O9 n: a( j46行:发送I2C开始信号;- k7 _3 s- A( D% O
% n! n: {3 E5 m
49行:发送AT24C02的设备地址,最后一位表示写操作(接下来要写数据地址);, c$ q9 I7 F8 ?1 Y+ I
4 R4 {, g; Y) h/ H3 ~; d7 Y) F
52~60行:根据EEPROM型号,调用不同的数据地址长度设置函数(AT24C01/02为8位,AT24C04/08/16为16位);6 `! b/ m0 [0 M% V$ R; M1 j. x
% V, w! p2 @2 Q" y0 ]. u4 Q( q63行:再次发送I2C开始信号;
2 @% o5 r, Y6 M+ V% o
: z) S, p; G8 y3 ~ z0 h5 G9 m! y' b66行:发送AT24C02的设备地址,最后一位表示读操作;
C" l4 i [ Y4 Y; f1 a
* }6 T2 ^+ C) j/ j; s B69行:读取AT24C02数据,且无需ACK;( A! p/ ?; f' I& s2 |- A- w
6 o' x2 G7 c; I
72行:发送I2C停止信号;
6 k% M4 ~ I6 b1 Z2 Q( l1 [: a
) k6 ]/ B7 o' X1 o, E$ ~实现了对AT24C02单字节的读写,还需要实现多字节的读写。多字节读写可以通过AT24Cxx的页写模式和顺序读模式,实现多个数据的连续读写。在页写模式时,需要程序上设置,不能跨页写,这里简单处理,直接多次调用前面的单次读写即可,如代码段 16.3.8 所示。1 ^' Z: W8 \, t7 I1 d @1 @
3 u- A" u0 ^1 y1 m代码段 16.3.8 读写AT24C02多字节数据(driver_eeprom.c)9 V' C! t. S+ x1 N& a5 }
' J6 w! {- C. ?2 c
- /*
1 @) R- D$ @) R - * 函数名:void EEPROM_Write_NBytes(uint16_t addr, uint8_t *pdata, uint16_t sz)
! @9 d/ N8 Q F+ F0 M C - * 输入参数:addr -> 写一个字节的EEPROM初始地址, x0 [* r) d' f. K
- * data -> 要写的数据指针
2 x, }: m$ U5 @8 [* V6 S - * sz -> 要写的字节个数
1 s; q" e5 C$ L; _4 }1 q& P# o - * 输出参数:无% L6 [- d W [% z- g0 M# \( U: E& [
- * 返回值:无
& ?8 {( S0 A. ~/ G" k) ] - * 函数作用:EEPROM写N个字节
4 h- b. |0 X! b - */
7 f* d$ Z7 \5 V# r& r Q" A - void EEPROM_Write_NBytes(uint16_t addr, uint8_t *pdata, uint16_t sz)
2 c" V9 A' b: ]% w6 U# ]# [6 G - {/ p+ }* p" l. z6 i! W0 I
- uint16_t i = 0;* x% z7 ?/ ]* D
+ b" S+ S6 D+ x; m- for(i=0; i<sz; i++)6 [' Z" ]9 m4 I. O* Q& I. {
- {8 P9 S" I# B% ~. M: H
- EEPROM_WriteByte(addr, pdata<i>);/ h. J; L5 g( v. x
- addr++;# j( t1 E: ^5 o# n
- HAL_Delay(10); // Write Cycle Time 5ms
0 n, C) L$ W7 e& u* q! @ - }
R! g5 @4 C8 S+ r* Q3 | - }( D: X* W3 _" W
- ) @+ G" X" x5 d: u) o
- /*6 @5 p9 ?' F1 c. i# m0 k
- * 函数名:void EEPROM_Read_NBytes(uint16_t addr, uint8_t *pdata, uint16_t sz)! O7 M# M: @7 |, y5 k# N
- * 输入参数:addr -> 读一个字节的EEPROM初始地址$ y4 H, B. r- L+ H2 l+ l
- * data -> 要读的数据指针* u6 a- X; n3 j9 n. F a. o
- * sz -> 要读的字节个数
4 n* A4 b. V! f5 P6 B9 J, z# n - * 输出参数:无: v1 E# w7 o1 a0 d6 i
- * 返回值:无
2 D' y2 |$ K5 L T! r - * 函数作用:EEPROM读N个字节
) ~: b& c( P+ L- z: R! l - */; o3 Q* L7 v/ b6 G
- void EEPROM_Read_NBytes(uint16_t addr, uint8_t *pdata, uint16_t sz)
) F" B7 p* L% S" B! L, F: E) E9 Q$ F - {8 j" H8 \4 y. C' N% ^1 }; h0 f; O
- uint16_t i = 0;
1 D+ N7 ], v X+ y! W% t - 6 O2 W$ M' w% o9 ~1 M) c
- for(i=0; i<sz; i++)) Y! x. d, f M, N( z) t
- {
/ p! A! \2 c, S, K7 d6 z# o; f3 G - EEPROM_ReadByte(addr, &pdata<i>);! ^1 d+ }1 P. |7 D! }
- addr++;
$ V- X/ _' o7 a% ?' c" u' T# \ - }
9 r- q: K6 |) K8 X6 ^# ]+ Z - }</i></i>
复制代码
- E/ t' R3 g" E" a" h3 c1 r需要注意的是,AT24Cxx每次写操作后,有一个写间隔,需要间隔5ms以上,因此在写多个字节时,每次写完都需要延时5ms以上。, ^* H4 } s1 M9 h0 V
: ^: J! \1 \; g7 ]7 H
6 h! H2 G. X& }3 S ^% W. c6 O
0 L* T) R2 a/ L; F/ {4) 主函数控制逻辑: @3 q, i5 M6 X0 q# A) e* F
- N$ k; M9 _- b# K5 p! A" G* s在主函数里,每按一下按键,调用“EEPROM_Write_Nbytes()”对AT24C02写一串数据,再调用“EEPROM_Read_Nbytes()”读出该数据,如代码段 16.3.9 所示。
+ s: d4 Z! [4 @6 R1 d5 ~
9 [* u. z& I: [; {0 t代码段 16.3.9 主函数控制逻辑(main.c)
! g, _7 t/ b8 W; [$ w( \: c. |6 c, \; c1 `' n( ?
- // 初始化I2C
& ?$ R: p, o7 R7 z9 f9 m. M: H7 U - I2C_Init();" ^) ~3 u% L; s! |. t' ^2 j
- $ m- h, o+ b9 N" i! {' z) N4 z3 A
- while(1)
- ?7 O/ }+ P: F5 r - {. H# I) M! o: Y7 N, S: J2 G7 X
- if(key_flag) // 按键按下
* e- J( _$ ~, [) z, L: u% t - {/ t- h3 E, j8 ?
- key_flag = 0;
; i) {6 Y8 w7 J/ Q - - A; g. l, Y7 S/ I: T- |& @! W( D
- printf("\n\r");
2 w7 ?& Z. l- z+ `/ n - printf("Start write and read eeprom.\n\r");( \" X ~( P9 G. p+ ]# p1 X
* C B% ]9 L9 }* [/ }3 c- // 读写一串字符,并打印
: Q$ [$ I9 A; j# U0 n7 {' L - EEPROM_Write_NBytes(0, tx_buffer, sizeof(tx_buffer)); // 写数据) n0 l8 u+ o7 g3 i2 l; x
- HAL_Delay(1);
7 M( X8 ]1 P( Q. M# G5 _4 p
2 ~( f2 U5 ^% y, t- EEPROM_Read_NBytes(0, rx_buffer, sizeof(tx_buffer)); // 读数据$ v6 z8 F* u$ G& I4 H0 W/ ~
- HAL_Delay(1);
8 |" s9 \8 B' F. x3 K6 u
& G$ P" t2 s) x, r9 m+ g- printf("EEPROM Write: %s\n\r", tx_buffer);
" S! f: o9 H: P! @' D - printf("EEPROM Read : %s\n\r", rx_buffer);
% B' e1 f# S3 X9 v
2 i, D r9 J+ G; [$ m( X- memset((uint8_t*)rx_buffer, 0, sizeof(rx_buffer)); // 清空接收的数据 ~8 u# {" Y) R4 k- C. q7 g
- }& M# P! E7 J- U* c0 e
- }7 r% @6 K4 _: o) d" b" H7 B/ L, W7 F
复制代码 ; J. g) D8 |8 X5 h x6 ^
% f$ q: `( l6 P# K16.4 实验效果
; r, r9 K/ `& S! M; Q5 Q本实验对应配套资料的“5_程序源码\8_通信—模拟I2C\”。打开工程后,编译,下载,按下按键KEY,即可看到串口如图 16.4.1 所示。; |1 Y$ |( j4 R" T
: L1 V0 f- H$ K S/ t+ N8 `5 ~7 O
/ u3 r8 n6 P1 m/ u5 F- z0 ~
图 16.4.1 模拟I2C读写AT24C02数据
! `* R- `4 x9 b- {; @ s+ s0 n( w5 x, W: @% f3 X' z$ T( E$ A
作者:攻城狮子黄
' `* s- U# K% F9 x8 |0 U2 g6 i$ s5 f! l/ y( H
$ R6 N0 Y* V- j. \
|