I2C软实现基于GD32F407VE的天空星的配置

I2C软实现基于GD32F407VE的天空星的配置

一、I2C协议基础

1.1 什么是I2C?

I2C(Inter-Integrated Circuit)是一种通用的串行通信总线协议,仅使用两根线(时钟线SCL数据线SDA)就能实现多个设备间的通信。在GD32F407VE天空星开发板上,我们可以通过软件模拟的方式实现I2C通信。

1.2 I2C通信模型

想象一个课堂场景

  • 主设备 = 老师
  • 从设备 = 学生
  • SCL = 老师的手拍节拍
  • SDA = 老师和学生对话的通道

通信流程示例:

  1. 老师敲黑板:"开始上课!"(起始信号)
  2. 老师点名:"张三,回答这个问题"(设备地址)
  3. 张三举手:"到!"(ACK响应)
  4. 老师和张三对话(数据传输)
  5. 老师敲桌子:"下课!"(停止信号)

二、硬件连接

2.1 天空星开发板引脚定义

c 复制代码
#ifndef __I2C0_SOFT_H__
#define __I2C0_SOFT_H__
#include "gd32f4xx.h"
#include "systick.h"
// PB6 - SCL
#define I2C0S_SCL_RCU     RCU_GPIOB
#define I2C0S_SCL_PORT    GPIOB
#define I2C0S_SCL_PIN     GPIO_PIN_6
// PB7 - SDA
#define I2C0S_SDA_RCU     RCU_GPIOB
#define I2C0S_SDA_PORT    GPIOB
#define I2C0S_SDA_PIN     GPIO_PIN_7

#define  FAST    1   // 0 标准 100k    1 快速  400k

void I2C0_soft_init();

/* 写数据到I2C设备指定寄存器 */
uint8_t I2C0_soft_write(uint8_t addr, uint8_t reg, uint8_t* dat, uint32_t len);

/* 从I2C设备指定寄存器读取数据 */
uint8_t I2C0_soft_read(uint8_t addr, uint8_t reg, uint8_t* dat, uint32_t len);

#endif

2.2 原理图连接

复制代码
天空星开发板      PCF8563模块
PB6(SCL)   ---   SCL
PB7(SDA)   ---   SDA
3.3V       ---   VCC
GND        ---   GND

注意 :SCL和SDA线需要接4.7kΩ上拉电阻到3.3V!

三、软件实现详解

3.1 引脚初始化

c 复制代码
void I2C0_soft_init() {
	// SCL SDA 开漏输出  电平使用默认的
	GPIO_output(I2C0S_SCL_RCU, I2C0S_SCL_PORT, I2C0S_SCL_PIN, GPIO_OTYPE_OD, 2); // 2 默认电平,配置完不做电平处理
	GPIO_output(I2C0S_SDA_RCU, I2C0S_SDA_PORT, I2C0S_SDA_PIN, GPIO_OTYPE_OD, 2); // 2 默认电平,配置完不做电平处理
}

为什么用开漏输出?

  • 支持"线与"功能,多个设备可以共享总线
  • 配合上拉电阻实现高电平
  • 避免总线冲突

3.2 时序控制宏

c 复制代码
//设置引脚的高低电平
#define I2C0S_SDA(bit)  gpio_bit_write(I2C0S_SDA_PORT, I2C0S_SDA_PIN, (bit) ? SET : RESET)
#define I2C0S_SCL(bit)  gpio_bit_write(I2C0S_SCL_PORT, I2C0S_SCL_PIN, (bit) ? SET : RESET)

// 标准模式 100Kbits/s, 快速模式 400Kbits/s
// 100Kbits/s -> 100 000 bit / 1 000 000us -> 1bit/10us  一个高一个低才是1bit
// 400Kbits/s -> 400 000 bit / 1 000 000us -> 4bit/10us -> 每1bit大概需要2.5us
#if !FAST
	#define I2C0S_DELAY()   delay_1us(5)	// 标准模式
#else
	#define I2C0S_DELAY()   delay_1us(1)	// 快速模式
#endif
#define I2C0S_SDA_STATE() gpio_input_bit_get(I2C0S_SDA_PORT, I2C0S_SDA_PIN) // 读取SDA电平

3.3 基本信号实现

