这里仅总结一下IO控制相关及这种总线等  ~持续更新 第13部的啊  

今天的主角是UART。

我们通常说的串口,UART包含TTL电平和RS-232电平两种,嵌入式系统里面,单片机的串口一般都是TTL电平。

嵌入式分享~IO相关13_嵌入式硬件

今天的内容关于UART的帧格式,比较简单,玩过单片机的小伙伴应该都知道。

UART的英文全称是Universal Asynchronous Receiver/Transmitter,意为通用异步收发传输器。

UART因为有两根线数据线TX和RX,可以以全双工的形式进行发送和接收数据,同一时刻,两条链路的发送器和接收器可以同时传输数据。

嵌入式分享~IO相关13_嵌入式硬件_02

区别于全双工的,还有另一种,是半双工,因为只有一根数据线,所以数据传输是这样。

嵌入式分享~IO相关13_嵌入式硬件_03

或者是下面这样,同一时刻,只有一条链路在传输数据。

嵌入式分享~IO相关13_嵌入式硬件_04

除了双工形式,还有一种是半工,只有发送器到接收器这一个链路。

嵌入式分享~IO相关13_嵌入式硬件_05

说完了UART的工作模式,下面进入主题——UART帧格式,也可以称之为UART协议,单片机与PC之间的通信,为了保证数据通信的可靠性,双方都必须遵从UART协议。

嵌入式分享~IO相关13_嵌入式硬件_06

(UART数据帧格式)

其中各位的含义如下:

  • 起始位:发送1位逻辑0(低电平),开始传输数据。
  • 数据位:可以是5~8位的数据,先发低位,再发高位,一般常见的就是8位(1个字节),其他的如7位的ASCII码。
  • 校验位:奇偶校验,将数据位加上校验位,1的位数为偶数(偶校验),1的位数4为奇数(奇校验)。
  • 停止位:停止位是数据传输结束的标志,可以是1/1.5/2位的逻辑1(高电平)。
  • 空闲位:空闲时数据线为高电平状态,代表无数据传输。

如果我们传输数据0X33(00110011),那么对应的波形就是如下这样,因为是LSB在前,所以8位数据依次是11001100。

嵌入式分享~IO相关13_嵌入式硬件_07

(发送0X33数据帧格式)

如果再发其他数据,再依次循环这个过程即可。

UART是异步传输,以1个字符为传输单位,传输2个字符之间的时间间隔,比如传输0X33后再传输0X35,这两者时间间隔是未知的。

但是,同一字符内相邻位间的时间间隔是确定的,比如0X33低两位的1和1之间的时间间隔是确定的,这涉及到UART传输速率的概念——波特率

波特率的单位是bps,全称是bit per second,意为每秒钟传输的bit数量。

波特率9600bps,代表每秒钟传输bit的数量为9600,那么传输1bit数据的时间就是1/9600=104us,波特率115200bps,代表传输1bit数据的时间是8us。

那么,两个串口之间是如何发送和接受数据呢?

首先,UART1以9600波特率发送0X33,先在数据线上放1个104us脉宽的低电平(起始位),然后是连续2个104us脉宽的高电平(2bit逻辑1),依次类推。

其次,UART2以9600波特率接收0X33,通过数这些数据的脉宽,来确认数据。

为了确保数据传输的正确性,减少误差,一般UART1和UART2之间的波特率差别小于10%,一次最多只能传输1个字节(8bit),也有效减小了累计误差。

二、串口通信及其常见问题

什么是串口通信

    串口通信,就是传数据只有一根线传输,一次只能传一个位,要传一个字节就需要传8次。串口通信就是把数据串在一根线上传输,所以就叫串口吧。

    在对速率要求不高的情况,使用一根线发送数据是带来大大的方便和实用价值的。

    为了能正常发送和接收正确的数据,那异步串口通信就需要如下图的格式。

嵌入式分享~IO相关13_嵌入式硬件_08

  在串口的通信参数上,就有了波特率、起始位、数据位、校验位、停止位这几个参数。

    串口通信主要为分232,485,422三种通信方式,这三种有什么区别呢?

232

