STM32+BM8563时钟芯片不走时问题解决(含配置代码)
一、寄存器
BM8563是一款低功耗CMOS实时时钟/日历芯片,它提供一个可编程的时钟输出,一个中断输出和一个掉电检测器,所有的地址和数据都通过I2C总线接口串行传递。最大总线速度为400Kbits/s,每次读写数据后,内嵌的字地址寄存器会自动递增。
BM8563有16个寄存器,其中11个是BCD格式。配置是要注意值范围,不能超出。更多具体应用请看官方手册。
二、晶振
晶振选择非常重要。32.768不用说了,主要是ESR值,不能小了,也不能太大。按照厂家给出的30K肯定可以启振并稳定振荡。负载电容建议6pf,其实芯片内部已经接了一个负载电容,外面的负载电容是用来微调秒走时的准确性的。
特别提出,不要使用贴片的那种小的晶振,那种ESR才几十个欧姆,BM8563由于需要低功耗,所以输出的激励也很小,根本不能使这种低ESR的晶振启振。一定要按照官方给出的30K欧姆来选择晶振。
三、 典型应用图
晶振接的两个电容是调整秒针精度的。如果没有专业仪器调整,那就按官方建议的6pF和19pF,走时也不会太差。
SDA和SCL两根线必须加上拉电阻,4.7K或者10K即可。STM32对应管脚需配置成开漏输出模式。
四、IIC和BM8563初始化配置代码
下面代码是按照官方手册C代码进行修改来的,在STM32RCT6单片机上测试通过,
需要的可以直接复制拿去。一开始我也下载了其他代码,看起来很全面,但是却不好用,IIC部分时钟有点问题,读出数据是不对的。最后直接参照官方手册,写了下面代码。
#include "iic.h"
uint8_t twdata[9]={0x00,0x00,0x50,0x59,0x23,0x31,0x06,0x12,0x04};/*前2个数据用来设
置状态寄存器,后 7 个用来设置时间寄存器 */
uint8_t trdata[7]={0}; /*定义数组用来存储读取的时间数据 */
uint8_t asc[14]={0}; /*定义数组用来存储转换的 asc 码时间数据,供显示用 */
volatile uint8_t ack = 0;
volatile uint8_t bm_status = 0;//如果BM时钟芯片无应答,则为0,。应答正常则为1.
void IIC_SDA_SET(void)
{
HAL_GPIO_WritePin(BM8563_SDA_GPIO_Port,BM8563_SDA_Pin,GPIO_PIN_SET);
}
void IIC_SDA_RESET(void)
{
HAL_GPIO_WritePin(BM8563_SDA_GPIO_Port,BM8563_SDA_Pin,GPIO_PIN_RESET);
}
void IIC_SCL_SET(void)
{
HAL_GPIO_WritePin(BM8563_SCL_GPIO_Port,BM8563_SCL_Pin,GPIO_PIN_SET);
}
void IIC_SCL_RESET(void)
{
HAL_GPIO_WritePin(BM8563_SCL_GPIO_Port,BM8563_SCL_Pin,GPIO_PIN_RESET);
}
uint8_t IN_SDA(void)
{
if(HAL_GPIO_ReadPin(BM8563_SDA_GPIO_Port,BM8563_SDA_Pin))
{
return 1;
}
else
{
return 0;
}
}
/******************************************************************************
* Function Name --> IIC启动
* Description --> SCL高电平期间,SDA由高电平突变到低电平时启动总线
* SCL: __________
* \__________
* SDA: _____
* \_______________
* Input --> none
* Output --> none
* Reaturn --> none
******************************************************************************/
void IIC_Start(void)
{
IIC_SDA_SET(); //为SDA下降启动做准备
Delay_1us();
IIC_SCL_SET(); //在SCL高电平时,SDA为下降沿时候总线启动
Delay_Nus(10);
IIC_SDA_RESET(); //突变,总线启动
Delay_Nus(10);
IIC_SCL_RESET();
Delay_Nus(2);
}
/******************************************************************************
* Function Name --> IIC停止
* Description --> SCL高电平期间,SDA由低电平突变到高电平时停止总线
* SCL: ____________________
* __________
* SDA: _________/
* Input --> none
* Output --> none
* Reaturn --> none
******************************************************************************/
void IIC_Stop(void)
{
IIC_SCL_RESET();
Delay_1us();
IIC_SCL_SET(); //在SCL高电平时,SDA为上升沿时候总线停止
Delay_Nus(10);
IIC_SDA_SET(); //突变,总线停止
Delay_Nus(10);
}
/******************************************************************************
* Function Name --> 主机向从机发送应答信号
* Description --> 每从 BM8563 读取一个字节数据后都要发送应答信号
* Input --> a:应答信号
* 0:应答信号
* 1:非应答信号
* Output --> none
* Reaturn --> none
******************************************************************************/
void IIC_Ack(uint8_t a)
{
if(a) IIC_SDA_SET(); //放上应答信号电平
else IIC_SDA_RESET();
Delay_Nus(10);
IIC_SCL_SET(); //为SCL下降做准备
Delay_Nus(10);
IIC_SCL_RESET(); //突变,将应答信号发送过去
Delay_Nus(2);
}
/******************************************************************************
* Function Name --> 向IIC总线发送一个字节数据
* Description --> 向 BM8563 写一个字节的数据
* Input --> dat:要发送的数据
* Output --> none
* Reaturn --> ack:返回应答信号
******************************************************************************/
void IIC_Write_Byte(uint8_t dat)
{
uint8_t i;
for(i=0;i<8;i++)
{
if((dat<<i)&0x80)
{
IIC_SDA_SET(); //判断发送位,先发送高位
}
else
{
IIC_SDA_RESET();
}
Delay_1us();
IIC_SCL_SET(); //为SCL下降做准备
Delay_Nus(10);
IIC_SCL_RESET(); //突变,将数据位发送过去
} //字节发送完成,开始接收应答信号
Delay_Nus(2);
IIC_SDA_SET(); //释放数据线
Delay_Nus(2);
IIC_SCL_SET(); //为SCL下降做准备
Delay_Nus(4);
if(IN_SDA())//读取应答信号
{
ack = 0;
}
else
{
ack = 1;
}
IIC_SCL_RESET();
Delay_Nus(2);
}
/******************************************************************************
* Function Name --> 从IIC总线上读取一个字节数据
* Description --> none
* Input --> none
* Output --> none
* Reaturn --> x:读取到的数据
******************************************************************************/
uint8_t IIC_Read_Byte(void)
{
uint8_t i;
uint8_t rect=0;
IIC_SDA_SET(); //首先置数据线为高电平
for(i=0;i<8;i++)
{
Delay_1us();
IIC_SCL_RESET();
Delay_Nus(10);
IIC_SCL_SET();
Delay_Nus(2);
rect=rect<<1;
if(IN_SDA())
{
rect=rect+1;
}
Delay_Nus(2);
}
IIC_SCL_RESET();
Delay_Nus(2);
return rect; //返回读取到的数据
}
/********************************************************************
函 数 名: GetBM8563(void)
功 能:从 BM8563 的内部寄存器(时间、状态、报警等寄存器)读取数据
说 明:该程序函数用来读取 BM8563 的内部寄存器,譬如时间,报警,状态等寄存器
采用页写的方式,设置数据的个数为 no,no 参数设置为 1 就是单字节方式
调 用:Start_I2C(),SendByte(),RcvByte(),Ack_I2C(),Stop_I2C()
入口参数:sla(BM8563 从地址), suba(BM8563 内部寄存器地址)
*s(设置读取数据存储的指针), no(传输数据的个数)
返 回 值:有,用来鉴定传输成功否
***********************************************************************/
uint8_t GetBM8563(uint8_t sla,uint8_t suba,uint8_t *s,uint8_t no)
{
uint8_t i;
IIC_Start();
IIC_Write_Byte(sla);
if(ack==0)return(0);
IIC_Write_Byte(suba);
if(ack==0)return(0);
IIC_Start();
IIC_Write_Byte(sla+1);
if(ack==0)return(0);
for(i=0;i<no-1;i++)
{
*s=IIC_Read_Byte();
IIC_Ack(0);
s++;
}
*s=IIC_Read_Byte();
IIC_Ack(1);
IIC_Stop();//除最后一个字节外,其他都要从 MASTER 发应答。
return(1);
}
/********************************************************************
函 数 名:SetBM8563(void)
功 能:设置 BM8563 的内部寄存器(时间,报警等寄存器)
说 明:该程序函数用来设置 BM8563 的内部寄存器,譬如时间,报警,状态等寄存器
采用页写的方式,设置数据的个数为 no,no 参数设置为 1 就是单字节方式
调 用:Start_I2C(),SendByte(),Stop_I2C()
入口参数:sla(BM8563 从地址), suba(BM8563 内部寄存器地址)
*s(设置初始化数据的指针), no(传输数据的个数)
返 回 值:有,用来鉴定传输成功否
***********************************************************************/
uint8_t SetBM8563(uint8_t sla,uint8_t suba,uint8_t *s,uint8_t no)
{
uint8_t i;
IIC_Start();
IIC_Write_Byte(sla);
if(ack==0)return(0);
IIC_Write_Byte(suba);
if(ack==0)return(0);
for(i=0;i<no;i++)
{
IIC_Write_Byte(*s);
if(ack==0)return(0);
s++;
}
IIC_Stop();
return(1);
}
/********************************************************************
函 数 名:void Bcd2asc(void)
功 能:bcd 码转换成 asc 码,供液晶显示用
说 明:
调 用:
入口参数:
返 回 值:无
***********************************************************************/
void Bcd2asc(void)
{
uint8_t i,j;
for (j=0,i=0; i<7; i++)
{
asc[j++] =(trdata[i]&0xf0)>>4|0x30 ;/*格式为: 秒 分 时 日 月 星期 年 */
asc[j++] =(trdata[i]&0x0f)|0x30;
}
}
/********************************************************************
函 数 名:datajust(void)
功 能:将读出的时间数据的无关位屏蔽掉
说 明:BM8563 时钟寄存器中有些是无关位,可以将无效位屏蔽掉
调 用:
入口参数:
返 回 值:无
***********************************************************************/
void datajust(void)
{
trdata[0] = trdata[0]&0x7f;
trdata[1] = trdata[1]&0x7f;
trdata[2] = trdata[2]&0x3f;
trdata[3] = trdata[3]&0x3f;
trdata[4] = trdata[4]&0x07;
trdata[5] = trdata[5]&0x1f;
trdata[6] = trdata[6]&0xff;
}
/********************************************************************
函 数 名:Set_Start_BM8563(void)
功 能:配置启动BM8563
说 明:
调 用:
入口参数:
返 回 值:无
***********************************************************************/
void Set_Start_BM8563(void)
{
printf_sz_hex(twdata,9);
do
{
bm_status = SetBM8563(0xa2,0x00,twdata,0x09);//设置时间和日期
printf("设置时间\r\n");
printf_sz_hex(twdata,9);
}
while(bm_status==0);
printf("设置成功\r\n");
}
下面是printf重定向到串口,串口输出十六进制数组函数。
/*重定义Printf函数到串口3*/
int fputc(int ch,FILE *f)
{
HAL_UART_Transmit(&huart3,(uint8_t *)&ch,1,0xFFFF);
return ch;
}
/*以十六进制格式输出数组*/
void printf_sz_hex(uint8_t *pdata,uint32_t len)
{
printf("\r\n");
for(uint32_t i=0;i<len;i++)
{
printf("%02X ", *(pdata+i));//十六进制格式输出,两位,高位没有则补0,X字母大写,x字母小写
}
printf("\r\n");
}
然后在main函数里调用void Set_Start_BM8563(void)即可。
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART3_UART_Init();
HAL_Delay(1000);
Set_Start_BM8563(); //设置时间、日期
while (1)
{
HAL_Delay(1000);
HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);
do{
bm_status=GetBM8563(0xa2,0x02,trdata,0x07);//测试读取时间、日期
}
while(bm_status==0);
datajust();
Bcd2asc();
printf_sz_hex(asc,14); //打印时间、日期
}
}
五、测试结果
串口助手每秒打印输出一次时间日期。可以看出,读出来的秒是走的。开始时用贴片3225封装的32.768K晶振,怎么弄也不走,最后查了手册需要30K欧ESR的晶振,然后又采购换上了这种圆柱体的,测试很稳定。
最后,如果晶振不起振,IIC一样可以读写寄存器,读写寄存器不依赖32.768K晶振。
遇到什么问题,一定要仔细阅读官方手册。也欢迎大家交流。