起始信号
c 复制代码
// 开始信号
static void start() {
	I2C0S_SDA(1); // SDA 拉高
	I2C0S_DELAY();// 延时
	I2C0S_SCL(1);// SCL 拉高
	I2C0S_DELAY();// 延时
	I2C0S_SDA(0); // SDA 拉低
	I2C0S_DELAY();// 延时
	I2C0S_SCL(0);// SCL 拉低
	I2C0S_DELAY();// 延时
}
停止信号
c 复制代码
// 停止信号
static void stop() {
	I2C0S_SDA(0); // SDA 拉低
	I2C0S_DELAY();// 延时
	I2C0S_SCL(1);// SCL 拉高
	I2C0S_DELAY();// 延时
	I2C0S_SDA(1); // SDA 拉高
	I2C0S_DELAY();// 延时
}
发送字节
c 复制代码
// 设备地址,寄存器地址,数据
static void send(uint8_t dat) {
	for(uint8_t i = 0; i < 8; i++) {
		// 判断最高位为1还是0, 如果为1, 条件就为真
		if (dat & 0x80) I2C0S_SDA(1);
		else 			I2C0S_SDA(0);
		dat = dat << 1; // 左移移位
		I2C0S_DELAY();// 延时
		I2C0S_SCL(1);// SCL 拉高
		I2C0S_DELAY();// 延时
		I2C0S_SCL(0);// SCL 拉低
		I2C0S_DELAY();// 延时	
	}	
}
接收字节
c 复制代码
// 接收1个字节
static uint8_t recv() {
	uint8_t dat = 0;
	
	I2C0S_SDA(1); // SDA 拉高
	
	for(uint8_t i = 0; i < 8; i++) {
		I2C0S_SCL(1);// SCL 拉高
		I2C0S_DELAY();// 延时
		dat <<= 1; // 左移一位,留一个位置放数据,不处理,这个位置(第0位)为0
		if (SET == I2C0S_SDA_STATE())  dat |= 1;  // 是|=1, 不是=1
		
		I2C0S_SCL(0);// SCL 拉低
		I2C0S_DELAY();// 延时
	}
	
	return dat;
}
应答控制
c 复制代码
// 发送ack响应
static void send_ack() {
	I2C0S_SDA(0); // SDA 拉低
	I2C0S_DELAY();// 延时
	I2C0S_SCL(1);// SCL 拉高
	I2C0S_DELAY();// 延时
	I2C0S_SCL(0);// SCL 拉低
	I2C0S_DELAY();// 延时
}
// 发送nack响应
static void send_nack() {
	I2C0S_SDA(1); // SDA 拉高
	I2C0S_DELAY();// 延时
	I2C0S_SCL(1);// SCL 拉高
	I2C0S_DELAY();// 延时
	I2C0S_SCL(0);// SCL 拉低
	I2C0S_DELAY();// 延时
}

// 等待响应,返回值 > 0, 出现异常
static uint8_t wait_ack() {
	I2C0S_SDA(1); // SDA 拉高
	I2C0S_DELAY();// 延时
	I2C0S_SCL(1);// SCL 拉高, 控制权交给从设备
	I2C0S_DELAY();// 延时
	// ============  控制权交给从设备,MCU读数据线
	// if (RESET == gpio_input_bit_get(I2C0S_SDA_PORT, I2C0S_SDA_PIN)) { // 低电平应答成功
	if (RESET == I2C0S_SDA_STATE()) {
		I2C0S_SCL(0);// SCL 拉低, 控制权交给主设备
		I2C0S_DELAY();// 延时
		return 0;
	} else {// 高电平应答失败
		stop();
		return 1;
	}
}

四、完整的读写函数

4.1 写操作函数

c 复制代码
/**********************************************************
 * @brief 写数据到I2C设备指定寄存器
 * @param  addr 设备地址 (写地址的前7位) 0x51
				写地址 (addr << 1) | 0     0xA2
 * @param  reg  寄存器地址
 * @param  dat  字节数组
 * @param  len  数据长度
 * @return 0成功,1 设备不存在, 2 寄存器不存在, 3 数据无响应
 **********************************************************/
uint8_t I2C0_soft_write(uint8_t addr, uint8_t reg, uint8_t* dat, uint32_t len) {
	// 开始
	start();
	// 发: 设备地址
	send(addr << 1); // 往左移,传过来是往右移的
	// 等待响应
	if (wait_ack() > 0) return 1;
	// 发: 寄存器地址
	send(reg);
	// 等待响应
	if (wait_ack() > 0) return 2;
	// 循环发送数据
	for(uint32_t i = 0; i < len; i++) {
		send(dat[i]);
		if (wait_ack() > 0) return 3;
	}
	// 停止
	stop();
	
	return 0;
}

4.2 读操作函数

c 复制代码
/**********************************************************
 * @brief 从I2C设备指定寄存器读取数据
 * @param  addr 设备地址  (写地址的前7位)   0x51
                写地址 (addr << 1) | 0     0xA2
                读地址 (addr << 1) | 1     0xA3
 * @param  reg  寄存器地址
 * @param  dat  字节数组
 * @param  len  数据长度
 * @return 0成功,1 写设备不存在, 2 寄存器不存在, 3 读设备不存在
 * @return
 **********************************************************/
