知不足而奋进 望远山而前行
目录
前言
在嵌入式系统开发中,I2C(Inter-Integrated Circuit)总线是一种常见且重要的通信协议,用于连接多种外设和传感器,实现数据的可靠传输和控制。本文将详细介绍如何在STM32开发板上配置和操作I2C,以及如何利用I2C接口与PCF8563实时时钟(RTC)芯片进行通信。通过正确配置I2C通信参数和实现读写功能,我们能够有效地设置和读取PCF8563的时钟信息,从而在嵌入式应用中实现精确的时间管理功能。
目标
- 掌握I2C配置方式
- 掌握I2C读写操作
内容
需求
开发板中的PCF8563的RTC时钟设置和读取。
I2C功能配置
选择对应的I2C,打开。
配置完成后可以查询引脚是否符合要求。
I2C编码
移植PCF8563驱动
#ifndef __PCF8563_H__
#define __PCF8563_H__
#include "stm32f4xx.h"
#include "i2c.h"
#ifndef u8
#define u8 uint8_t
#endif
#ifndef u16
#define u16 uint16_t
#endif
// 如果定义了该宏,Alarm的相关函数才会被编译并启用,被注释掉就是禁用
//#define PCF8563_ALARM_ENABLE
// 如果定义了该宏,Timer的相关函数才会被编译并启用,被注释掉就是禁用
//#define PCF8563_TIMER_ENABLE
// 设备地址
#define PCF8563_ADDR 0x51
// 存储地址:时间的存储地址开始位置
#define PCF8563_REG_TD 0x02
// 控制寄存器2
#define PCF8563_REG_CS2 0x01
// I2C写操作
#define I2C_WRITE(a, r, p, n) I2C1_write(a, r, p, n)
// I2C读操作
#define I2C_READ(a, r, p, n) I2C1_read(a, r, p, n)
// 秒、分、时、星期、日、月、年、世纪
// year = 2023, month = 2, day = 28, week = 4;
// hour = 23, minute = 59, second = 52;
typedef struct {
u16 year;
u8 month;
u8 day;
u8 week;
u8 hour;
u8 minute;
u8 second;
} Clock_t;
// 警报、闹铃Alarm结构体
typedef struct {
u8 minute;
u8 hour;
u8 day;
u8 weekday;
} Alarm_t;
typedef enum {
HZ4096 = 0,
HZ64 = 1,
HZ1 = 2,
HZ1_60 = 3,
}TimerFreq;
void PCF8563_init();
void PCF8563_set_clock(Clock_t c);
void PCF8563_get_clock(Clock_t *c);
void PCF8563_set_alarm(Alarm_t alarm);
void PCF8563_enable_alarm();
void PCF8563_disable_alarm();
void PCF8563_alarm_clear_flag();
void PCF8563_set_timer(TimerFreq freq ,u8 countdown);
void PCF8563_enable_timer();
void PCF8563_disable_timer();
void PCF8563_timer_clear_flag();
#ifdef PCF8563_ALARM_ENABLE
extern void PCF8563_on_alarm();
#endif
#ifdef PCF8563_TIMER_ENABLE
extern void PCF8563_on_timer();
#endif
#endif
#include "PCF8563.h"
#include <stdio.h>
#define NUMBER_TD 7
void PCF8563_init() {
}
void PCF8563_set_clock(Clock_t c) {
u8 p[NUMBER_TD]; // 用于存储时间字节数组
u8 Cent; // 世纪:0代表本世纪20, 1代表下个世纪,年99->00时交替
// 将数值转成一个字节表达 (BCD)
// 秒:VL 1 1 1 - 0 0 0 0
p[0] = ((c.second / 10) << 4) + (c.second % 10);
// 分: X 1 1 1 - 0 0 0 0
p[1] = ((c.minute / 10) << 4) + (c.minute % 10);
// 时: X X 1 1 - 0 0 0 0
p[2] = ((c.hour / 10) << 4) + (c.hour % 10);
// 天: X X 1 1 - 0 0 0 0
p[3] = ((c.day / 10) << 4) + (c.day % 10);
// 周: X X X X - X 0 0 0
p[4] = c.week;
// 世纪 20xx , 21xx
Cent = (c.year >= 2100 ? 1 : 0);
// 月: C X X 1 - 0 0 0 0
p[5] = (Cent << 7) + ((c.month / 10) << 4) + (c.month % 10);
// 年: 1 1 1 1 - 0 0 0 0 2023, 2036
p[6] = ((c.year % 100 / 10) << 4) + (c.year % 10);
// 设置数据Write
I2C_WRITE(PCF8563_ADDR, PCF8563_REG_TD, p, NUMBER_TD);
}
void PCF8563_get_clock(Clock_t *c) {
u8 Cent; // 世纪:0代表本世纪20, 1代表下个世纪,年99->00时交替
u8 p[NUMBER_TD]; // 用于存储时间字节数组
// 循环读取Read: 秒、分、时、星期、日、月、年、世纪
I2C_READ(PCF8563_ADDR, PCF8563_REG_TD, p, NUMBER_TD);
// for(i = 0; i < NUMBER; i++){
// printf("%d-> %d \n", (int)i, (int)p[i]);
// }
// printf("----------------\n");
// 秒:VL 1 1 1 - 0 0 0 0 转成十进制
c->second = ((p[0] >> 4) & 0x07) * 10 + (p[0] & 0x0F);
// 分: X 1 1 1 - 0 0 0 0 转成十进制
c->minute = ((p[1] >> 4) & 0x07) * 10 + (p[1] & 0x0F);
// 时: X X 1 1 - 0 0 0 0 转成十进制
c->hour = ((p[2] >> 4) & 0x03) * 10 + (p[2] & 0x0F);
// 天: X X 1 1 - 0 0 0 0 转成十进制
c->day = ((p[3] >> 4) & 0x03) * 10 + (p[3] & 0x0F);
// 周: X X X X - X 0 0 0 转成十进制
c->week = p[4] & 0x07;
// 世纪
// 月: C X X 1 - 0 0 0 0
c->month = ((p[5] >> 4) & 0x01) * 10 + (p[5] & 0x0F);
Cent = p[5] >> 7; // 0->20xx年 1->21xx年
// 年: 1 1 1 1 - 0 0 0 0 1969, 2023
c->year = ((p[6] >> 4) & 0x0F) * 10 + (p[6] & 0x0F);
c->year += Cent == 0 ? 2000 : 2100;
}
void PCF8563_set_alarm(Alarm_t alarm){
// 想将某个类型关闭掉,传一个负数
// 或者多传1个字段,低4位,根据01决定是否启动对应类型 0000 0011
u8 a[4];
// a = 0; // 分钟
// I2C_WRITE(PCF8563_ADDR, 0x09, &a, 1);
//
// a = 0; // 小时
// I2C_WRITE(PCF8563_ADDR, 0x0A, &a, 1);
// 分 M 1 1 1 - 0 0 0 0 enable->0, diabled->0x80
a[0] = ((alarm.minute / 10) << 4) + (alarm.minute % 10) + 0;
// 时 H x 1 1 - 0 0 0 0 enable->0, diabled->0x80
a[1] = ((alarm.hour / 10) << 4) + (alarm.hour % 10) + 0;
// 天 D x 1 1 - 0 0 0 0 enable->0, diabled->0x80
a[2] = ((alarm.day / 10) << 4) + (alarm.day % 10) + 0;
// 周 W x x x - x 0 0 0 enable->0, diabled->0x80
a[3] = alarm.weekday + 0;
I2C_WRITE(PCF8563_ADDR, 0x09, a, 4);
}
void PCF8563_enable_alarm(){
u8 cs2 = 0; // 控制状态寄存器2
// 配置 cs2 寄存器以启用Alarm -------------------------------------------------
I2C_READ(PCF8563_ADDR, 0x01, &cs2, 1);
// printf("cs2: %d \n", (int)cs2);
// 清除Alarm标记,AF设置为0,下一次闹钟到点时,才能触发
cs2 &= ~0x08;
// 开启Alarm中断, AIE为1,启用Alarm闹钟
cs2 |= 0x02;
I2C_WRITE(PCF8563_ADDR, 0x01, &cs2, 1);
}
void PCF8563_disable_alarm(){
u8 cs2 = 0; // 控制状态寄存器2
// 配置 cs2 寄存器以启用Alarm -------------------------------------------------
I2C_READ(PCF8563_ADDR, 0x01, &cs2, 1);
// printf("cs2: %d \n", (int)cs2);
// 清除Alarm标记,AF设置为0,下一次闹钟到点时,才能触发
cs2 &= ~0x08;
// 开启Alarm中断, AIE为0,禁用Alarm闹钟
cs2 &= ~0x02;
I2C_WRITE(PCF8563_ADDR, 0x01, &cs2, 1);
}
void PCF8563_alarm_clear_flag(){
u8 cs2 = 0; // 控制状态寄存器2
// 01H寄存器中AF位,会在Alarm触发时,被置为1.
// 必须手动置为0,下一个Alarm才能触发。
// 配置 cs2 寄存器
I2C_READ(PCF8563_ADDR, 0x01, &cs2, 1);
// printf("cs2: %d \n", (int)cs2);
// 清除Alarm标记,AF设置为0,下一次闹钟到点时,才能触发
cs2 &= ~0x08;
I2C_WRITE(PCF8563_ADDR, 0x01, &cs2, 1);
}
void PCF8563_set_timer(TimerFreq freq ,u8 countdown){
u8 p = 0;
// 设置Timer运行频率、启用timer source clock
p = 0x80 + freq; // 64Hz
I2C_WRITE(PCF8563_ADDR, 0x0E, &p, 1);
// 设置Timer计数值(每个周期)
p = countdown;
I2C_WRITE(PCF8563_ADDR, 0x0F, &p, 1);
}
void PCF8563_enable_timer(){
u8 cs2 = 0; // 控制状态寄存器2
// 通过cs2启用Timer
I2C_READ(PCF8563_ADDR, 0x01, &cs2, 1);
// 开启Timer中断: TIE
cs2 |= 0x01;
// 清除Timer flag; TF
cs2 &= ~0x04;
I2C_WRITE(PCF8563_ADDR, 0x01, &cs2, 1);
}
void PCF8563_disable_timer(){
u8 cs2 = 0; // 控制状态寄存器2
// 通过cs2启用Timer
I2C_READ(PCF8563_ADDR, 0x01, &cs2, 1);
// 开启Timer中断: TIE
cs2 &= ~0x01;
// 清除Timer flag; TF
cs2 &= ~0x04;
I2C_WRITE(PCF8563_ADDR, 0x01, &cs2, 1);
}
void PCF8563_timer_clear_flag(){
u8 cs2 = 0; // 控制状态寄存器2
// 通过cs2启用Timer
I2C_READ(PCF8563_ADDR, 0x01, &cs2, 1);
// 清除Timer flag; TF
cs2 &= ~0x04;
I2C_WRITE(PCF8563_ADDR, 0x01, &cs2, 1);
}
// 当中断触发时,此函数会执行(Alarm、Timer)
void ext_int3_call() {
u8 cs2;
I2C_READ(PCF8563_ADDR, PCF8563_REG_CS2, &cs2, 1);
// printf("cs2: %d \n", (int) cs2);
#ifdef PCF8563_TIMER_ENABLE
// Alarm Flag && AIE
if((cs2 & 0x08) && (cs2 & 0x02)){
// 清除Alarm标记,避免下次无法触发
PCF8563_alarm_clear_flag();
// 事件触发
PCF8563_on_alarm();
}
#endif
#ifdef PCF8563_TIMER_ENABLE
// Timer Flag && TIE
if((cs2 & 0x04) && (cs2 & 0x01)){
// 清除Timer标记,避免下次无法触发
PCF8563_timer_clear_flag();
// 事件触发
PCF8563_on_timer();
}
#endif
}
只需要实现 i2c_write和i2c_read,驱动就可以正常运行。
/* USER CODE BEGIN WHILE */
// pcf8563初始化
PCF8563_init();
Clock_t c;
c.year = 2023;
c.month = 3;
c.day = 10;
c.week = 2;
c.hour = 23;
c.minute = 59;
c.second = 55;
PCF8563_set_clock(c);
while (1)
{
PCF8563_get_clock(&c);
printf("%d-%d-%d %d %d:%d:%d\r\n", c.year, c.month, c.day, c.week, c.hour, c.minute,c.second);
HAL_Delay(1000);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
I2C读写实现
添加头定义
/* USER CODE BEGIN Prototypes */
uint8_t I2C1_write(uint8_t addr, uint8_t reg, uint8_t* data, uint32_t len);
uint8_t I2C1_read(uint8_t addr, uint8_t reg, uint8_t* data, uint32_t len);
/* USER CODE END Prototypes */
添加c实现
/* USER CODE BEGIN 1 */
#include "string.h"
uint8_t I2C1_write(uint8_t addr, uint8_t reg, uint8_t* data, uint32_t len) {
uint8_t write_addr = addr << 1;
// uint8_t buff[len + 1];
// buff[0] = reg;
// memcpy(&buff[1], data, len);
// HAL_I2C_Master_Transmit(&hi2c1, addr << 1, buff, len + 1, HAL_MAX_DELAY);
return HAL_I2C_Mem_Write(&hi2c1, write_addr, reg, I2C_MEMADD_SIZE_8BIT, data, len, HAL_MAX_DELAY);
}
uint8_t I2C1_read(uint8_t addr, uint8_t reg, uint8_t* data, uint32_t len) {
uint8_t write_addr = addr << 1;
uint8_t read_addr = (addr << 1) | 1; // 0xA3
// HAL_I2C_Master_Transmit(&hi2c1, addr << 1, ®, 1, HAL_MAX_DELAY);
// HAL_I2C_Master_Receive(&hi2c1, (addr << 1) | 0x01, data, len, HAL_MAX_DELAY);
return HAL_I2C_Mem_Read(&hi2c1, read_addr, reg, I2C_MEMADD_SIZE_8BIT, data, len, HAL_MAX_DELAY);
}
/* USER CODE END 1 */
- HAL库的设备地址,要的是具体的读写地址。
- HAL库中的写操作中的数据,包含了寄存器地址。
- HAL库中读操作,需要先写再读。
- 返回值不是读写的数据,而是读写成功与否的枚举值
总结
本文详细介绍了在STM32微控制器上配置和使用I2C接口的方法,特别是与PCF8563实时时钟芯片的集成。首先,我们通过HAL库选择并打开了适当的I2C通道,并确保引脚配置符合要求。随后,我们实现了简单而有效的I2C读写函数(i2c_write和i2c_read),并将其用于移植PCF8563驱动,实现对RTC的初始化、设置和读取操作。
在编码实现中,我们注意到HAL库提供了方便的API来管理I2C的传输和操作,包括单字节和多字节的读写功能。通过这些步骤,我们成功地将PCF8563的时钟设置为特定日期和时间,并能够周期性地读取当前时钟值并输出到终端。这种能力对于许多嵌入式应用而言至关重要,特别是需要精确时间管理的系统。
通过学习本文所述的方法和实现步骤,开发人员不仅能够掌握STM32上I2C接口的基本配置和操作,还能在此基础上进行更复杂的外设集成和应用开发。有效的I2C通信管理和PCF8563驱动移植,为嵌入式系统的时钟管理提供了可靠的解决方案,有助于提高系统的稳定性和功能性。
标签:HAL,u8,读写操作,--,void,ADDR,I2C,PCF8563,cs2 From: https://blog.csdn.net/xuewenyu_/article/details/139724905