layout: post
title: GD32F427学习记录
description: GD32F427学习记录
categories:

  • GD32
    tags:
  • GD32
  • USART
  • I2C

GD32F427学习记录

一、时钟控制单元(CCTL)

1.简介

时钟控制单元提供了一系列频率的时钟功能,包括一个内部16M RC振荡器时钟(IRC16M)、一个内部48M RC振荡器时钟(IRC48M)、一个外部高速晶体振荡器时钟(HXTAL)、一个内部32K RC振荡器时钟(IRC32K)、一个外部低速晶体振荡器时钟(LXTAL)、三个锁相环(PLL)、一个HXTAL时钟监视器、时钟预分频器、时钟多路复用器和时钟门控电路。

2.时钟树

GD32F4XX时钟树

AHB: 240 MHz

APB2: 120MHz

APB1: 60MHz

  • RCU 通过 AHB 时钟(HCLK)8 分频后作为 Cortex 系统定时器(SysTick)的外部时钟

  • ADC时钟由APB2时钟经2、4、6、8分频或由AHB时钟经5、6、10、20分频获得

  • TIMER时钟由AHB时钟分频获得,它的频率可以等于CK_APBx、CK_APBx的两倍或CK_APBx的四倍

  • USBFS/USBHS/TRNG/SDIO的时钟由CK48M时钟提供

  • 可以选择PLLQ时钟、PLLSAIP时钟或IRC48M时钟做为CK48M的时钟源

  • CTC时钟由IRC48M时钟提供

  • I2S时钟可以选择由PLLI2SR时钟或外部I2S_CKIN引脚输入时钟提供

  • TLI时钟可以选择由PLLSAIR时钟的2、4、8、16分频提供

  • 以太网TX/RX时钟可以选择由外部引脚(ENET_TX_CLK / ENET_RX_CLK)输入时钟提供

  • RTC时钟可以选择由LXTAL时钟、IRC32K时钟或HXTAL时钟的2-31(由RCU_CFG0寄存器的RTCDIV位域值决定)分频提供

  • 当FWDG启动时,FWDG时钟被强制选择由IRC32K时钟做为时钟源

3.时钟源

  1. 外部高速晶体振荡器
  2. 内部16 MHz RC振荡器(IRC16M)
  3. 内部48 MHz RC振荡器(IRC48M)
  4. 32,768 Hz外部低速晶体振荡器(LXTAL)
  5. 内部32 KHz RC振荡器(IRC32K)

4.锁相环(PLL)

PLLP:可做为系统时钟

PLLQ:可以做为USBFS/USBHS/TRNG/SDIO模块的时钟源

PLLI2S:可以做为I2S模块的时钟源

PLLSAI:可以做为CK48M或TLI模块的时钟源

5.系统时钟(CK_SYS)选择

系统复位后,IRC16M时钟默认做为CK_SYS的时钟源

二、通用和备用输入/输出接口(GPIO 和 AFIO)

1.简介

最多可支持 140 个通用 I/O 引脚(GPIO),每个 GPIO 引脚可以由软件配置为输出(推挽或开漏)、输入、外设备用功能或者模拟模式。每个 GPIO 引脚都可以配置为上拉、下拉或无上拉/下拉。除模拟模式外,所有的 GPIO 引脚都具备大电流驱动能力

2.IO端口结构

IO端口基本结构图

3.引脚配置

在复位期间或复位之后

  • PA15:JTDI 为上拉模式

  • PA14:JTCK / SWCLK 为下拉模式

  • PA13:JTMS / SWDIO 为上拉模式

  • PB4:NJTRST 为上拉模式。

  • PB3:JTDO 为浮空模式。

  • 其他IO口:浮空

IO口配置为输入引脚时,可以选择内部弱上拉和弱下拉

IO口配置位输出引脚时,可以选择配置为开漏或推挽模式

4.中断时间

只有在输入模式下配置,端口才能使用外部中断/事件线

5.备用功能

当端口配置为 AFIO时,每个端口可以配置 16 个备用功能

6.输入结构图

GPIO输入结构

输入时配备了若上拉和下拉,并且有ESD保护

7.输出结构图

GPIO输出结构

8.模拟配置

GPIO模拟配置结构

配置为模拟模式时,弱上拉和下拉电阻禁用

9.GPIO 锁定功能

  GPIO 的锁定机制可以保护 I/O 端口的配置
  被保护的寄存器有:GPIOx_CTL,GPIOx_OMODE,GPIOx_OSPD,GPIOx_PUD 和GPIOx_AFSELz(z=0,1)。通过配置 32 位锁定寄存器(GPIOx_LOCK)可以锁定 I/O 端口的配置。通过特定的锁定序列配置 GPIOx_LOCK 中的 LKK 位和 LKy 位,相应的端口位被锁定,直到下一个复位前,相应端口位的配置都不能修改。建议在电源驱动模块的配置中使用锁定功能。

10.GPIO 单周期输出翻转功能

  GPIO 可以在一个 AHB 时钟周期内翻转 I/O 的输出电平。输出信号的频率可以达到 AHB 时钟的一半

11.GPIO基地址

| IO口 | 基地址 |
| —– | ———– |
| GPIOA | 0x4002 0000 |
| GPIOB | 0x4002 0400 |
| GPIOC | 0x4002 0800 |
| GPIOD | 0x4002 0C00 |
| GPIOE | 0x4002 1000 |
| GPIOF | 0x4002 1400 |
| GPIOG | 0x4002 1800 |
| GPIOH | 0x4002 1C00 |
| GPIOI | 0x4002 2000 |

12.各寄存器及地址偏移

| 寄存器 | 寄存器功能 | 偏移 | 复位值 |
| ———— | ——————– | —- | ———————————————————— |
| GPIOx_CTL | 端口控制寄存器 | 0x00 | 端口 A 0xA800 0000;端口 B 0x0000 0280;其他端口 0x0000 0000 |
| GPIOx_OMODE | 端口输出模式寄存器 | 0x04 | 0x0000 0000 |
| GPIOx_OSPD | 端口输出速度寄存器 | 0x08 | 端口 A 0x0C00 0000;端口 B 0x0000 00C0;其他端口 0x0000 0000 |
| GPIOx_PUD | 端口上拉/下拉寄存器 | 0x0C | 端口 A 0x6400 0000;端口 B 0x0000 0100;其他端口 0x0000 0000 |
| GPIOx_ISTAT | 端口输入状态寄存器 | 0x10 | 0x0000 XXXX |
| GPIOx_OCTL | 端口输出控制寄存器 | 0x14 | 0x0000 0000 |
| GPIOx_BOP | 端口位操作寄存器 | 0x18 | 0x0000 0000 |
| GPIOx_LOCK | 端口配置锁定寄存器 | 0x1C | 0x0000 0000 |
| GPIOx_AFSEL0 | 备用功能选择寄存器 0 | 0x20 | 0x0000 0000 |
| GPIOx_AFSEL1 | 备用功能选择寄存器 1 | 0x24 | 0x0000 0000 |
| GPIOx_BC | 位清除寄存器 | 0x28 | 0x0000 0000 |
| GPIOx_TG | 端口位翻转寄存器 | 0x2C | 0x0000 0000 |

13.初始化例程

13.1 初始化LED输出例程