uint8_t I2C0_soft_read(uint8_t addr, uint8_t reg, uint8_t* dat, uint32_t len) {
	// 开始
	start();
	// 发: 设备地址, 写
	send(addr << 1); // 往左移,传过来是往右移的
	// 等待响应
	if (wait_ack() > 0) return 1;
	// 发: 寄存器地址
	send(reg);
	// 等待响应
	if (wait_ack() > 0) return 2;
	
	// 开始
	start();
	
	// 发: 设备地址, 读
	send((addr << 1) | 1);  // 地址不一样
	// 等待响应
	if (wait_ack() > 0) return 3;
	
	// 循环接收数据
	for(uint32_t i = 0; i < len; i++) {
		dat[i] = recv(); // 接收
		if (i == len - 1)   send_nack(); // 最后1个数据,发空响应
		else  				send_ack(); // 非最后1个数据,发响应
	}
	// 停止
	stop();
	
	return 0;
}

五、PCF8563时钟芯片应用

5.1 PCF8563设备信息

c 复制代码
// 设备地址
#define		PCF8563_DEV_ADDR		(0xa2 >> 1)
// 存储地址(寄存器地址): 时间(秒)存储地址
#define		PCF8563_REG_SECOND		0x02

typedef struct {
	u16 year; 
	u8 month;
	u8 day;
	u8 weekday;
	u8 hour;
	u8 minute;
	u8 second;
} Clock_t;

5.2 BCD码转换宏

c 复制代码
// 十进制转BCD码
#define  WRITE_BCD(val)   ((val / 10) << 4) + (val%10)   // 没有分号,宏定义
// BCD码转十进制  
#define  READ_BCD(val)    (val >> 4) * 10 + (val & 0x0f)

5.3 PCF8563驱动函数

c 复制代码
// 设置时间
void PCF8563_set_clock(Clock_t temp) {
	u8 p[7] = {0};  // 写完整  年月日 星期几  时分秒
	// 秒的寄存器地址为: 0x02
    // 秒:  第0~3位记录个位,第4~6位记录十位
    //     十位                  个位
    p[0] = WRITE_BCD(temp.second);
    // 分: 第0~3位,保存个数,第4到6位,保存十位
    p[1] = WRITE_BCD(temp.minute);
    // 时:第0~3位,保存个数,第4到5位,保存十位
    p[2] = WRITE_BCD(temp.hour);
    // 日:第0~3位,保存个数,第4到5位,保存十位
    p[3] = WRITE_BCD(temp.day);
    // 周:第0~2位,保存个数
    p[4] = temp.weekday;
    // 月_世纪:  第0~3位记录个位,第4位记录十位,第7位为0,世纪数为20xx,为1,世纪数为21xx
    p[5] = WRITE_BCD(temp.month);
    // 月的第7位
    if (temp.year >= 2100) { // 第7位置1
        p[5] |= (1 << 7);
    }  // 第7位置0,不处理就是0

    // 年:第0~3位,保存个数,第4到7位,保存十位
    // 2025  ===> 25 
    p[6] = WRITE_BCD(temp.year%100);
    I2C_WriteNbyte(PCF8563_DEV_ADDR, PCF8563_REG_SECOND, p, 7);
}

// 获取时间
void PCF8563_get_clock(Clock_t *temp) {
	u8 p[7] = {0};  
	u8 flag;
	// 读时间
	I2C_ReadNbyte(PCF8563_DEV_ADDR, PCF8563_REG_SECOND, p, 7);

	// 10进制  用 16进制表示    低4位放个位     高4位放10位
	// 秒: 第0~3位记录个位,第4~6位记录十位
	temp->second = READ_BCD(p[0]);
	// 分: 第0~3位,保存个数,第4到6位,保存十位
	temp->minute = READ_BCD(p[1]);
	// 时:第0~3位,保存个数,第4到5位,保存十位
	temp->hour = READ_BCD(p[2]);
	// 日:第0~3位,保存个数,第4到5位,保存十位
	temp->day = READ_BCD(p[3]);
	// 周:第0~2位,保存个数
	temp->weekday = p[4]; // 如果是星期日,是0
	// 月_世纪:  第0~3位记录个位,第4位记录十位,第7位为0,世纪数为20xx,为1,世纪数为21xx
	// 处理第7位
	// 取出第7位
	flag = p[5] >> 7;
	// 第7位置0, 月的第7位,是年的标志位,不是月的有效数据
	p[5] &= ~(1 << 7);
	temp->month = READ_BCD(p[5]);
	// 年:第0~3位,保存个数,第4到7位,保存十位
	temp->year = READ_BCD(p[6]);
	if (flag == 1) temp->year += 2100;
	else temp->year += 2000;
}

六、完整应用示例

main主函数:

c 复制代码
#include "gd32f4xx.h"
#include "systick.h"
#include <stdio.h>
#include "USART0.h"
#include "bsp_pcf8563.h"

void USART0_on_recv(uint8_t* data, uint32_t len) {
    printf("recv[%d]:%s\n", len, data);
}

