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