void led_init(void)
{
    rcu_periph_clock_enable(RCU_GPIOE);                                           // 初始化GPIOE时钟
    gpio_mode_set(GPIOE, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_PIN_2);           // 初始化PE2为浮空输出
    gpio_output_options_set(GPIOE, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_2); // 初始化PE2为推挽输出,速度为50MHz
    GPIO_BC(GPIOE) = GPIO_PIN_2;                                                  // PE2复位
}

13.2 初始化按键输入例程

void key_init(void)
{
    rcu_periph_clock_enable(RCU_GPIOA);                                // 初始化GPIOA时钟
    rcu_periph_clock_enable(RCU_SYSCFG);                               // 初始化SYSCFG时钟
    gpio_mode_set(GPIOA, GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_PIN_0); // 设置PA0为浮空输入

    nvic_irq_enable(EXTI0_IRQn, 2U, 0U);                          // 配置外部中断0中断优先级
    syscfg_exti_line_config(EXTI_SOURCE_GPIOA, EXTI_SOURCE_PIN0); // 配置GPIO口对应的外部中断口
    exti_init(EXTI_0, EXTI_INTERRUPT, EXTI_TRIG_FALLING);         // 配置外部中断0触发方式为下降沿
    exti_interrupt_flag_clear(EXTI_0);                            // 清除EXTI线0中断标志位
}

14.GPIO库函数汇总

| 库函数名称 | 库函数名称 |
| ———————– | ———————- |
| gpio_deinit | 复位外设GPIOx |
| gpio_mode_set | 设置GPIO模式 |
| gpio_output_options_set | 设置GPIO输出模式和速度 |
| gpio_bit_set | 置位引脚值 |
| gpio_bit_reset | 复位引脚值 |
| gpio_bit_write | 将特定的值写入引脚 |
| gpio_port_write | 将特定的值写入一组端口 |
| gpio_input_bit_get | 获取引脚的输入值 |
| gpio_input_port_get | 获取一组端口的输入值 |
| gpio_output_bit_get | 获取引脚的输出值 |
| gpio_output_port_get | 获取一组端口的输出值 |
| gpio_af_set | 设置GPIO复用功能 |
| gpio_pin_lock | 相应的引脚配置被锁定 |
| gpio_bit_toggle | 翻转GPIO引脚状态 |
| gpio_port_toggle | 翻转一组GPIO状态 |

三、通用同步异步收发器(USART)

1.简介

  数据帧可以通过全双工或半双工,同步或异步的方式进行传输,可支持红外编码规范,SIR,智能卡协议,LIN,以及同步单双工模式,支持多处理器通信和Modem流控操作(CTS/RTS)。数据帧支持从LSB或者MSB开始传输。支持DMA传输。

多种状态标志

  • 接收缓冲区不为空(RBNE),发送缓冲区为空(TBE),传输完成(TC),忙(BSY)
  • 过载错误(ORERR),噪声错误(NERR),帧格式错误(FERR),奇偶校验错误(PERR)
  • 硬件流控操作标志:CTS变化(CTSF)
  • LIN模式标志:LIN断开检测(LBDF)
  • 多处理器通信模式标志:IDLE帧检测(IDLEF)
  • 智能卡模式标志:块结束(EBF)和接收超时(RTF)
  • 若相应的中断使能,这些事件发生将会触发中断

USART0/1/2/5完全实现上述功能,但是UART3/4/6/7只实现了上面所介绍功能的部分

2.内部框图

GD32F4XX_USART内部框图

3.时钟源

USART0/5的系统时钟为PCLK2,USART1/2和UART3/4/6/7的系统时钟为PCLK1。在使能USART之前,必须在时钟控制单元使能系统时钟。

USART_时钟树

  • PCLK1的时钟来源于APB1,APB2最高频率为120MHz

  • PCLK2的时钟来源于APB2,APB2最高频率为60MHz

  • APB1和APB2时钟均由AHB总线分频获得

4.USART发送步骤

  1. 在USART_CTL0寄存器中置位UEN位,使能USART;

  2. 通过USART_CTL0寄存器的WL设置字长;

  3. 在USART_CTL1寄存器中写STB[1:0]位来设置停止位的长度;

  4. 如果选择了多级缓存通信方式,应该在USART_CTL2寄存器中使能DMA (DENT位);

  5. 在USART_BAUD寄存器中设置波特率;

  6. 在USART_CTL0寄存器中设置TEN位;

  7. 等待TBE置位;

  8. 向USART_DATA寄存器写数据;

  9. 若DMA未使能,每发送一个字节都需重复步骤7-8;

  10. 等待TC=1,发送完成

USART_发送

5.USART接收步骤

  1. 在USART_CTL0寄存器中置位UEN位,使能USART;
  2. 写USART_CTL0寄存器的WL去设置字长;
  3. 在USART_CTL1寄存器中写STB[1:0]位来设置停止位的长度;
  4. 如果选择了多级缓存通信方式,应该在USART_CTL2寄存器中使能DMA(DENR位);
  5. 在USART_BAUD寄存器中设置波特率;
  6. 在USART_CTL0中设置REN位

USART_过采样接收图

6.DMA 方式访问数据缓冲区

6.1 DMA发送

  1. 将USART_STAT0中TC清0
  2. 将 USART_DATA的地址设置为 DMA目的地址
  3. 将存放数据的片内SRAM地址设置为DMA源地址
  4. 将要传输的数据字节数设置为DMA 传输字节数
  5. DMA其他设置,中断使能,校验位设置等
  6. 使能用于USART的DMA通道
  7. 等待TC置位

6.2 DMA接收

  1. 将USART_DATA的地址设置为DMA源地址
  2. 将存放数据的片内SRAM地址设置为DMA目的地址
  3. 将要传输的数据字节数设置为DMA 传输字节数
  4. DMA其他设置,中断使能,校验位设置等
  5. 使能用于USART的DMA通道

7.硬件控制流

7.1 RTS流控

  USART接收器输出nRTS,它用于反映接收缓冲区状态。当一帧数据接收完成,nRTS变成高电平,这样是为了阻止发送器继续发送下一帧数据。当接收缓冲区满时,nRTS保持高电平,可以通过读USART_DATA寄存器来清零。

7.2 CTS流控

  USART发送器监视nCTS输入引脚来决定数据帧是否可以发送。如果USART_STAT0寄存器中TBE位是0且nCTS为低电平,发送器发送数据帧。在发送期间,若nCTS信号变为高电平,发送器将会在当前数据帧发送完成后停止发送。

7.3 硬件流控制图

USART_硬件流控图

8.多处理器通信

USART可以进入静默模式,通过空闲总线检测和地址掩码检测唤醒。

9.同步通信模式

USART可以使用额外的一根同步线作为时钟频率线,采用同步模式时,接收器在时钟捕获沿采样数据,并无任何过采样。

10.串行红外(IrDA SIR)编解码功能模块

  在IrDA模式下,USART数据帧由SIR发送编码器进行调制,调制后的信号经由红外LED进行发送,经解调后将数据发送至USART接收器。对于编码器而言,波特率应小于115200。

11.半双工工作模式