int main(void) {
	// 配置全局优先级分组规则
    nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2);
    // 初始化系统嘀嗒定时器
    systick_config();
    // 初始化USART
    USART0_init();
	PCF8563_init(); 	// PCF8563 初始化
	Clock_t temp = {2025, 8, 11, 1, 23, 59, 54}; // 结构体变量
	PCF8563_set_clock(temp); // 设置时间
    while(1) {
		PCF8563_get_clock(&temp); // 获取时间
        printf("%02d-%02d-%02d\n", (int)temp.year, (int)temp.month, (int)temp.day);
        printf("weekday: %02d\n", (int)temp.weekday);
        printf("%02d:%02d:%02d\n", (int)temp.hour, (int)temp.minute, (int)temp.second);
		delay_1ms(1000);
    }
	return 0;
}

i2c软实现函数:

I2C0_soft.h

c 复制代码
#ifndef __I2C0_SOFT_H__
#define __I2C0_SOFT_H__

#include "gd32f4xx.h"
#include "systick.h"

// PB6 - SCL
#define I2C0S_SCL_RCU     RCU_GPIOB
#define I2C0S_SCL_PORT    GPIOB
#define I2C0S_SCL_PIN     GPIO_PIN_6
// PB7 - SDA
#define I2C0S_SDA_RCU     RCU_GPIOB
#define I2C0S_SDA_PORT    GPIOB
#define I2C0S_SDA_PIN     GPIO_PIN_7

#define  FAST    1   // 0 标准  1 快速

void I2C0_soft_init();

/* 写数据到I2C设备指定寄存器 */
uint8_t I2C0_soft_write(uint8_t addr, uint8_t reg, uint8_t* dat, uint32_t len);

/* 从I2C设备指定寄存器读取数据 */
uint8_t I2C0_soft_read(uint8_t addr, uint8_t reg, uint8_t* dat, uint32_t len);

#endif

I2C0_soft.c

c 复制代码
#include "I2C0_soft.h"
#include "gpio_cfg.h"
#define I2C0S_SDA(bit)  gpio_bit_write(I2C0S_SDA_PORT, I2C0S_SDA_PIN, (bit) ? SET : RESET)
#define I2C0S_SCL(bit)  gpio_bit_write(I2C0S_SCL_PORT, I2C0S_SCL_PIN, (bit) ? SET : RESET)

// 标准模式 100Kbits/s, 快速模式 400Kbits/s
// 100Kbits/s -> 100 000 bit / 1 000 000us -> 1bit/10us  一个高一个低才是1bit
// 400Kbits/s -> 400 000 bit / 1 000 000us -> 4bit/10us -> 每1bit大概需要2.5us
#if !FAST
	#define I2C0S_DELAY()   delay_1us(5)	// 标准模式
#else
	#define I2C0S_DELAY()   delay_1us(1)	// 快速模式
#endif


#define I2C0S_SDA_STATE() gpio_input_bit_get(I2C0S_SDA_PORT, I2C0S_SDA_PIN) // 读取SDA电平

// 开始信号
static void start() {
	
	I2C0S_SDA(1); // SDA 拉高
	I2C0S_DELAY();// 延时
	I2C0S_SCL(1);// SCL 拉高
	I2C0S_DELAY();// 延时
	I2C0S_SDA(0); // SDA 拉低
	I2C0S_DELAY();// 延时
	I2C0S_SCL(0);// SCL 拉低
	I2C0S_DELAY();// 延时
}

// 设备地址,寄存器地址,数据
static void send(uint8_t dat) {
	for(uint8_t i = 0; i < 8; i++) {
		// 判断最高位为1还是0, 如果为1, 条件就为真
		if (dat & 0x80) I2C0S_SDA(1);
		else 			I2C0S_SDA(0);
		dat = dat << 1; // 左移移位
		I2C0S_DELAY();// 延时
		I2C0S_SCL(1);// SCL 拉高
		I2C0S_DELAY();// 延时
		I2C0S_SCL(0);// SCL 拉低
		I2C0S_DELAY();// 延时	
	}	
}

// 停止信号
static void stop() {
	I2C0S_SDA(0); // SDA 拉低
	I2C0S_DELAY();// 延时
	I2C0S_SCL(1);// SCL 拉高
	I2C0S_DELAY();// 延时
	I2C0S_SDA(1); // SDA 拉高
	I2C0S_DELAY();// 延时
}

// 接收1个字节
static uint8_t recv() {
	uint8_t dat = 0;
	
	I2C0S_SDA(1); // SDA 拉高
	
	for(uint8_t i = 0; i < 8; i++) {
		I2C0S_SCL(1);// SCL 拉高
		I2C0S_DELAY();// 延时
		dat <<= 1; // 左移一位,留一个位置放数据,不处理,这个位置(第0位)为0
		if (SET == I2C0S_SDA_STATE())  dat |= 1;  // 是|=1, 不是=1
		
		I2C0S_SCL(0);// SCL 拉低
		I2C0S_DELAY();// 延时
	}
	
	return dat;
}