嵌入式分享~IO相关13_嵌入式硬件_09

  232通信主要是由RX、TX、GND三根线组成。

    RX接TX,TX接RX,GND接GND。这里发送和接收分别是由不同的线处理的,也就是能同时发送数据和接收数据,这就是所谓的全双工通信。

    在这里扩展一下,串口通信还有一个功能叫做全功能串口通信,也叫标准串口。因为在两个设备间进行数据传输,有些设备处理速度比较快,有些数据比较慢。为了保证数据能正常传输,在RX、TX的基础上,还增加了几个控制引脚,最后成了9个引脚,也就是常见的DB9这个东西,如下图所示。

嵌入式分享~IO相关13_嵌入式硬件_10

 但是,如今很多控制器、人机界面、PLC等使用串口通信中一般不使用标准串口,而是直接使用RX、TX、GND三根线来通信的。

485

    485的出现,是为了解决232通信距离受限的问题。

    485通信只需要+、-两根线,或者也叫A、B两根线。A,B两根线的差分电平信号就是作为数据信号传输。由于发送与接收都是用这两根线,也就是说每次只能用作发送或者只能用作接收。所以,485是半双工通信。

嵌入式分享~IO相关13_嵌入式硬件_11

485就是这样牺牲了232全双工的效率来达到自己传输距离远的代价。

422

    422的出现,是为了既实现232的全双工通信方式,又能像485这样提高传输距离。422也常被标注为485-4,而485被标注为485-2。因为485-2是2根线,485-4是4根线,下图是422的示意图。 

嵌入式分享~IO相关13_嵌入式硬件_12

  422就是把232的RX分成两根线,RX+,RX-,把TX分成TX+,TX-。这样就可以同时发送和同时接收了,还可以像485这样,有较远的传输距离。可是这样一种很有优势的通信方式,却用的不多,最常用到的是232跟485。

串口通信常见问题

电脑使用USB转串口可以和设备通信上,换成屏与设备就通信不上了

1)有可能电脑USB转串口接到设备上,使用的是标准串口功能,也就是除了RX,TX,GDN外,还使用了其它引脚。比如像欧姆龙PLC,三菱PLC,在实际与屏的通信中,就需要接某些引脚短接的情况。

2)电脑与控制器或PLC通信时,是扫描波特率参数,自适应的,屏通信可能参数没有设备好。在三菱,基恩士等PLC,就存在变化波特率进行通信交互的过程。

3)也有可能是接线方式不对。因为有些DB9,还需要公头,母头。如果不注意的话,也会存在把TX接到TX上,把RX接到RX上,这样需要注意的地方。

4) 在这里补充一下,有时候可能会使用一些串口助手发送测试数据与控制器通信,有些串口助手的奇偶校验是不起作用,这个要提醒一下。

这A家的屏可以和设备通信,换成B家的屏就通信不上了

1) 首先确认一下接线是否正确了,RX和TX是否兼容。

2) 地线是否没有接。

3) 除了RX,TX,GND,是否还有其它引脚需要短接的。

4) 通信协议是否一致或不完善,波特率是否一样。

以前不接地线可以通信,换个设备为什么需要接地线了

    这个问题和上一个类似,因为有些设备使用了隔离电源。以前不接地可以通信,有可能实际上地线已经接了,所以才可以通信。可能换了个带隔离电源的,两个设备的地是隔离的,就需要在串口上把地线接起来。这个我是自身经历过的,有个客户老说他的设备通信不上,后来拍个照我给我,他地线没有接,他说以前不接地线可以通信的。于是我就给他科普了一下。

一个设备是232,另一个设备是422,没有转换设备,怎么办

    这个情况我遇到过,客户的设备是422通信的,但是我手上并没有422设备,只有232通信可以测试。因此就需要把422转成232进行通信。

    刚才也讲了422和232的接线,因为这两个都是全双工的,接收和发送都是分到的,而422只是以一种差分信号进行传输。

    把422的Rx+与232的TX接,422的RX-与232的GND接。

    把422的TX+与232的RX接,422的TX-与232的GDN接。

    这样,422设备要发送数据的,就可以发送到了232的RX上。232的TX发数据后,由于TX和GND也形成了差分信号给422,422就可以接收到数据了。