在半双工通信模式下,USART_CTL1寄存器的LMEN,CKEN位和USART_CTL2寄存器的SCEN,IREN位清零。
半双工模式下,TX引脚和RX引脚将从内部连接到一起,RX引脚不再使用。TX引脚应该被配置为开漏输出模式。通信冲突由软件处理。

12.USART 中断

| 中断事件 | 事件标志 | 控制寄存器 | 使能控制位 |
| ————————————————— | ——————— | ———- | ———- |
| 发送数据寄存器空 | TBE | USART_CTL0 | TBEIE |
| CTS 标志 | CTSF | USART_CTL2 | CTSIE |
| 发送结束 | TC | USART_CTL0 | TCIE |
| 接收到的数据可以读取 | RBNE | USART_CTL0 | RBNEIE |
| 检测到过载错误 | ORERR | USART_CTL0 | RBNEIE |
| 检测到线路空闲 | IDLEF | USART_CTL0 | IDLEIE |
| 奇偶校验错误 | PERR | USART_CTL0 | PERRIE |
| LIN模式下,检测到断开标志 | LBDF | USART_CTL1 | LBDIE |
| 接收超时错误 | RTF | USART_CTL3 | RTIE |
| 发现块尾 | EBF | USART_CTL3 | EBIE |
| 接收错误(噪声错误、溢出错误、帧错误)当DMA接收使能时 | NERR or ORERR or FERR | USART_CTL2 | ERRIE |

在任何时候 USART 只能向控制器产生一个中断请求,但是软件可以在一个中断服务程序里处理多个中断事件

13.USART 寄存器

| 寄存器 | 寄存器功能 | 偏移 | 复位值 |
| ———– | ———————— | —- | ————— |
| USART_STAT0 | 状态寄存器 0 | 0x00 | 0x0000 000000C0 |
| USART_DATA | 数据寄存器 | 0x04 | 未定义 |
| USART_BAUD | 波特率寄存器 | 0x08 | 0x0000 0000 |
| USART_CTL0 | 控制寄存器 0 | 0x0C | 0x0000 0000 |
| USART_CTL1 | 控制寄存器 1 | 0x10 | 0x0000 0000 |
| USART_CTL2 | 控制寄存器 2 | 0x14 | 0x0000 0000 |
| USART_GP | 保护时间和预分频器寄存器 | 0x18 | 0x0000 0000 |
| USART_CTL3 | 控制寄存器 3 | 0x80 | 0x0000 0000 |
| USART_RT | 接收超时寄存器 | 0x84 | 0x0000 0000 |
| USART_STAT1 | 状态寄存器 1 | 0x88 | 0x0000 0000 |
| USART_CHC | 兼容性控制寄存器 | 0xC0 | 0x0000 0000 |

14.初始化例程

14.1 串口0初始化例程

void usart0_init(void)
{
    rcu_periph_clock_enable(RCU_GPIOA);  // 初始化GPIOA时钟
    rcu_periph_clock_enable(RCU_USART0); // 初始化USART0时钟

    gpio_af_set(GPIOA, GPIO_AF_7, GPIO_PIN_9);  // 配置PA9复用为TX
    gpio_af_set(GPIOA, GPIO_AF_7, GPIO_PIN_10); // 配置PA10复用为RX

    gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_9);              // 配置PA9为复用输出,上拉
    gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9);  // 配置PA9为推挽输出,速度50MHz
    gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_10);             // 配置PA10为复用输出,上拉
    gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_10); // 配置PA10为推挽输出,速度50MHz

    usart_deinit(USART0);                              // 重置USART0
    usart_baudrate_set(USART0, 115200U);                  // 配置USART0 波特率为115200
    usart_receive_config(USART0, USART_RECEIVE_ENABLE);   // 使能串口接收
    usart_transmit_config(USART0, USART_TRANSMIT_ENABLE); // 使能串口发送
    usart_enable(USART0);                              // 使能USART0
}

14.2 串口接收中断例程

void USART0_IRQHandler(void)
{
    // 检测串口接收中断
    if ((RESET != usart_interrupt_flag_get(USART0, USART_INT_FLAG_RBNE)) &&
        (RESET != usart_flag_get(USART0, USART_FLAG_RBNE)))
    {
        rxbuffer[rxcount++] = (usart_data_receive(USART0) & 0x7F); // 接收串口数据
    }
    // 检测串口空闲中断
    if ((RESET != usart_flag_get(USART0, USART_FLAG_TBE)) &&
        (RESET != usart_interrupt_flag_get(USART0, USART_INT_FLAG_TBE)))
    {
        // 将接收的数据发送出去
        usart_data_transmit(USART0, txbuffer[txcount++]);
        if (txcount >= rxcount)
        {
            usart_interrupt_disable(USART0, USART_INT_TBE); // 所有数据发送完成后,关闭串口空闲中断
        }
    }
}

14.3 串口DMA初始化

void usart0_dma_init(void)
{
    dma_single_data_parameter_struct dma_init_struct;              // 初始化DMA结构体
    rcu_periph_clock_enable(RCU_DMA1);                             // 初始化DMA1时钟
    dma_deinit(DMA1, DMA_CH7);                                     // 复位DMA1 通道7
    dma_init_struct.direction = DMA_MEMORY_TO_PERIPH;              // 选择从内存到外设,用于DMA串口发送
    dma_init_struct.memory0_addr = (uint32_t)txbuffer;             // 配置发送内存地址
    dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE;       // 打开DMA地址自增
    dma_init_struct.periph_memory_width = DMA_PERIPH_WIDTH_8BIT;   // DMA传输带宽为8位
    dma_init_struct.number = buffer_size;                          // 配置DMA传输数据的大小
    dma_init_struct.periph_addr = (uint32_t)&USART_DATA(USART0);   // 配置外设地址
    dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE;      // 禁止外设地址自增
    dma_init_struct.priority = DMA_PRIORITY_ULTRA_HIGH;            // 超高优先级
    dma_single_data_mode_init(DMA1, DMA_CH7, &dma_init_struct);    // 初始化DMA1通道7数据模式
    dma_circulation_disable(DMA1, DMA_CH7);                        // 禁止DMA循环模式
    dma_channel_subperipheral_select(DMA1, DMA_CH7, DMA_SUBPERI4); // DMA通道外设选择
    dma_channel_enable(DMA1, DMA_CH7);                             // 使能DMA
}

此处的初始化仅为DMA发送的初始化程序,接收部分选择方向与发送相反,即从外设到内存

14.4 串口DMA发送

void usart0_dma_send(uint8_t *buffer, uint16_t len)
{
    dma_channel_disable(DMA1, DMA_CH7);
    dma_memory_address_config(DMA1, DMA_CH7, (uint32_t)buffer); // 设置要发送数据的内存地址
    dma_transfer_number_config(DMA1, DMA_CH7, len);             // 一共发多少个数据
    dma_channel_enable(DMA1, DMA_CH7);                          // 使能DMA通道
    usart_dma_transmit_config(USART0, USART_DENT_ENABLE);       // 使能串口DMA发送
}

串口DMA接收可通过DMA半满、全满中断或串口空闲中断获取DMA的接收状态信息,然后直接对存储地址进行操作即可

14.5 printf函数重写

int fputc(int ch, FILE *f)
{
    while (RESET == usart_flag_get(USART0, USART_FLAG_TBE)); // 等待上一次发送完成
    usart_data_transmit(USART0, (uint32_t)ch); // 串口0发送
    return ch;
}