// 发送ack响应
static void send_ack() {
	I2C0S_SDA(0); // SDA 拉低
	I2C0S_DELAY();// 延时
	I2C0S_SCL(1);// SCL 拉高
	I2C0S_DELAY();// 延时
	I2C0S_SCL(0);// SCL 拉低
	I2C0S_DELAY();// 延时
}
// 发送nack响应
static void send_nack() {
	I2C0S_SDA(1); // SDA 拉高
	I2C0S_DELAY();// 延时
	I2C0S_SCL(1);// SCL 拉高
	I2C0S_DELAY();// 延时
	I2C0S_SCL(0);// SCL 拉低
	I2C0S_DELAY();// 延时
}

// 等待响应,返回值 > 0, 出现异常
static uint8_t wait_ack() {
	I2C0S_SDA(1); // SDA 拉高
	I2C0S_DELAY();// 延时
	I2C0S_SCL(1);// SCL 拉高, 控制权交给从设备
	I2C0S_DELAY();// 延时
	// ============  控制权交给从设备,MCU读数据线
	// if (RESET == gpio_input_bit_get(I2C0S_SDA_PORT, I2C0S_SDA_PIN)) { // 低电平应答成功
	if (RESET == I2C0S_SDA_STATE()) {
		I2C0S_SCL(0);// SCL 拉低, 控制权交给主设备
		I2C0S_DELAY();// 延时
		return 0;
	} else {// 高电平应答失败
		stop();
		return 1;
	}
}

void I2C0_soft_init() {
	// SCL SDA 开漏输出  电平使用默认的
	GPIO_output(I2C0S_SCL_RCU, I2C0S_SCL_PORT, I2C0S_SCL_PIN, GPIO_OTYPE_OD, 2); // 2 默认电平,配置完不做电平处理
	GPIO_output(I2C0S_SDA_RCU, I2C0S_SDA_PORT, I2C0S_SDA_PIN, GPIO_OTYPE_OD, 2); // 2 默认电平,配置完不做电平处理
}

/**********************************************************
 * @brief 写数据到I2C设备指定寄存器
 * @param  addr 设备地址 (写地址的前7位) 0x51
				写地址 (addr << 1) | 0     0xA2
 * @param  reg  寄存器地址
 * @param  dat  字节数组
 * @param  len  数据长度
 * @return 0成功,1 设备不存在, 2 寄存器不存在, 3 数据无响应
 **********************************************************/
uint8_t I2C0_soft_write(uint8_t addr, uint8_t reg, uint8_t* dat, uint32_t len) {
	// 开始
	start();
	// 发: 设备地址
	send(addr << 1); // 往左移,传过来是往右移的
	// 等待响应
	if (wait_ack() > 0) return 1;
	// 发: 寄存器地址
	send(reg);
	// 等待响应
	if (wait_ack() > 0) return 2;
	// 循环发送数据
	for(uint32_t i = 0; i < len; i++) {
		send(dat[i]);
		if (wait_ack() > 0) return 3;
	}
	// 停止
	stop();
	
	return 0;
}

/**********************************************************
 * @brief 从I2C设备指定寄存器读取数据
 * @param  addr 设备地址  (写地址的前7位)   0x51
                写地址 (addr << 1) | 0     0xA2
                读地址 (addr << 1) | 1     0xA3
 * @param  reg  寄存器地址
 * @param  dat  字节数组
 * @param  len  数据长度
 * @return 0成功,1 写设备不存在, 2 寄存器不存在, 3 读设备不存在
 * @return
 **********************************************************/
uint8_t I2C0_soft_read(uint8_t addr, uint8_t reg, uint8_t* dat, uint32_t len) {
	// 开始
	start();
	// 发: 设备地址, 写
	send(addr << 1); // 往左移,传过来是往右移的
	// 等待响应
	if (wait_ack() > 0) return 1;
	// 发: 寄存器地址
	send(reg);
	// 等待响应
	if (wait_ack() > 0) return 2;
	
	// 开始
	start();
	
	// 发: 设备地址, 读
	send((addr << 1) | 1);  // 地址不一样
	// 等待响应
	if (wait_ack() > 0) return 3;
	
	// 循环接收数据
	for(uint32_t i = 0; i < len; i++) {
		dat[i] = recv(); // 接收
		if (i == len - 1)   send_nack(); // 最后1个数据,发空响应
		else  				send_ack(); // 非最后1个数据,发响应
	}
	// 停止
	stop();
	
	return 0;
}

pcf8563的库函数实现:

bsp_pcf8563.h :

c 复制代码
#ifndef __BSP_PCF8563_H__
#define __BSP_PCF8563_H__

#include "gd32f4xx.h"
#include "I2C.h"

#define  I2C_WriteNbyte(a,b,c,d)  I2C0_write(a,b,c,d)
#define  I2C_ReadNbyte(a,b,c,d)   I2C0_read(a,b,c,d)