用232通信没问题,用485通信没问题,使用232转485之后就通信不稳定

    232和485从通信原理上,最大一个差别是全双工和半双工的区别。可是应用层发送数据和接收数据才不管底下是全双工还是半双工。

    但是485就得管了。因为既然是半双工,就得严格保证通路上只能有发送或只能有接收的数据,一旦同时有发送和接收,数据就会冲突了。所以解决的办法就是主站设备,也就是主动命令的一方就需要严格控制好发送数据命令的节奏了。当然有些232转485的设备做的比较好了,可以优化这个,但是主站还是要控制,比较把通信速率调节慢一些。

要想实现两个屏或两个主站通过485访问modbus设备,有什么好的办法

    在485通信中,基本上是一主多从。但是遇到一些客户实际使用中,有客户想用两个屏来访问一个modbus设备的。目前暂时还没有好的办法。等这个功能出来后,再来给大家演示操作吧。

串口通信的弱点

1)信号干扰的问题

    建议使用带屏蔽线,接线要严格,比如要接地。有些485通信上,还考虑接上终端电阻来匹配。如果是232,尽量不要让线太长。通信协议上尽量避免长报文的数据通信。

2)波特率匹配的问题

    因为有些设备的计算的波特率是存在误差的,特别是一些控制器,由于使用的晶振不一样。因此在一些波特率比如9600波特率就存在误差。存在误差带来的影响是什么呢。因为接收方是通过时间来计算一个位的。那么如果一个报文过长,就会存在误差积累的问题,算着算着就偏了。所以,这也是串口通信不稳定的一些地方,在使用上应注意避免发送太长数据的包。

3)在一些可能会存在干扰的情况,可以考虑使用奇校验或者偶校验

    因为虽说出现错误的可能性不大,但既然存在干扰,如果加了校验,至少可以把错误的报文过滤掉。总好比没有校验然后通信数据错了不知道。或者尽量使用一些带校验的协议,防止数据出错。

4) 串口通信本来就比较慢,请降低对数据响应的要求

    因为串口通信本身就比以太网慢。而且,串口通信并不是能像CPU那样多线程处理。因为就一个口一个线数据出去,即便你应用到程序再怎么用多线程处理数据,但是最底下也只有一个口出去,一次也只能传一个位,一个字节过去。因为有客户在使用9600的波特率通信,但是又希望多少的数据可以在多少毫秒内得到响应。

    但是串口通信还是要事实求是,所以正确认识串口通信对应用,对开发,对沟通都有着很大的帮助的。

为什么不用同步通信

    刚才提到,同步通信需要依赖于时钟信号。这就存在一个问题,这个时钟信号是谁来发起呢。在同步通信中,往往需要一个主设备发起时钟信号读从模块的数据。在实际中,有屏读PLC,有屏读屏的数据。而单纯地从异步串口通信来说,是没有主从之说,双方都是平等的角色,都可以互发信息,互收信息。而同步通信一般是应用于CPU读一些模块,由CPU发起时钟信号,比如读SD卡模块,就可以通过SPI方式,还有一些传感器模块。

三、STM32串口发送字符串的几种写法

STM32用USART发送字符串

嵌入式分享~IO相关13_嵌入式硬件_13

嵌入式分享~IO相关13_嵌入式硬件_14

代码含义是:

当接收引脚有数据时,状态寄存器的USART_FLAG_RXNE就会为1,此时USART_GetFlagStatus(USART1,USART_FLAG_RXNE)的返回值就为1(SET),若无数据则为RESET。

代码常见写法,及其接收数据效果

#1

嵌入式分享~IO相关13_嵌入式硬件_15

这种写法在不是特殊(不掉电、不待机等)情况下,问题不大,USART数据会成功发送出去。但是在上面说的特殊情况下,问题就来了,代码只将数据放到了发送缓冲区,而没有发送出去就掉电或待机了,这个时候其实最后两个字符是没有发送出去的。

#2

嵌入式分享~IO相关13_嵌入式硬件_16

这种写法达到的效果和上面存在不同的就是倒数第二个数据发送出去了,也就是只有最后一个字符是没有发送出去的。

#3

嵌入式分享~IO相关13_嵌入式硬件_17

这种写法达到的效果和上面两种写法有不一样,发送了10个字符。

#4

嵌入式分享~IO相关13_嵌入式硬件_18

这种写法按理说可以实现功能,但实际多次试验结果确实第一字节数据丢失了。

#5

 

嵌入式分享~IO相关13_嵌入式硬件_19