14.6 设置串口接收超时

usart_receiver_timeout_enable(USART0);                       // 使能串口接收超时
usart_receiver_timeout_threshold_config(USART0, 115200*3);   // 配置串口接收超时阈值

15.库函数汇总

| 库函数名称 | 库函数描述 |
| —————————————- | ———————————————————— |
| usart_deinit | 复位外设USART |
| usart_baudrate_set | 配置USART波特率 |
| usart_parity_config | 配置USART奇偶校验 |
| usart_word_length_set | 配置USART字长 |
| usart_stop_bit_set | 配置USART停止位 |
| usart_enable | 使能USART |
| usart_disable | 失能USART |
| usart_transmit_config | USART发送配置 |
| usart_receive_config | USART接收配置 |
| usart_data_first_config | 配置数据传输时低位在前或高位在前 |
| usart_invert_config | 配置USART反转功能 |
| usart_overrun_enable | 使能USART溢出禁止功能 |
| usart_overrun_disable | 失能USART溢出禁止功能 |
| usart_oversample_config | 配置USART过采样模式 |
| usart_sample_bit_config | 配置USART单次采样方式 |
| usart_receiver_timeout_enable | 使能USART接收超时 |
| usart_receiver_timeout_disable | 失能USART接收超时 |
| usart_receiver_timeout_threshold_con fig | 设置USART接收超时阈值 |
| usart_data_transmit | USART发送数据功能 |
| usart_data_receive | USART接收数据功能 |
| usart_autobaud_detection_enable | 使能USART自动波特率检测 |
| usart_autobaud_detection_disable | 失能USART自动波特率检测 |
| usart_autobaud_detection_mode_con fig | 配置USART自动波特率检测模式 |
| usart_address_config | 在地址掩码唤醒模式下配置USART地址 |
| usart_address_detection_mode_confi g | 配置USART地址检测模式 |
| usart_mute_mode_enable | 使能USART静默模式 |
| usart_mute_mode_disable | 失能USART静默模式 |
| usart_mute_mode_wakeup_config | 配置USART静默模式唤醒方式 |
| usart_lin_mode_enable | 使能USART LIN模式 |
| usart_lin_mode_disable | 失能USART LIN模式 |
| usart_lin_break_detection_length_con fig | 配置USART LIN模式中断帧长度 |
| usart_halfduplex_enable | 使能USART半双工模式 |
| usart_halfduplex_disable | 失能USART半双工模式 |
| usart_clock_enable | 使能USART CK引脚 |
| usart_clock_disable | 失能USART CK引脚 |
| usart_synchronous_clock_config | 配置USART同步通讯模式参数 |
| usart_guard_time_config | 在USART智能卡模式下配置保护时间值 |
| usart_smartcard_mode_enable | 使能USART智能卡模式 |
| usart_smartcard_mode_disable | 失能USART智能卡模式 |
| usart_smartcard_mode_nack_enable | 在USART智能卡模式下使能NACK |
| usart_smartcard_mode_nack_disable | 在USART智能卡模式下失能NACK |
| usart_smartcard_mode_early_nack_e nable | 使能USART智能卡模式提前NACK |
| usart_smartcard_mode_early_nack_di sable | 失能USART智能卡模式提前NACK |
| usart_smartcard_autoretry_config | 配置智能卡自动重试次数 |
| usart_block_length_config | 配置智能卡T=1的接收时块的长度 |
| usart_irda_mode_enable | 使能USART串行红外编解码功能模块 |
| usart_irda_mode_disable | 失能USART串行红外编解码功能模块 |
| usart_prescaler_config | 在USART IrDA低功耗模式下或者SmartCard模式配置外设时 钟分频系数 |
| usart_irda_lowpower_config | 配置USART IrDA低功耗模式 |
| usart_hardware_flow_rts_config | 配置USART RTS硬件控制流 |
| usart_hardware_flow_cts_config | 配置USART CTS硬件控制流 |
| usart_rs485_driver_enable | 使能USART rs485驱动 |
| usart_rs485_driver_disable | 失能USART rs485驱动 |
| usart_driver_assertime_config | 配置USART驱动使能置位时间 |
| usart_driver_deassertime_config | 配置USART驱动使能置低时间 |
| usart_depolarity_config | 配置USART驱动使能极性模式 |
| usart_dma_receive_config | 配置USART DMA接收 |
| usart_dma_transmit_config | 配置USART DMA发送 |
| usart_reception_error_dma_disable | USART接收错误时禁能DMA |
| usart_reception_error_dma_enable | USART接收错误时使能DMA |
| usart_wakeup_enable | 使能USART唤醒 |
| usart_wakeup_disable | 失能USART唤醒 |
| usart_wakeup_mode_config | 配置USART唤醒模式 |
| usart_command_enable | 使能USART请求 |
| usart_receive_fifo_enable | 使能接收FIFO |
| usart_receive_fifo_disable | 失能接收FIFO |
| usart_receive_fifo_counter_number | 读取接收FIFO计数器的值 |
| usart_flag_get | 得到STAT/RFCS寄存器中的标志 |
| usart_flag_clear | 清除USART状态 |
| usart_interrupt_enable | 使能USART中断 |
| usart_interrupt_disable | 失能USART中断 |
| usart_interrupt_flag_get | 得到USART中断和标志状态 |
| usart_interrupt_flag_clear | 清除USART中断标志位 |

四、内部集成电路总线接口(I2C)

1.简介

I2C(内部集成电路总线)模块提供了符合工业标准的两线串行制接口,使用两条串行线:串行数据线 SDA 和串行时钟线 SCL。

具备CRC校验功能、支持 SMBus(系统管理总线)、PMBus(电源管理总线)和 SAM_V(验证安全控制模块)模式,支持 DMA 模式。

  • 同一接口既可实现主机功能又可实现从机功能
  • 主从机之间的双向数据传输
  • 支持标速模式(最高 100 kHz)和快速模式(最高 400 kHz)
  • 支持 DMA 模式

2.I2C程序框图

I2C程序框图

I2C时钟来自于APB总线

3.SDA和SCL线

SDA:串行数据线

SCL:串行时钟线

当总线空闲时,两条线都是高电平,逻辑‘0’和逻辑‘1’的电平并不是固定的,取决于 VDD 的实际电平。

3.1 开始信号和停止信号

所有的数据传输起始于一个 START 结束于一个 STOP

  • START 信号定义:在 SCL 为高时,SDA 线上出现一个从高到低的电平转换

  • STOP 信号定义:在 SCL 为高时,SDA 线上出现一个从低到高的电平转换

I2C起始信号和终止信号

3.2 同步时钟

  SCL 线的高到低切换会使器件开始计数它们的低电平周期,而且当主机的时钟变低电平时,它会使 SCL 线保持这种状态直到到达时钟的高
电平

  但是如果另一个时钟仍处于低电平周期,这个时钟的低到高切换不会改变 SCL 线的状态。因此 SCL 线被有最长低电平周期的器件保持低电平

4.I2C通信流程