#ifndef u8
#define u8 uint8_t
#endif

#ifndef u16
#define u16 uint16_t
#endif

#ifndef u32
#define u32 uint32_t
#endif

/*
1. 如果 PCF8563_USE_ALARM 开关为1,闹钟中断开关
void PCF8563_on_alarm() {} 必须在合适位置定义
2. 如果 PCF8563_USE_TIMER 开关为1,定时器中断开关
void PCF8563_on_timer() {} 必须在合适位置定义
*/

#define 	PCF8563_USE_ALARM	0
#define 	PCF8563_USE_TIMER	0

// 函数声明
void PCF8563_on_alarm();
void PCF8563_on_timer();

#define  WRITE_BCD(val)   ((val / 10) << 4) + (val%10)   // 没有分号,宏定义
#define  READ_BCD(val)    (val >> 4) * 10 + (val & 0x0f)

// 设备地址
#define		PCF8563_DEV_ADDR		(0xa2 >> 1)
// 存储地址(寄存器地址): 时间(秒)存储地址
#define		PCF8563_REG_SECOND		0x02

typedef struct {
	u16 year; 
	u8 month;
	u8 day;
	u8 weekday;
	u8 hour;
	u8 minute;
	u8 second;
} Clock_t;

// PCF8563初始化
void PCF8563_init();

// 设置时间
void PCF8563_set_clock(Clock_t temp);

// 获取时间
void PCF8563_get_clock(Clock_t *temp);


//=============================闹钟
typedef struct {
    // 设置分\时\天\周,如果为-1,禁用此项
    char minute ;
    char hour ;
    char day ;
    char weekday;
} Alarm_t;

// 设置闹钟
void PCF8563_set_alarm(Alarm_t alarm);
// 启用闹钟
void PCF8563_enable_alarm();
// 禁用闹钟Alarm
void PCF8563_disable_alarm();
// 清理闹钟标记
void PCF8563_alarm_clear_flag();


//=============================定时器
// 国产芯片的HZ1有问题,不要使用,建议使用HZ64
typedef enum { HZ4096 = 0, HZ64 = 1, HZ1 = 2, HZ1_60 = 3} TimerFreq;

// 启动定时器
void PCF8563_enable_timer();
// 禁用定时器
void PCF8563_disable_timer();
// 清除定时器标志位
void PCF8563_clear_timer();     
// 设置定时器,参数1:时钟频率 参数2:倒计时计算值,时间为:参数2/参数1
void PCF8563_set_timer(TimerFreq freq, u8 countdown);

#endif

bsp_pcf8563.c的库函数实现

c 复制代码
#include 	"bsp_pcf8563.h"

// PCF8563初始化
void PCF8563_init() {
	I2C0_init();
}


// 设置时间
void PCF8563_set_clock(Clock_t temp) {
	u8 p[7] = {0};  // 写完整  年月日 星期几  时分秒
	// 秒的寄存器地址为: 0x02
    // 秒:  第0~3位记录个位,第4~6位记录十位
    //     十位                  个位
    p[0] = WRITE_BCD(temp.second);
    // 分: 第0~3位,保存个数,第4到6位,保存十位
    p[1] = WRITE_BCD(temp.minute);
    // 时:第0~3位,保存个数,第4到5位,保存十位
    p[2] = WRITE_BCD(temp.hour);
    // 日:第0~3位,保存个数,第4到5位,保存十位
    p[3] = WRITE_BCD(temp.day);
    // 周:第0~2位,保存个数
    p[4] = temp.weekday;
    // 月_世纪:  第0~3位记录个位,第4位记录十位,第7位为0,世纪数为20xx,为1,世纪数为21xx
    p[5] = WRITE_BCD(temp.month);
    // 月的第7位
    if (temp.year >= 2100) { // 第7位置1
        p[5] |= (1 << 7);
    }  // 第7位置0,不处理就是0

    // 年:第0~3位,保存个数,第4到7位,保存十位
    // 2025  ===> 25 
    p[6] = WRITE_BCD(temp.year%100);
    I2C_WriteNbyte(PCF8563_DEV_ADDR, PCF8563_REG_SECOND, p, 7);
}

// 获取时间
void PCF8563_get_clock(Clock_t *temp) {
	u8 p[7] = {0};  
	u8 flag;
	// 读时间
	I2C_ReadNbyte(PCF8563_DEV_ADDR, PCF8563_REG_SECOND, p, 7);

	// 10进制  用 16进制表示    低4位放个位     高4位放10位
	// 秒: 第0~3位记录个位,第4~6位记录十位
	temp->second = READ_BCD(p[0]);
	// 分: 第0~3位,保存个数,第4到6位,保存十位
	temp->minute = READ_BCD(p[1]);
	// 时:第0~3位,保存个数,第4到5位,保存十位
	temp->hour = READ_BCD(p[2]);
	// 日:第0~3位,保存个数,第4到5位,保存十位
	temp->day = READ_BCD(p[3]);
	// 周:第0~2位,保存个数
	temp->weekday = p[4]; // 如果是星期日,是0
	// 月_世纪:  第0~3位记录个位,第4位记录十位,第7位为0,世纪数为20xx,为1,世纪数为21xx
	// 处理第7位
	// 取出第7位
	flag = p[5] >> 7;
	// 第7位置0, 月的第7位,是年的标志位,不是月的有效数据
	p[5] &= ~(1 << 7);
	temp->month = READ_BCD(p[5]);
	// 年:第0~3位,保存个数,第4到7位,保存十位
	temp->year = READ_BCD(p[6]);
	if (flag == 1) temp->year += 2100;
	else temp->year += 2000;
}

