实验任务
阅读资料了解 STM32F103的RTC(实时时钟)原理,使用带SPI或IIC接口的OLED屏显模块实现以下功能:
-
读取STM32F103C8T6 内部的时钟(年月日时分秒),日历(星期x),1秒周期,通过串口输出到PC上位机,;
-
读取AHT20的温度和湿度,通过OLED,把年月份时分秒、日历和实时温度、湿度显示出来,2秒周期。
RTC简介
一、RTC简介
1、RTC实时时钟特征与原理
RTC (Real Time Clock):实时时钟
实时时钟是一个独立的定时器。RTC模块拥有一组连续计数的计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当前的时间和日期。
RTC模块和时钟配置系统(RCC_BDCR寄存器)处于后备区域,即在系统复位或从待机模式唤醒后, RTC的设置和时间维持不变。
系统复位后,对后备寄存器和RTC的访问被禁止,这是为了防止对后备区域(BKP)的意外写操作。执行以下操作将使能对后备寄存器和RTC的访问:
设置寄存器RCC_APB1ENR的PWREN和BKPEN位,使能电源和后备接口时钟
设置寄存器PWR_CR的DBP位,使能对后备寄存器和RTC的访问。
2、RTC组成
APB1接口:用来和APB1总线相连。通过APB1接口可以访问RTC的相关寄存器(预分频值,计数器值,闹钟值)。
RTC核心:由一组可编程计数器组成。分两个主要模块。
第一个是RTC预分频模块,它可以编程产生最长1秒的RTC时间基TR_CLK。如果设置了秒中断允许位,可以产生秒中断。
第二个是32位的可编程计数器,可被初始化为当前时间。系统时间按TR_CLK周期累加并与存储在RTC_ALR寄存器中的可编程时间相比,当匹配时候如果设置了闹钟中断允许位,可以产生闹钟中断。
RTC内核完全独立于APB1接口,软件通过APB1接口对RTC相关寄存器访问。但是相关寄存器只在RTC APB1时钟进行重新同步的RTC时钟的上升沿被更新。所以软件必须先等待寄存器同步标志位(RTC_CRL的RSF位)被硬件置1才读。
实验代码
OLED模块代码编写
在工程中导入OLED相关文件:OLED.h、OLED.c、OLED_Font.h。
OLED.h中声明了引脚的初始化函数以及各种函数,用于显示不同的数据类型:
#ifndef __OLED_H
#define __OLED_H
void OLED_Init(void); // OLED初始化
void OLED_Clear(void); // 清屏
void OLED_ShowChar(uint8_t Line, uint8_t Column, char Char); // 指定位置显示一个字符
void OLED_ShowString(uint8_t Line, uint8_t Column, char *String); // 指定位置显示一个字符串
void OLED_ShowNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length); // 指定位置显示一个数字
void OLED_ShowSignedNum(uint8_t Line, uint8_t Column, int32_t Number, uint8_t Length); // 指定位置显示一个有符号数字
void OLED_ShowHexNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length); // 指定位置显示一个十六进制数
void OLED_ShowBinNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length); // 指定位置显示一个二进制数
#endif
在OLED_I2C_Init()函数中,我们指定PB8作为SCL,PB9作为SDA
/*引脚配置*/
#define OLED_W_SCL(x) GPIO_WriteBit(GPIOB, GPIO_Pin_8, (BitAction)(x))
#define OLED_W_SDA(x) GPIO_WriteBit(GPIOB, GPIO_Pin_9, (BitAction)(x))
GPIO_InitTypeDef GPIO_InitStructure;
/*引脚初始化*/
void OLED_I2C_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_Init(GPIOB, &GPIO_InitStructure);
OLED_W_SCL(1);
OLED_W_SDA(1);
}在这里插入代码片
I2C协议读取AHT20模块
本次实验将采用软件I2C进行温湿度的采集。
AHT20模块相关信息和代码请到官网下载
下面是部分函数的介绍:
文件中定义了两个变量:T1和H1,分别存放温度和湿度的数据。
uint32_t H1=0; //Humility
uint32_t T1=0; //Temperature
AHT20芯片的使用过程 read_AHT20_once函数:
void read_AHT20_once(void)
{
delay_ms(10);
reset_AHT20();//重置AHT20芯片
delay_ms(10);
init_AHT20();//初始化AHT20芯片
delay_ms(10);
startMeasure_AHT20();//开始测试AHT20芯片
delay_ms(80);
read_AHT20();//读取AHT20采集的到的数据
delay_ms(5);
}
AHT20芯片读取数据 read_AHT20函数,由于本实验需要在OLED上显示数据,因此在该函数中进行了部分修改,也就是把得到的数据显示到OLED上。
void read_AHT20(void)
{
uint8_t i;
for(i=0; i<6; i++)
{
readByte[i]=0;
}
I2C_Start();//I2C启动
I2C_WriteByte(0x71);//I2C写数据
ack_status = Receive_ACK();//收到的应答信息
readByte[0]= I2C_ReadByte();//I2C读取数据
Send_ACK();//发送应答信息
readByte[1]= I2C_ReadByte();
Send_ACK();
readByte[2]= I2C_ReadByte();
Send_ACK();
readByte[3]= I2C_ReadByte();
Send_ACK();
readByte[4]= I2C_ReadByte();
Send_ACK();
readByte[5]= I2C_ReadByte();
SendNot_Ack();
//Send_ACK();
I2C_Stop();//I2C停止函数
//判断读取到的第一个字节是不是0x08,0x08是该芯片读取流程中规定的,如果读取过程没有问题,就对读到的数据进行相应的处理
if( (readByte[0] & 0x68) == 0x08 )
{
H1 = readByte[1];
H1 = (H1<<8) | readByte[2];
H1 = (H1<<8) | readByte[3];
H1 = H1>>4;
H1 = (H1*1000)/1024/1024;
T1 = readByte[3];
T1 = T1 & 0x0000000F;
T1 = (T1<<8) | readByte[4];
T1 = (T1<<8) | readByte[5];
T1 = (T1*2000)/1024/1024 - 500;
AHT20_OutData[0] = (H1>>8) & 0x000000FF;
AHT20_OutData[1] = H1 & 0x000000FF;
AHT20_OutData[2] = (T1>>8) & 0x000000FF;
AHT20_OutData[3] = T1 & 0x000000FF;
}
else
{
AHT20_OutData[0] = 0xFF;
AHT20_OutData[1] = 0xFF;
AHT20_OutData[2] = 0xFF;
AHT20_OutData[3] = 0xFF;
printf("读取失败!!!");
}
// 下面是在OLED上显示数据
OLED_ShowString(1, 1, "Temperature:");
OLED_ShowNum(1, 13, T1/100, 1);
OLED_ShowNum(1, 14, (T1/10)%10, 1);
OLED_ShowChar(1, 15, '.');
OLED_ShowNum(1, 16, T1%10, 1);
OLED_ShowString(2, 1, "Humidity:");
OLED_ShowNum(2, 10, H1/100, 1);
OLED_ShowNum(2, 11, (H1/10)%10, 1);
OLED_ShowChar(2, 12, '.');
OLED_ShowNum(2, 13, H1%10, 1);
}
RTC时钟代码编写
创建两个文件:MyRTC.c,MyRTC.h,进行实时时钟的计算和显示。
#include "stm32f10x.h" // Device header
#include <time.h>
#include "OLED.h"
uint16_t MyRTC_Time[] = {2023, 11, 21, 14, 54, 00};
void MyRTC_SetTime(void);
char* weekday;
uint16_t iWeek;
// 根据日期计算星期
void CalculateWeekDay(uint16_t year, uint16_t month, uint16_t day) {
if (month == 1 || month ==2)
{
month +=12;
year--;
}
iWeek = (day + 2 * month + 3 * (month + 1)/5 + year + year/4 - year/100 + year/400) % 7;
switch (iWeek)
{
case 0: weekday = "Monday"; break;
case 1: weekday = "Teusday"; break;
case 2: weekday = "Wednesday"; break;
case 3: weekday = "Thursday"; break;
case 4: weekday = "Friday"; break;
case 5: weekday = "Saturday"; break;
case 6: weekday = "Sunday"; break;
default: break;
}
}
// 初始化函数
void MyRTC_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE);
if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5)
{
RCC_LSEConfig(RCC_LSE_ON);
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET);
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
RCC_RTCCLKCmd(ENABLE);
RTC_WaitForSynchro();
RTC_WaitForLastTask();
RTC_SetPrescaler(32768 - 1);
RTC_WaitForLastTask();
MyRTC_SetTime();
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
}
else
{
RTC_WaitForSynchro();
RTC_WaitForLastTask();
}
}
void MyRTC_SetTime(void)
{
time_t time_cnt;
struct tm time_date;
time_date.tm_year = MyRTC_Time[0] - 1900;
time_date.tm_mon = MyRTC_Time[1] - 1;
time_date.tm_mday = MyRTC_Time[2];
time_date.tm_hour = MyRTC_Time[3];
time_date.tm_min = MyRTC_Time[4];
time_date.tm_sec = MyRTC_Time[5];
time_cnt = mktime(&time_date) - 8 * 60 * 60;
RTC_SetCounter(time_cnt);
RTC_WaitForLastTask();
}
// 读取时钟并显示
void MyRTC_ReadTime(void)
{
time_t time_cnt;
struct tm time_date;
time_cnt = RTC_GetCounter() + 8 * 60 * 60;
time_date = *localtime(&time_cnt);
MyRTC_Time[0] = time_date.tm_year + 1900;
MyRTC_Time[1] = time_date.tm_mon + 1;
MyRTC_Time[2] = time_date.tm_mday;
MyRTC_Time[3] = time_date.tm_hour;
MyRTC_Time[4] = time_date.tm_min;
MyRTC_Time[5] = time_date.tm_sec;
CalculateWeekDay(MyRTC_Time[0], MyRTC_Time[1], MyRTC_Time[2]);
// 下面把数据显示到OLED上
OLED_ShowNum(3, 1, MyRTC_Time[0], 4);
OLED_ShowChar(3, 5, '-');
OLED_ShowNum(3, 6, MyRTC_Time[1], 2);
OLED_ShowChar(3, 8, '-');
OLED_ShowNum(3, 9, MyRTC_Time[2], 2);
OLED_ShowNum(4, 1, MyRTC_Time[3], 2);
OLED_ShowChar(4, 3, ':');
OLED_ShowNum(4, 4, MyRTC_Time[4], 2);
OLED_ShowChar(4, 6, ':');
OLED_ShowNum(4, 7, MyRTC_Time[5], 2);
OLED_ShowString(4, 10, weekday);
}
OLED.h:
#ifndef __MYRTC_H
#define __MYRTC_H
extern uint16_t MyRTC_Time[];
void MyRTC_Init(void);
void MyRTC_SetTime(void);
void MyRTC_ReadTime(void);
#endif
以上就可以实现实时读取时钟并显示
main.c 代码编写
#include "delay.h"
#include "usart.h"
#include "bsp_i2c.h"
#include "OLED.h"
#include "MyRTC.h"
int main(void)
{
delay_init();
uart_init(9600);
OLED_Init();
MyRTC_Init();
IIC_Init();
MyRTC_SetTime();
while(1)
{
MyRTC_ReadTime(); // 读取时钟
read_AHT20_once(); // 读取AHT20
}
}
实验效果
实验总结
RTC模块配置完毕,我就能通过读取和设置寄存器来访问RTC的日期和时间。读取日期和时间的方法是读取相应的寄存器值,然后将其转换为易于理解的格式。比如,我可以读取RTC_DR寄存器来获取当前日历日期,通过读取RTC_TR寄存器来获取当前时钟时间,时钟部分秒数不准确,有时过了几秒钟OLED的显示才增加1秒,有时又会一次增加2秒。