每个I2C设备都通过唯一的地址进行识别,根据设备功能,他们既可以是发送器也可作为接收器。

  1. I2C从机检测到I2C总线上的起始信号
  2. 接收总线地址并与自身地址比较
  3. 如果地址相同,则发送一个ACK,并响应总线上的后续命令

主机负责产生起始和终止信号,并负责产生SCL时钟。

4.1 主机发送流程

I2C主机发送流程

4.2 主机接收流程

I2C主机接收流程

5.软件编程模型

  系统复位以后,I2C默认工作在从机模式下。通过软件配置使I2C在总线上发送一个START信号之后,I2C变为主机模式,软件配置在I2C总线上发送STOP信号
后,I2C又变回从机模式。

5.1从机发送模式下的软件流程

  1. 使能I2C外设时钟,配置I2C时序,默认I2C处于从机模式,等待总线上的START信号

  2. 进入发送状态,写入数据到I2C_DATA寄存器,数据将被移入内部位移寄存器,位移 寄存器清空,I2C开始发送数据到I2C总线

  3. 在第一个字节发送期间,可以向I2C_DATA寄存器中写第二个字节

  4. 第一个字节发送完成后,TBE再次置起,可以写第三个字节到I2C_DATA,在此之后,任何时候TBE被置1,只要依然有数据待被发送,软件都可以

    写入一个字节到I2C_DATA寄存器

  5. 倒数第二个字节发送期间,软件写最后一个数据到I2C_DATA寄存器来清除TBE标志位,之后就再不用关心TBE的状态。TBE位会在倒数第二个字节发送完成后置起,直到检测到STOP信号时被清0

  6. 在最后一个字节发送结束后,I2C从机的AERR(应答错误)会置起以通知软件发送结束

I2C从机发送模式

5.2从机接收模式下的软件流程

  1. 首先,软件应该使能I2C外设时钟,以及配置I2C_CTL1中时钟相关寄存器来确保正确的I2C时序。使能和配置以后,I2C运行在默认的从机模式状态,等待START信号以及地址
  2. 在接收到START起始信号和匹配的7位或10地址之后,I2C硬件将I2C状态寄存器0的ADDSEND位置1,此位应该通过软件轮询或者中断来检测,发现置起后,软件通过先读I2C_STAT0寄存器然后读I2C_STAT1寄存器来清除ADDSEND位。当ADDSEND位被清0时,I2C就开始接收来自I2C总线的数据
  3. 当接收到第一个字节时,RBNE位被硬件置1,软件可以读取I2C_DATA寄存器的第一个字节,此时RBNE位也被清0
  4. 任何时候RBNE被置1,软件可以从I2C_DATA寄存器读取一个字节
  5. 接收到最后一个字节后,RBNE被置1,软件可以读取最后的字节
  6. 当I2C检测到I2C总线上一个STOP信号,STPDET位被置1,软件通过先读I2C_STAT0寄存器再写I2C_CTL0寄存器来清除STPDET位

I2C从机接收模式

5.3 主机发送模式下的软件流程

  1. 首先,软件应该使能I2C外设时钟,以及配置I2C_CTL1中时钟相关寄存器来确保正确的I2C时序。使能和配置以后,I2C运行在默认的从机模式状态,等待START信号,随后等待I2C总线寻址。

  2. 软件将START位置1,在I2C总线上产生一个START信号。

  3. 发送一个START信号后,I2C硬件将I2C_STAT0的SBSEND位置1然后进入主机模式。现在软件应该读I2C_STAT0寄存器然后写一个7位地址位或10位地址的地址头到I2C_DATA寄存器来清除SBSEND位。当SBSEND位被清0时,I2C就开始发送地址或者地址头到I2C总线。如果发送的地址是10位地址的地址头,硬件在发送地址头的时候会将ADD10SEND位置1,软件应该通过读I2C_STAT0寄存器然后写10位低地址到I2C_DATA来清除ADD10SEND位

  4. 7位或10位的地址位发送出去之后,I2C硬件将ADDSEND位置1,软件通过读I2C_STAT0寄存器然后读I2C_STAT1寄存器清除ADDSEND位。

  5. I2C进入数据发送状态,因为移位寄存器和数据寄存器I2C_DATA都是空的,所以硬件将TBE位置1。此时软件可以写第一个字节数据到I2C_DATA寄存器,但是TBE位此时不会被清零,因为写入I2C_DATA寄存器的字节会被立即移入内部移位寄存器。当移位寄存器非空时,I2C就开始发送数据到总线。

  6. 在第一个字节的发送过程中,软件可以写第二个字节到I2C_DATA,此时TBE会被清零,因为I2C_DATA寄存器和移位寄存器都不为空。

  7. 任意时刻TBE被置1,软件都可以向I2C_DATA寄存器写入一个字节,只要还有数据待发送。

  8. 在倒数第二个字节发送过程中,软件写入最后一个字节数据到I2C_DATA来清除TBE标志位,此后就不用关心TBE位的状态。TBE位会在倒数第二个字节发送完成后被置起,直到发送STOP信号时被清零。

  9. 最后一个字节发送结束后,I2C主机将BTC位置起,因为移位寄存器和I2C_DATA寄存器此时都为空。软件此时应该配置STOP来发送一个STOP信号,此后TBE和BTC状态位都将被清0。

I2C主机发送模式

5.4 主机接收模式下的软件流程

方案A:

  1. 首先,软件应该使能I2C外设时钟,以及配置I2C_CTL1中时钟相关寄存器来确保正确的I2C时序。使能和配置以后,I2C运行在默认的从机模式状态,等待START信号,随后等待I2C总线寻址。

  2. 软件将START位置1,从而在I2C总线上产生一个START信号。

  3. 发送一个START信号后,I2C硬件将I2C_STAT0寄存器的SBSEND位置1然后进入主机模式。现在软件应该读I2C_STAT0寄存器然后写一个7位地址位或10位地址的地址头到I2C_DATA寄存器来清除SBSEND位。当SBSEND位被清0时,I2C就开始发送地址或者地址头到I2C总线。如果发送的地址是10位地址的地址头,硬件在发送地址头的时候会先将ADD10SEND位置1,软件应该通过读I2C_STAT0寄存器然后写10位低地址到I2C_DATA来清除ADD10SEND位。

  4. 7位或10位的地址位发送出去之后,I2C硬件将ADDSEND位置1,软件应该通过读I2C_STAT0寄存器然后读I2C_STAT1寄存器清除ADDSEND位。如果地址是10位格式,软件应该再次将START位置1来重新产生一个START。在START产生后,SBSEND位会被置1。软件应该通过先读I2C_STAT0然后写地址头到I2C_DATA来清除SBSEND位,然后地址头被发到I2C总线,ADDSEND再次被置1。软件应该再次通过先读I2C_STAT0然后读I2C_STAT1来清除ADDSEND位。

  5. 当接收到第一个字节时,硬件会将RBNE位置1。此时软件可以从I2C_DATA寄存器读取第一个字节,之后RBNE位被清0。

  6. 此后任何时候RBNE被置1,软件就可以从I2C_DATA寄存器读取一个字节。

  7. 接收完倒数第二个字节(N-1)数据之后,软件应该立即将ACKEN位清0,并将STOP位置1,这一过程需要在最后一个字节接收完毕之前完成,以确保NACK发送给最后一个字节。

  8. 最后一个字节接收完毕后,RBNE位被置1,软件可以读取最后一个字节。由于ACKEN已经在前一步骤中被清0,I2C不再为最后一个字节发送ACK,并在最后一个字节发送完毕后产生一个STOP信号。