// 设置闹钟
void PCF8563_set_alarm(Alarm_t alarm) {
	u8 p[4] = {0};
	//===================2.1 闹钟时间设置 寄存器地址 0x09
    // 分: 第0~3位,记录个数, 第4~6位记录十位, 第7位:置0启动, 置1禁用
	if (alarm.minute != -1) p[0] = WRITE_BCD(alarm.minute);
	else p[0] = 0x80;
	
    // 时: 第0~3位,记录个数, 第4~5位记录十位, 第7位:置0启动, 置1禁用
	p[1] = alarm.hour != -1 ? WRITE_BCD(alarm.hour) : 0x80 ;
    // 日: 第0~3位,记录个数, 第4~5位记录十位, 第7位:置0启动, 置1禁用
	p[2] = alarm.day != -1 ? WRITE_BCD(alarm.day) : 0x80 ;
    // 周: 第0~2位,记录个数, 第7位:置0启动, 置1禁用
	p[3] = alarm.weekday != -1 ? alarm.weekday : 0x80 ;
	I2C_WriteNbyte(PCF8563_DEV_ADDR, 0x09, p, 4);
}

// 启用闹钟
void PCF8563_enable_alarm() {
	u8 cfg;
    //===================2.2 闹钟开启 寄存器地址 0x01
    //a) 读原来的配置(不要乱改配置,只改自己的位,其它维持不变)
	I2C_ReadNbyte(PCF8563_DEV_ADDR, 0x01, &cfg, 1);
    //b) 在原来配置的基础上,清除标志位  第3位:置0清除标志位,置1维持不变
	cfg &= ~(1 << 3);
    //c) 在原来配置基础上,启动闹钟,第1位:置0禁用,置1启动
	cfg |= (1 << 1);	
    //d) 重新写入配置
	I2C_WriteNbyte(PCF8563_DEV_ADDR, 0x01, &cfg, 1);
}

// 禁用闹钟Alarm
void PCF8563_disable_alarm() {
	u8 cfg;
    //===================2.2 闹钟 寄存器地址 0x01
    //a) 读原来的配置(不要乱改配置,只改自己的位,其它维持不变)
	I2C_ReadNbyte(PCF8563_DEV_ADDR, 0x01, &cfg, 1);
    //b) 在原来配置的基础上,清除标志位  第3位:置0清除标志位,置1维持不变
	cfg &= ~(1 << 3);
    //c) 在原来配置基础上,禁用闹钟,第1位:置0禁用,置1启动
	cfg &= ~(1 << 1);	
    //d) 重新写入配置
	I2C_WriteNbyte(PCF8563_DEV_ADDR, 0x01, &cfg, 1);
}

// 清理闹钟标记
void PCF8563_alarm_clear_flag() {
	u8 cfg;
	// 清除闹钟的标志位,才能重复触发闹钟
	//a) 读原来的配置(不要乱改配置,只改自己的位,其它维持不变)
	I2C_ReadNbyte(PCF8563_DEV_ADDR, 0x01, &cfg, 1);
    //b) 在原来配置的基础上,清除标志位  第3位:置0清除标志位,置1维持不变
	cfg &= ~(1 << 3);	
    //c) 重新写入配置
	I2C_WriteNbyte(PCF8563_DEV_ADDR, 0x01, &cfg, 1);
}

// 启动定时器
void PCF8563_enable_timer() {
	u8 cfg;
    //============2 定时器开启  寄存器地址 0x01
    //a) 读原来的配置(不要乱改配置,只改自己的位,其它维持不变)
	I2C_ReadNbyte(PCF8563_DEV_ADDR, 0x01, &cfg, 1);
    //b) 在原来配置的基础上,清除标志位,第2位:置0清除标志位,置1维持不变 
	cfg &= ~(1 << 2);
    //c) 在原来配置基础上,启动定时器,第0位:置0禁用,置1启用  
	cfg |= (1 << 0);
    //d) 重新写入配置
	I2C_WriteNbyte(PCF8563_DEV_ADDR, 0x01, &cfg, 1);
}