这种写法是比较完成,为了保守起见,在特殊情况下使用该写法。 

四、STM32指针抽象出I2C的数据实例

I2C总线是由PHILIPS公司开发的一种简单、「双向二线制同步串行总线」。

关于i2c的使用,并不陌生,STM32、C51、ARM、MSP430等,都基本集成硬件i2c,或者不集成i2c的,可以根据总线时序图使用普通IO口翻转模拟一根i2c总线。

嵌入式分享~IO相关13_嵌入式硬件_20

对于流行的STM32饱受诟病的硬件I2C,相信很多人都是使用模拟I2C。

模拟i2c的源码比较多,大多都是大同小异,对于各类例程,提供的模拟i2c似乎都不是太规范(个人见解),特别是一根i2c总线挂多个外设、模拟多根i2c总线、以及更换一个i2c外设时,都需要大幅度修改源码、复制源码、重新调试时序等重复的工作。

在阅读过Linux设备驱动框架和RT-Thread的驱动框架,发现在总线分层上处理就特别好,完美解决了上述提及的问题。参考RT-Thread和Linux下的模拟i2c,整理修改在裸机上使用。

2.Linux、RT-Thread设备驱动模型

1)模型分为总线驱动和设备驱动;

2)  总线驱动与外设驱动分离,方便一根总线挂多个外设,方便移植;

3)  底层(与硬件相关)与上层分离,方便添加总线及移植到不同处理器,移植到其他处理器,只需重新实现硬件相关的“寄存器”层即可;

嵌入式分享~IO相关13_嵌入式硬件_21

MCU下裸机形式i2c总线抽象

此部分实现源码为:i2c_core.c  i2c_core.h

1)i2c总线抽象对外接口(API)

“i2c_bus_xfer”为i2c封装对外的API,函数原型如下,提供一个函数模型,具体需要实例化函数指针。

int i2c_bus_xfer(struct i2c_dev_device *dev,struct i2c_dev_message msgs[],unsigned int num)
{
 int size;
 
 size = dev->xfer(dev,msgs,num); 
 return size;
}

a)此函数即作为驱动外设的对外接口,所有操作通过此函数接口,与底层总线实现分离,如EEPROM、RTC、温度传感器等;

b)一个对外函数已经实现90%的情况使用,对应一些特殊情况,后期再完善或增加API。

c)struct i2c_dev_device *i2c_dev

2)i2c总线抽象API参数

a)i2c_dev:i2c设备指针,类型为“struct i2c_dev_device”,驱动一个i2c外设时,首先要对此指针设备初始化;

b)msgs:i2c一帧数据,发送数据及存放返回数据的缓存;

c)num:数据帧数量。

3)struct i2c_dev_device

该结构体为关键,调用API驱动外设时,首先对此初始化(类似于Linux/RT-Thread注册设备)。完整的设备包括两部分,数据操作函数和i2c相关信息(如硬件i2c或者模拟i2c)。因此“struct i2c_dev_device”的原型为:

struct i2c_dev_device
{
    int (*xfer)(struct i2c_dev_device *dev,struct i2c_dev_message msgs[],unsigned int num);
    void *i2c_phy;
};

a)第一个参数是函数指针,数据收发通过此函数指针调用实体函数实现;

b)第二个参数是一个void指针,初始化时指向我们使用的物理i2c(硬件/模拟),使用时可强制转换为对应的类型。

4)xfer

该函数与i2c总线设备对外接口函数“i2c_bus_xfer”具有相同的参数,形参参数参考此项的第2点,初始化时实例化指向实体函数。

5)struct i2c_dev_message

“struct i2c_dev_message”为i2c总线访问外设的一帧数据信息,包括发送数据、外设从地址、访问标识等。原型如下:

struct i2c_dev_message
{
 unsigned short  addr;
 unsigned short flags;
 unsigned short size;
 unsigned char *buff;
 unsigned char   retries;  
};

a)addr:i2c外设从机地址,常用为7位,10位较少用;

b)flags:标识,发送、接收、应答、地址位选择等标识;几种标识如下:

#define I2C_BUS_WR             0x0000
#define I2C_BUS_RD             (1u << 0)
#define I2C_BUS_ADDR_10BIT     (1u << 2)
#define I2C_BUS_NO_START      (1u << 4)
#define I2C_BUS_IGNORE_NACK    (1u << 5)
#define I2C_BUS_NO_READ_ACK    (1u << 6)