以上步骤要求字节数目N>1,如果N=1,步骤7应该在步骤4之后就执行,且需要在字节接收完
成之前完成。

I2C主机接收方案A

方案B:

  1. 首先,软件应该使能I2C外设时钟,配置I2C_CTL1中时钟相关寄存器来确保正确的I2C时序。初始化完成之后,I2C运行在默认的从机模式状态,等待START信号和地址。

  2. 软件将START位置1,从而在I2C总线上产生一个START信号。

  3. 发送一个START信号后,I2C硬件将I2C_STAT0寄存器的SBSEND位置1然后进入主机模式。现在软件应该读I2C_STAT0寄存器然后写一个7位地址位或10位地址的地址头到I2C_DATA寄存器来清除SBSEND位。当SBSEND位被清0时,I2C就开始发送地址或者地址头到I2C总线。如果发送的地址是10位地址的地址头,硬件在发送地址头之后会将ADD10SEND位置1,软件应该通过读I2C_STAT0寄存器然后写10位低地址到I2C_DATA来清除ADD10SEND位。

  4. 7位或10位的地址位发送出去之后,I2C硬件将ADDSEND位置1,软件应该通过读I2C_STAT0寄存器然后读I2C_STAT1寄存器清除ADDSEND位。如果地址是10位格式,软件应该接着将START位再次置1来产生一个开始信号,START被发送出去以后SBSEND位被再次置1。软件应该通过先读I2C_STAT0然后写地址头到I2C_DATA来清除SBSEND位,然后地址头被发到I2C总线,ADDSEND再次被置1。软件应该再次通过先读I2C_STAT0然后读I2C_STAT1来清除ADDSEND位。

  5. 当第一个字节被接收时,RBNE位会被硬件置1。此时软件可从I2C_DATA寄存器读取出第一个字节,同时RBNE位被清0。

  6. 此后任何时刻,只要RBNE位被置1,软件就可以从I2C_DATA寄存器读取一个字节的数据,直到主机接收了N-3个字节。

第 N-2 个字节还没被软件读出,之后第 N-1 个字节被接收,此时 BTC 和 RBNE 都被置位,总线就会被主机锁死以阻止最后一个字节的接收。然后软件应该清除 ACKEN 位

  1. 软件从I2C_DATA读出倒数第三个(N-2)字节数据,同时也将BTC位清0。此后第N-1个字节从移位寄存器被移到I2C_DATA,总线得到释放然后开始接收最后一个字节,由于ACKEN已经被清除,因此主机不会给最后一个字节数据发送ACK响应。
  2. 最后一个字节接收完毕后,硬件再次把BTC位和RBNE置1,并拉低SCL,软件将STOP位置1,主机发出一个STOP信号。
  3. 软件读取第N-1个字节,清除BTC。此后最后一个字节从移位寄存器被移动到I2C_DATA。
  4. 软件读取最后一个字节,清除RBNE。以上步骤需要字节数字N>2,N=1和N=2的情况近似:
    N=1
    在第 4 步,软件应该在清除 ADDSEND 位之前将 ACKEN 位清 0,在清除 ADDSEND 位之后将 STOP 位置 1。当 N=1 时步骤 5 是最后一步。
    N=2
    在第2步,软件应该在START置1之前将POAP置1。在第4步,软件应该在清除ADDSEND位之前将ACKEN位清0。在第5步,软件应该一直等到BTC位被置1然后将STOP位置1且读取I2C_DATA两次

I2C主机接收方案B

6.DMA 模式下数据传输

I2C 的 DMA 功能可以在 TBE 或 RBNE 位置 1 时,自动进行一次写或读操作。

7.状态、错误和中断

7.1 事件状态标志位

| 事件标志位名称 | 说明 |
| ————– | ——————————— |
| SBSEND | 主机发送 START 信号 |
| ADDSEND | 地址发送和接收 |
| ADD10SEND | 10 位地址模式中地址头发送 |
| STPDET | 监测到 STOP 信号 |
| BTC | 字节发送结束 |
| TBE | 发送时 I2C_DATA 为空 |
| RBNE | 接收时 I2C_DATA 非空 |
| RFR | SAM_V 模式时检测到 rxframe 上升沿 |
| RFF | SAM_V 模式时检测到 rxframe 下降沿 |
| TFR | SAM_V 模式时检测到 txframe 上升沿 |
| TFF | SAM_V 模式时检测到txframe 下降沿 |

7.2 错误标志位

| 错误名称 | 说明 |
| ——– | ———————————– |
| BERR | 总线错误 |
| LOSTARB | 仲裁丢失 |
| OUERR | 当禁用 SCL 拉低后,发生了上溢或下溢 |
| AERR | 没有接收到应答 |
| PECERR | CRC 值不相同 |
| SMBTO | SMBus 模式下总线超时 |
| SMBALT | SMBus 警报 |

8.I2C寄存器

| 寄存器名称 | 寄存器描述 |
| ———- | ——————– |
| I2C_CTL0 | 控制寄存器0 |
| I2C_CTL1 | 控制寄存器1 |
| I2C_SADDR0 | 从机地址寄存器0 |
| I2C_SADDR1 | 从机地址寄存器1 |
| I2C_DATA | 传输缓冲区寄存器 |
| I2C_STAT0 | 传输状态寄存器0 |
| I2C_STAT1 | 传输状态寄存器1 |
| I2C_CKCFG | 时钟配置寄存器 |
| I2C_RT | 上升时间寄存器 |
| I2C_FMPCFG | 快速+ 模式配置寄存器 |

9.I2C库函数汇总

| 库函数名称 | 库函数描述 |
| ———————————- | ——————————— |
| i2c_deinit | 复位外设I2C |
| i2c_clock_config | 配置I2C时钟 |
| i2c_mode_addr_config | 配置I2C地址 |
| i2c_smbus_type_config | SMBus类型选择 |
| i2c_ack_config | 是否发送ACK |
| i2c_ackpos_config | 配置在接收模式下ACK和PEC的位置 |
| i2c_master_addressing | 主机发送从机地址 |
| i2c_dualaddr_enable | 双地址模式使能 |
| i2c_dualaddr_disable | 双地址模式禁能 |
| i2c_enable | 使能I2C模块 |
| i2c_disable | 关闭I2C模块 |
| i2c_start_on_bus | 在I2C总线上生成起始位 |
| i2c_stop_on_bus | 在I2C总线上生成停止位 |
| i2c_data_transmit | 发送数据 |
| i2c_data_receive | 接收数据 |
| i2c_dma_enable | I2C DMA模式使能 |
| i2c_dma_last_transfer_config | 配置下一个DMA EOT是否最后一次传输 |
| i2c_stretch_scl_low_config | 当从机数据没有准备好时是否拉低SCL |
| i2c_slave_response_to_gcall_config | 从机是否响应广播呼叫 |
| i2c_software_reset_config | 配置I2C软件复位 |
| i2c_pec_enable | 报文错误校验使能 |
| i2c_pec_transfer_enable | 传输PEC值使能 |
| i2c_pec_value_get | 获取报文错误校验值 |
| i2c_smbus_issue_alert | 通过SMBA引脚发送警告 |
| i2c_smbus_arp_enable | SMBus下ARP协议是否开启 |
| i2c_flag_get | 获取I2C标志位 |
| i2c_flag_clear | 清除I2C标志位 |
| i2c_interrupt_enable | 中断使能 |
| i2c_interrupt_disable | 中断除能 |
| i2c_interrupt_flag_get | 中断标志位获取 |
| i2c_interrupt_flag_clear | 中断标志位清除 |