// 禁用定时器
void PCF8563_disable_timer() {
	u8 cfg;
    //============2 定时器开启  寄存器地址 0x01
    //a) 读原来的配置(不要乱改配置,只改自己的位,其它维持不变)
	I2C_ReadNbyte(PCF8563_DEV_ADDR, 0x01, &cfg, 1);
    //b) 在原来配置的基础上,清除标志位,第2位:置0清除标志位,置1维持不变 
	cfg &= ~(1 << 2);
    //c) 在原来配置基础上,启动定时器,第0位:置0禁用,置1启用  
	cfg &= ~(1 << 0);
    //d) 重新写入配置
	I2C_WriteNbyte(PCF8563_DEV_ADDR, 0x01, &cfg, 1);
}

// 清除定时器标志位
void PCF8563_clear_timer() {
	u8 cfg;
	
	// 清除标志位,才能重复触发
	//a) 读原来的配置(不要乱改配置,只改自己的位,其它维持不变)
	I2C_ReadNbyte(PCF8563_DEV_ADDR, 0x01, &cfg, 1);
    //b) 在原来配置的基础上,清除标志位,第2位:置0清除标志位,置1维持不变 
	cfg &= ~(1 << 2);
    //c) 重新写入配置
	I2C_WriteNbyte(PCF8563_DEV_ADDR, 0x01, &cfg, 1);
}

// 设置定时器,参数1:时钟频率 参数2:倒计时计算值,时间为:参数2/参数1
void PCF8563_set_timer(TimerFreq freq, u8 countdown) {
	u8 p[2] = {0};	
	
	//============1 定时器设置 寄存器地址 0x0e
    //a) 时钟频率  不建议用 1hz,有bug
    // 第7位为0,定时器禁用,第七位为1,定时器启用
	p[0] = freq + 0x80; // 64hz
    //b) 计数值(0~255) ===》时间为: 计数值/时钟频率 
	p[1] = countdown; // 时间间隔:countdown / freq 
	I2C_WriteNbyte(PCF8563_DEV_ADDR, 0x0e, p, 2);

}

void int3_callback() { // 外部中断3的回调
	u8 cfg;
	
	// 读取原来的配置
	I2C_ReadNbyte(PCF8563_DEV_ADDR, 0x01, &cfg, 1);
	// 如果是闹钟,第1位和第3位是1 
	if ((cfg & 0x02) && (cfg & 0x08)) {
		// 清理闹钟标记
		PCF8563_alarm_clear_flag();
		
		// 调用
		#if PCF8563_USE_ALARM
		PCF8563_on_alarm();
		#endif	
	}
	
	// 如果是定时器,第0位和第2位是1 
	if ((cfg & 0x01) && (cfg & 0x04)) {
		// 清除定时器标志位
		PCF8563_clear_timer();
		
		// 调用
		#if PCF8563_USE_TIMER
		PCF8563_on_timer();
		#endif		
	}
}

七、调试技巧

7.1 逻辑分析仪使用

使用逻辑分析仪可以直观查看I2C时序:

  1. 连接方式

    • CH0接SCL(PB6)
    • CH1接SDA(PB7)
    • GND接开发板GND
  2. 软件设置

    • 采样率:4MHz以上
    • 触发方式:SCL下降沿
    • 解码协议:I2C

7.2 常见问题解决

  1. 无应答信号

    • 检查设备地址是否正确
    • 确认上拉电阻已连接
    • 检查设备电源
  2. 时序错误

    • 调整延时参数
    • 确认时钟频率设置
  3. 数据错误

    • 检查BCD码转换
    • 验证寄存器地址
相关推荐
充哥单片机设计3 小时前
【STM32项目开源】基于STM32的人体健康监测系统
stm32·单片机·嵌入式硬件
hazy1k4 小时前
51单片机基础-独立按键
stm32·单片机·嵌入式硬件·51单片机
文火冰糖的硅基工坊5 小时前
[创业之路-702]:“第三次”与“第四次工业革命”的范式跃迁
大数据·人工智能·科技·嵌入式硬件·架构·嵌入式·gpu
点灯小铭6 小时前
基于单片机的架空线路接地故障检测与报警系统
单片机·毕业设计·课程设计
清风6666666 小时前
基于单片机的智能水瓶温度控制系统
单片机·嵌入式硬件·毕业设计·课程设计
沐欣工作室_lvyiyi6 小时前
基于单片机的 220v车载逆变电源的设计与制作(论文+图纸)
stm32·单片机·车载逆变器·12v到220v
沐欣工作室_lvyiyi6 小时前
基于单片机的智能洗衣机的设计与实现(论文+源码)
单片机·嵌入式硬件·毕业设计·洗衣机控制器
兆龙电子单片机设计8 小时前
【STM32项目开源】STM32单片机智能农业大棚控制系统
stm32·单片机·物联网·开源·自动化
文火冰糖的硅基工坊9 小时前
[嵌入式系统-136]:主流AIOT智能体软件技术栈
嵌入式硬件·架构·嵌入式·cpu·gpu