c)size:发送的数据大小,或者接收的缓存大小;

d)buff:缓存区;

e)retries:i2c启动失败时,重启的次数。

模拟i2c抽象

对于模拟i2c,在以往的实现方式中,基本是时序图和外设代码混合在一起,增加外设或者使用新的i2c外设时,需要对模拟i2c代码进行较大工作量的修改,或者以“复制”的方式实现一套新的i2c总线。

但同理,可以把模拟i2c时序部分代码抽象出来,以“复用”代码的形式实现。此部分实现源码为:i2c_bitops.c  i2c_bitops.h

1)模拟i2c抽象对外接口

根据上述封装的对外API,使用时,首先需要实现入口参数“i2c_dev”实例化,用模拟i2c即是调用模拟i2c相关接口。

int i2c_bitops_bus_xfer(struct ops_i2c_dev *i2c_bus,struct i2c_dev_message msgs[],unsigned long num)
{
 struct i2c_dev_message *msg;
 unsigned long i;
 unsigned short ignore_nack;
 int ret;
 
 ignore_nack = msg->flags & I2C_BUS_IGNORE_NACK;
 i2c_bitops_start(i2c_bus);       
    for (i = 0; i < num; i++)
    {
        msg = &msgs[i];
        if (!(msg->flags & I2C_BUS_NO_START))
        {
            if (i)
            {
                i2c_bitops_restart(i2c_bus); 
            }
            ret = i2c_bitops_send_address(i2c_bus,msg);
            if ((ret != 0) && !ignore_nack)
                goto out;
        }
        if (msg->flags & I2C_BUS_RD)
        {//read
            ret = i2c_bitops_bus_read(i2c_bus,msg);
            if(ret < msg->size)
            {
                ret = -1;
                goto out;
            }
        }
        else
        {//write
            ret = i2c_bitops_bus_write(i2c_bus,msg);
            if(ret < msg->size)
            {
                ret = -1;
                goto out;
            }
        }
    }
 ret = i;
out:
 i2c_bitops_stop(i2c_bus);
  
 return ret;
}
int ops_i2c_bus_xfer(struct i2c_dev_device *i2c_dev,struct i2c_dev_message msgs[],unsigned int num)
{
 return (i2c_bitops_bus_xfer((struct ops_i2c_dev*)(i2c_dev->i2c_phy),msgs,num));
}

a)模拟一根i2c总线时,对外的操作函数都通过上诉函数;i2c信息帧相关参数由上层调用传递进入,此处主要增加“struct ops_i2c_dev”的封装;

b)该函数使用到的函,其中入口参数为“struct ops_i2c_dev”类型的都是模拟i2c相关;

d)模拟i2c封装实现主要针对“struct ops_i2c_dev”原型的实例化。

2)struct ops_i2c_dev

“struct ops_i2c_dev”原型如下:

struct ops_i2c_dev
{
        void (*set_sda)(int8_t state);
        void (*set_scl)(int8_t state);
        int8_t (*get_sda)(void);
        int8_t (*get_scl)(void);
        void (*delayus)(uint32_t us);
};

a)set_sda:数据线输出;

b)set_scl:时钟线输出;

c)get_sda:数据线输入(捕获);

d)get_scl:时钟线输入(捕获);

e)delayus:延时函数;

要实现一个模拟i2c,只需将上诉函数指针的实体实现即可,具体看后面描述。

3)模拟i2c时序

以产生i2c起始信号函数为例子,简要分析:

static void i2c_bitops_start(struct ops_i2c_dev *i2c_bus)
{
    i2c_bus->set_sda(0);                                          
    i2c_bus->delayus(3);
    i2c_bus->set_scl(0);                                                       
}

入口参数为struct ops_i2c_dev * i2c_bus,其实就是i2c_bitops_bus_xfer应用层函数传入的参数,最终是在此调用,底层需要实现的就是io模拟的输入/输出状态函数。

其他函数,如

static void i2c_bitops_restart(struct ops_i2c_dev *i2c_bus)
static char i2c_bitops_wait_ack(struct ops_i2c_dev *i2c_bus)
static int i2c_bitops_send_byte(struct ops_i2c_dev*i2c_bus,unsigned char data)