10.硬件I2C例程

10.1 I2C初始化

void i2c_init(void)
{
    rcu_periph_clock_enable(RCU_GPIOB);    // 使能GPIOB时钟
    rcu_periph_clock_enable(RCU_I2C0);     // 使能I2C0时钟

    gpio_af_set(GPIOB, GPIO_AF_4, GPIO_PIN_6);    // 配置I2C0_SCL到PB6
    gpio_af_set(GPIOB, GPIO_AF_4, GPIO_PIN_7);    // 配置I2C0_SDA到PB7

    // 配置I2C所用的IO口
    gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_6);
    gpio_output_options_set(GPIOB, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_6);
    gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_7);
    gpio_output_options_set(GPIOB, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_7);

    i2c_clock_config(I2C0, 400000, I2C_DTCY_2);                                                // 配置I2C时钟
    i2c_mode_addr_config(I2C0, I2C_I2CMODE_ENABLE, I2C_ADDFORMAT_7BITS, I2C0_OWN_ADDRESS7);    // 配置I2C地址
    i2c_enable(I2C0);                                                                          // 使能I2C0
    i2c_ack_config(I2C0, I2C_ACK_ENABLE);                                                      // 使能I2C ACK
}

10.2 I2C主机写单字节

void i2c_byte_write(uint8_t p_buffer, uint8_t write_address)
{
    while (i2c_flag_get(I2C0, I2C_FLAG_I2CBSY)) {};        // 等待I2C总线空闲
    i2c_start_on_bus(I2C0);                                // 向I2C总线发送启动条件
    while (!i2c_flag_get(I2C0, I2C_FLAG_SBSEND)) {};       // 等待SBSEND位设置
    i2c_master_addressing(I2C0, 0xA0, I2C_TRANSMITTER);    // 发送从地址到I2C总线
    while (!i2c_flag_get(I2C0, I2C_FLAG_ADDSEND)) {};      // 等待ADDSEND位设置
    i2c_flag_clear(I2C0, I2C_FLAG_ADDSEND);                // 清除ADDSEND位
    while (SET != i2c_flag_get(I2C0, I2C_FLAG_TBE)) {};    // 等待发送寄存器空
    i2c_data_transmit(I2C0, write_address);                // 发送EEPROM的内部地址写:只有一个字节地址
    while (!i2c_flag_get(I2C0, I2C_FLAG_BTC)) {};          // 等待BTC位设置完成
    i2c_data_transmit(I2C0, p_buffer);                     // 发送要写入的字节
    while (!i2c_flag_get(I2C0, I2C_FLAG_BTC)) {};          // 等待BTC位设置完成
    i2c_stop_on_bus(I2C0);                                 // 向I2C总线发送停止位
    while (I2C_CTL0(I2C0) & 0x0200) {};                    // 等待停止条件完成
}

10.3 I2C主机写整页

void i2c_page_write(uint8_t *p_buffer, uint8_t write_address, uint8_t number_of_byte)
{
    while (i2c_flag_get(I2C0, I2C_FLAG_I2CBSY)) {};        // 等待I2C总线空闲
    i2c_start_on_bus(I2C0);                                // 向I2C总线发送启动条件
    while (!i2c_flag_get(I2C0, I2C_FLAG_SBSEND)) {};       // 等待SBSEND位设置
    i2c_master_addressing(I2C0, 0xA0, I2C_TRANSMITTER);    // 发送从地址到I2C总线
    while (!i2c_flag_get(I2C0, I2C_FLAG_ADDSEND)) {};      // 等待ADDSEND位设置
    i2c_flag_clear(I2C0, I2C_FLAG_ADDSEND);                // 清除ADDSEND位
    while (SET != i2c_flag_get(I2C0, I2C_FLAG_TBE)) {};    // 等待传输数据缓冲区为空
    i2c_data_transmit(I2C0, write_address);                // 发送i2c的内部地址写:只有一个字节地址
    while (!i2c_flag_get(I2C0, I2C_FLAG_BTC)) {};          // 等待BTC位设置完成
    while (number_of_byte--) {                             // 有数据要写的时候循环
        i2c_data_transmit(I2C0, *p_buffer);                // 发送i2c的数据
        p_buffer++;                                        // 指向要写入的下一个字节
        while (!i2c_flag_get(I2C0, I2C_FLAG_BTC)) {};      // 等待BTC位设置完成
    }
    i2c_stop_on_bus(I2C0);                 // 向I2C总线发送停止位
    while (I2C_CTL0(I2C0) & 0x0200) {};    // 等待停止条件完成
}

10.4 I2C主机读数据

void i2c_buffer_read(uint8_t *p_buffer, uint8_t read_address, uint16_t number_of_byte)
{
    while (i2c_flag_get(I2C0, I2C_FLAG_I2CBSY)) {};                       // 等待I2C总线空闲
    if (2 == number_of_byte) i2c_ackpos_config(I2C0, I2C_ACKPOS_NEXT);    // ACK配置
    i2c_start_on_bus(I2C0);                                               // 向I2C总线发送启动条件
    while (!i2c_flag_get(I2C0, I2C_FLAG_SBSEND)) {};                      // 等待SBSEND位设置
    i2c_master_addressing(I2C0, 0xA0, I2C_TRANSMITTER);                   // 发送从地址到I2C总线
    while (!i2c_flag_get(I2C0, I2C_FLAG_ADDSEND)) {};                     // 等待ADDSEND位设置
    i2c_flag_clear(I2C0, I2C_FLAG_ADDSEND);                               // 清除ADDSEND位
    while (SET != i2c_flag_get(I2C0, I2C_FLAG_TBE)) {};                   // 等待传输数据缓冲区为空
    i2c_enable(I2C0);                                                     // 使能I2C
    i2c_data_transmit(I2C0, read_address);                                // 发送EEPROM的内部地址
    while (!i2c_flag_get(I2C0, I2C_FLAG_BTC)) {};                         // 等待BTC位设置完成
    i2c_start_on_bus(I2C0);                                               // 向I2C总线发送启动条件
    while (!i2c_flag_get(I2C0, I2C_FLAG_SBSEND)) {};                      // 等待SBSEND位设置
    i2c_master_addressing(I2C0, 0xA0, I2C_RECEIVER);                      // 发送从地址到I2C总线
    if (number_of_byte < 3) i2c_ack_config(I2C0, I2C_ACK_DISABLE);        // 禁用ACK
    while (!i2c_flag_get(I2C0, I2C_FLAG_ADDSEND)) {};                     // 等待ADDSEND位设置
    i2c_flag_clear(I2C0, I2C_FLAG_ADDSEND);                               // 清除ADDSEND位
    if (1 == number_of_byte) i2c_stop_on_bus(I2C0);                       // 向I2C总线发送停止条件
    while (number_of_byte) {                                              // 当有数据要读取时
        if (3 == number_of_byte) {
            while (!i2c_flag_get(I2C0, I2C_FLAG_BTC)) {};    // 等待BTC位设置完成
            i2c_ack_config(I2C0, I2C_ACK_DISABLE);           // 禁用ACK
        }
        if (2 == number_of_byte) {
            while (!i2c_flag_get(I2C0, I2C_FLAG_BTC)) {};    // 等待BTC位设置完成
            i2c_stop_on_bus(I2C0);                           // 向I2C总线发送停止条件
        }
        // 等待RBNE位设置完毕并清除它
        if (i2c_flag_get(I2C0, I2C_FLAG_RBNE)) {
            *p_buffer = i2c_data_receive(I2C0);    // 从i2c中读取一个字节
            p_buffer++;                            // 指向将保存所读字节的下一个位置
            number_of_byte--;                      // 递减读字节计数器
        }
    }
    while (I2C_CTL0(I2C0) & 0x0200) {};                // 等待停止条件完成
    i2c_ack_config(I2C0, I2C_ACK_ENABLE);              // 使能ACK
    i2c_ackpos_config(I2C0, I2C_ACKPOS_CURRENT);       // ACK配置
    while (i2c_flag_get(I2C0, I2C_FLAG_I2CBSY)) {};    // 等待I2C总线空闲
}

