I2C软实现基于GD32F407VE的天空星的配置
一、I2C协议基础
1.1 什么是I2C?
I2C(Inter-Integrated Circuit)是一种通用的串行通信总线协议,仅使用两根线(时钟线SCL 和数据线SDA)就能实现多个设备间的通信。在GD32F407VE天空星开发板上,我们可以通过软件模拟的方式实现I2C通信。
1.2 I2C通信模型
想象一个课堂场景:
- 主设备 = 老师
- 从设备 = 学生
- SCL = 老师的手拍节拍
- SDA = 老师和学生对话的通道
通信流程示例:
- 老师敲黑板:"开始上课!"(起始信号)
- 老师点名:"张三,回答这个问题"(设备地址)
- 张三举手:"到!"(ACK响应)
- 老师和张三对话(数据传输)
- 老师敲桌子:"下课!"(停止信号)
二、硬件连接
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时序:
-
连接方式:
- CH0接SCL(PB6)
- CH1接SDA(PB7)
- GND接开发板GND
-
软件设置:
- 采样率:4MHz以上
- 触发方式:SCL下降沿
- 解码协议:I2C
7.2 常见问题解决
-
无应答信号:
- 检查设备地址是否正确
- 确认上拉电阻已连接
- 检查设备电源
-
时序错误:
- 调整延时参数
- 确认时钟频率设置
-
数据错误:
- 检查BCD码转换
- 验证寄存器地址