等等,入口参数都是i2c_bus,时序实现与常规裸机程序设计是一致的,不同的是函数指针的分离调用,具体看附件源码。

4)标识位

在以往的模拟i2c或者硬件i2c中,操作外设时都有各类情况,如读和写方向的切换、连续操作(不需启动i2c总线,如写EEPROM,先写地址再写数据)等。对于这类情况,我们处理办法是选择相关的宏标识即可,具体实现由“中间层”实现,让i2c外设驱动起来更简单!以上述对外函数为例:

a)通过标识位判断是读还是写状态

if (msg->flags & I2C_BUS_RD)
{//read
    ret = i2c_bitops_bus_read(i2c_bus,msg);
    if(ret < msg->size)
    {
        ret = -1;
        goto out;
    }
}

b)应答状态标识

ignore_nack = msg->flags & I2C_BUS_IGNORE_NACK;

5)读写函数

读写函数最终是通过io口1bit的翻转模拟出时序,从而获得数据,这部分与常规模拟i2c一致,通过函数指针方式操作。主要实现接口函数:

static unsigned long i2c_bitops_bus_write(struct ops_i2c_dev *i2c_bus,struct i2c_dev_message *msg);
static unsigned long i2c_bitops_bus_read(struct ops_i2c_dev *i2c_bus,struct i2c_dev_message *msg);

模拟i2c总线实现

此部分实现源码为:i2c_hw.c  i2c_hw.h

以stm32f1为硬件平台,采用上述模拟i2c封装,实现一根模拟i2c总线。

1)实现struct ops_i2c_dev函数实体

除了“delayus”函数外,其余为io翻转,以“set_sda”和“delayus”为例,实现如下:

static void gpio_set_sda(int8_t state)
{
    if (state)
     I2C1_SDA_PORT->BSRR = I2C1_SDA_PIN;
    else
     I2C1_SDA_PORT->BRR = I2C1_SDA_PIN;
}

static void gpio_delayus(uint32_t us)
{
#if 0  
    volatile int32_t i;

    for (; us > 0; us--)
    {
        i = 30;  //mini 17
        while(i--);
    }
#else
        Delayus(us);
#endif
}

a)为例提高速率,上诉代码采用寄存器方式操作,可以用库函数操作io口;

b)延时可以用硬件定时器延时,或者软件延时,具体根据cpu时钟计算;

c)其他源码看附件中“i2c_hw.c”

2)初始化一根模拟i2c总线

void stm32f1xx_i2c_init(void)
{
 GPIO_InitTypeDef GPIO_InitStructure;          
 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);  
 
 GPIO_InitStructure.GPIO_Pin = I2C1_SDA_PIN | I2C1_SCL_PIN;
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;      
 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;      
 GPIO_Init(I2C1_SDA_PORT, &GPIO_InitStructure);               
 I2C1_SDA_PORT->BSRR = I2C1_SDA_PIN;            
 I2C1_SCL_PORT->BSRR = I2C1_SCL_PIN;
 
 //device init
 ops_i2c1_dev.set_sda = gpio_set_sda;
 ops_i2c1_dev.get_sda = gpio_get_sda;
 ops_i2c1_dev.set_scl = gpio_set_scl;
 ops_i2c1_dev.get_scl = gpio_get_scl;
 ops_i2c1_dev.delayus = gpio_delayus;
  
 i2c1_dev.i2c_phy   = &ops_i2c1_dev;
 i2c1_dev.xfer    = ops_i2c_bus_xfer; 
}

a)i2c io初始化;

b)i2c设备实例化,其中“ops_i2c1_dev”和“i2c1_dev”即是我们定义的总线设备,后面使用该总线时主要通过“i2c1_dev”实现对底层的调用。

驱动EEPROM(AT24C16)

此部分实现源码为:24clxx.c  24clxx.h

上面总线完成后,驱动一个i2c外设可以说就是信手拈来的事情了,而且模拟i2c总线抽象出来后,不需在做重复调试时序的工作。

假设初始化的i2c设备为i2c1_dev。

1)写EEPROM

写一个字节,页写算法详细见源码附件(24clxx.c):