10.5 I2C从机读数据

void i2c_slave_receive(void)
{
    while (!i2c_flag_get(I2C0, I2C_FLAG_ADDSEND)) {};    // 等待ADDSEND置1
    i2c_flag_clear(I2C0, I2C_FLAG_ADDSEND);              // 清除ADDSEND位
    for (i = 0; i < 16; i++) {
        while (!i2c_flag_get(I2C0, I2C_FLAG_RBNE)) {};    // 等待RBNE置1
        i2c_receiver[i] = i2c_data_receive(I2C0);         // 从I2C_DATA寄存器读数据
    }
    while (!i2c_flag_get(I2C0, I2C_FLAG_STPDET)) {};    // 等待STPDET置1
    i2c_enable(I2C0);                                   // 清除STPDET位
}

10.6 I2C从机写数据

void i2c_slave_transmitter(uint8_t data)
{
    while (!i2c_flag_get(I2C0, I2C_FLAG_ADDSEND)) {};    // 等待ADDSEND置1
    i2c_flag_clear(I2C0, I2C_FLAG_ADDSEND);              // 清除ADDSEND位
    while (!i2c_flag_get(I2C0, I2C_FLAG_TBE)) {};        // 等待发送数据寄存器清空
    i2c_data_transmit(I2C0, data);                       // 发送数据
    while (!i2c_flag_get(I2C0, I2C_FLAG_TBE)) {};        // 等待发送数据寄存器清空
    while (!i2c_flag_get(I2C0, I2C_FLAG_AERR)) {};       // 等待主机不回复ACK
    i2c_flag_clear(I2C0, I2C_FLAG_AERR);                 // 清AERR位
}

11.软件I2C

由于硬件I2C资源有限,在一部分速率要求不高的情况下可以使用软件I2C

11.1 软件I2C初始化

// IO方向设置宏定义
#define SDA_IN()                                                                                                       \
    {                                                                                                                  \
        gpio_mode_set(GPIOB, GPIO_MODE_INPUT, GPIO_PUPD_PULLUP, GPIO_PIN_7);                                           \
    }    // PB7输入模式
#define SDA_OUT()                                                                                                      \
    {                                                                                                                  \
        gpio_mode_set(GPIOB, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_PIN_7);                                          \
    }    // PB7输出模式

#define IIC_SCL PBout(6)    // SCL
#define IIC_SDA PBout(7)    // SDA

void soft_i2c_init(void)
{
    rcu_periph_clock_enable(RCU_GPIOB);    // 使能GPIOB时钟
    // 配置软件I2C使用的IO口
    gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_6);
    gpio_output_options_set(GPIOB, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_6);
    gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_7);
    gpio_output_options_set(GPIOB, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_7);
}

11.2 软件I2C产生起始信号

// 产生起始信号
void i2c_start(void)
{
    SDA_OUT();    // sda线输出
    IIC_SDA = 1;
    IIC_SCL = 1;
    __nop();
    __nop();
    __nop();
    __nop();
    IIC_SDA = 0;    // CLK线拉高时,SDA线拉低
    __nop();
    __nop();
    __nop();
    __nop();
    IIC_SCL = 0;    // 钳住I2C总线,准备发送或接收数据
}

11.3 软件I2C产生终止信号

// 产生终止信号
void i2c_stop(void)
{
    SDA_OUT();    // sda线输出
    IIC_SCL = 0;
    IIC_SDA = 0;    // CLK线拉高时,SDA线拉低
    __nop();
    __nop();
    __nop();
    __nop();
    IIC_SCL = 1;
    IIC_SDA = 1;    // 发送I2C总线结束信号
    __nop();
    __nop();
    __nop();
    __nop();
}

11.4 软件I2C等待应答信号

uint8_t i2c_wait_ack(void)
{
    uint8_t err_time = 0;
    SDA_IN();    // SDA设置为输入
    IIC_SDA = 1;
    __nop();
    IIC_SCL = 1;
    __nop();
    while (IIC_SDA) {
        err_time++;
        if (err_time > 250) {
            iic_stop();
            return 1;
        }
    }
    IIC_SCL = 0;    // 时钟输出0
    return 0;
}

11.5 软件I2C产生应答信号

void i2c_ack(void)
{
    IIC_SCL = 0;
    SDA_OUT();
    IIC_SDA = 0;
    __nop();
    __nop();
    IIC_SCL = 1;
    __nop();
    __nop();
    IIC_SCL = 0;
}

11.6 软件I2C不产生应答信号

void i2c_without_ack(void)
{
    IIC_SCL = 0;
    SDA_OUT();
    IIC_SDA = 1;
    __nop();
    __nop();
    IIC_SCL = 1;
    __nop();
    __nop();
    IIC_SCL = 0;
}

11.7 软件I2C发送单字节数据

void i2c_send_byte(uint8_t txd)
{
    uint8_t t;
    SDA_OUT();
    IIC_SCL = 0;    // 拉低时钟开始数据传输
    for (t = 0; t < 8; t++) {
        IIC_SDA = (txd & 0x80) >> 7;
        txd <<= 1;
        __nop();
        __nop();    // 对TEA5767这三个延时都是必须的
        IIC_SCL = 1;
        __nop();
        __nop();
        IIC_SCL = 0;
        __nop();
        __nop();
    }
}

11.8 软件IIC读单字节数据

uint8_t i2c_read_byte(uint8_t ack)
{
    uint8_t i, receive = 0;
    SDA_IN();    // SDA设置为输入
    for (i = 0; i < 8; i++) {
        IIC_SCL = 0;
        __nop();
        __nop();
        IIC_SCL = 1;
        receive <<= 1;
        if (IIC_SDA) receive++;
        __nop();
    }
    if (!ack)
        i2c_without_ack();    // 发送nACK
    else
        i2c_ack();    // 发送ACK
    return receive;
}