char ee_24clxx_writebyte(u16 addr,u8 data)
{
     struct i2c_dev_message ee24_msg[1];
     u8 buf[3];
     u8  slave_addr;
     if(EEPROM_MODEL > 16)
     {       
         slave_addr =EE24CLXX_SLAVE_ADDR;
         buf[0] = (addr >>8)& 0xff;   
         buf[1] = addr & 0xff;
         buf[2] = data;
         ee24_msg[0].size  = 3;
     }
     else
     {
         slave_addr = EE24CLXX_SLAVE_ADDR | (addr>>8);
         buf[0] = addr & 0xff;
         buf[1] = data;
         ee24_msg[0].size = 2;
     }
     ee24_msg[0].addr = slave_addr;
     ee24_msg[0].flags = I2C_BUS_WR;
     ee24_msg[0].buff = buf;
     i2c_bus_xfer(&i2c1_dev,ee24_msg,1);
  
     return 0;
}

2)读EEPROM

voidee_24clxx_readbytes(u16 read_ddr, char* pbuffer, u16 read_size)
{ 
     struct i2c_dev_message ee24_msg[2];
     u8     buf[2];
     u8     slave_addr;
     if(EEPROM_MODEL > 16)
     {
          slave_addr =EE24CLXX_SLAVE_ADDR;
          buf[0] = (read_ddr>>8)& 0xff;
          buf[1] = read_ddr& 0xff;
          ee24_msg[0].size  = 2;
     }
     else
     {
          slave_addr =EE24CLXX_SLAVE_ADDR | (read_ddr>>8);
          buf[0] = read_ddr & 0xff;
          ee24_msg[0].size  = 1;
     }
     ee24_msg[0].buff  = buf;
     ee24_msg[0].addr  = slave_addr;
     ee24_msg[0].flags = I2C_BUS_WR;
     ee24_msg[1].addr  = slave_addr;
     ee24_msg[1].flags = I2C_BUS_RD;
     ee24_msg[1].buff  = (u8*)pbuffer;
     ee24_msg[1].size  = read_size;
     i2c_bus_xfer(&i2c1_dev,ee24_msg,2);
}

3)注意事项

驱动一个外设相对容易了,注意的事项就是标识位部分。

a)此处外设地址(addr),是实际地址,不含读写位(7bit),比如AT24C16外设地址为0x50,可能大家平常用的是0xA0,因为包括读写位;

b)写数据时,如果以2帧i2c_dev_message消息发送,需要注意“I2C_BUS_NO_START”宏,此宏标识意思是不需要再次启动i2c了,一般看i2c外设手册时序图可知道。如写EEPROM是先写地址,然后写数据这个过程是连续的,此时就需用到“I2C_BUS_NO_START”标识。程序可改成这样:

char ee_24clxx_writebyte(u16 addr,u8 data)
{
     struct i2c_dev_message ee24_msg[2];
     u8     buf[2];
     u8 slave_addr;
     if(EEPROM_MODEL > 16)
     {                                   
          slave_addr =EE24CLXX_SLAVE_ADDR;
          buf[0] = (addr>>8)& 0xff;  
          buf[1] = addr &0xff;
          ee24_msg[0].size  = 2;
     }
     else
     {
           slave_addr =EE24CLXX_SLAVE_ADDR | (addr>>8);
           buf[0] = addr &0xff;
           ee24_msg[0].size  = 1;
     }
     ee24_msg[0].addr = slave_addr;
     ee24_msg[0].flags = I2C_BUS_WR;
     ee24_msg[0].buff  = buf;
     ee24_msg[1].addr = slave_addr;
     ee24_msg[1].flags = I2C_BUS_WR |I2C_BUS_NO_START;
     ee24_msg[1].buff  = &data;
     ee24_msg[1].size  = 1;
     i2c_bus_xfer(&i2c1_dev,ee24_msg,2);
          
     return 0;
}

4)其他

理解之后,或者使用过Linux、RT-Thread的驱动框架的,再驱动其他i2c外设,就是很容易的事情了,剩下的就是配置寄存器、应用算法的问题了。

总结

1)整体思路比较易理解,本质就是函数指针,将与硬件底层无关的部分抽象出来,相关联的地方分层明确,通过函数指针的方式进行调用。

2)事务分离,通用、重复的事情交给总线处理,特殊任务留给外设驱动。

源码

【1】  https://github.com/Prry/drivers-for-mcu

参考

【1】  https://github.com/RT-Thread/rt-thread