一、硬件连接与系统架构
1.1 硬件连接表
| 模块 | STM32F103C8T6引脚 | 说明 |
|---|---|---|
| DS3231 RTC | ||
| SDA | PB7 (I2C1_SDA) | I2C数据线,需4.7kΩ上拉 |
| SCL | PB6 (I2C1_SCL) | I2C时钟线,需4.7kΩ上拉 |
| VCC | 3.3V | 主电源 |
| GND | GND | 共地 |
| TM1640 数码管驱动 | ||
| DIN | PA0 | 数据输入,GPIO模拟时序 |
| SCLK | PA1 | 时钟输入,GPIO模拟时序 |
| VCC | 5V | 数码管驱动电压 |
| GND | GND | 共地 |
| 4位共阴数码管 | ||
| 段选 | TM1640输出 | a,b,c,d,e,f,g,dp |
| 位选 | TM1640输出 | DIG1-DIG4 |
1.2 系统架构
┌─────────────┐ I2C ┌─────────────┐
│ STM32 │◄──────────►│ DS3231 │
│ F103C8T6 │ │ RTC时钟 │
└──────┬──────┘ └─────────────┘
│ GPIO模拟时序
▼
┌─────────────┐
│ TM1640 │◄─── 数码管显示
│ 驱动芯片 │ 4位时间显示
└──────┬──────┘
│
▼
┌─────────────┐
│ 4位共阴 │
│ 数码管 │
└─────────────┘
二、完整代码实现
2.1 DS3231驱动头文件(ds3231.h)
c
#ifndef __DS3231_H
#define __DS3231_H
#include "stm32f10x.h"
// DS3231 I2C地址
#define DS3231_ADDR 0xD0 // 写地址
#define DS3231_READ 0xD1 // 读地址
// DS3231寄存器地址
#define DS3231_SECOND 0x00
#define DS3231_MINUTE 0x01
#define DS3231_HOUR 0x02
#define DS3231_DAY 0x03
#define DS3231_DATE 0x04
#define DS3231_MONTH 0x05
#define DS3231_YEAR 0x06
#define DS3231_CONTROL 0x0E
#define DS3231_STATUS 0x0F
// 时间结构体
typedef struct {
uint8_t second;
uint8_t minute;
uint8_t hour;
uint8_t day; // 星期几 1-7
uint8_t date; // 日期 1-31
uint8_t month; // 月份 1-12
uint8_t year; // 年份 0-99
} RTC_TimeTypeDef;
// 函数声明
void DS3231_Init(void);
void DS3231_SetTime(RTC_TimeTypeDef *time);
void DS3231_GetTime(RTC_TimeTypeDef *time);
uint8_t DS3231_ReadTemp(void);
void DS3231_EnableAlarm(void);
void DS3231_DisableAlarm(void);
#endif /* __DS3231_H */
2.2 DS3231驱动源文件(ds3231.c)
c
#include "ds3231.h"
#include "i2c.h"
#include "delay.h"
/**
* @brief DS3231初始化
*/
void DS3231_Init(void)
{
// 初始化I2C
I2C_Init();
// 配置DS3231控制寄存器
uint8_t ctrl_reg = 0x00; // 禁用闹钟中断,方波输出1Hz
I2C_WriteByte(DS3231_ADDR, DS3231_CONTROL, ctrl_reg);
// 清除状态寄存器
uint8_t status_reg = 0x00;
I2C_WriteByte(DS3231_ADDR, DS3231_STATUS, status_reg);
printf("DS3231初始化完成\r\n");
}
/**
* @brief BCD转十进制
*/
static uint8_t BCD2DEC(uint8_t bcd)
{
return ((bcd >> 4) * 10) + (bcd & 0x0F);
}
/**
* @brief 十进制转BCD
*/
static uint8_t DEC2BCD(uint8_t dec)
{
return ((dec / 10) << 4) | (dec % 10);
}
/**
* @brief 设置时间
*/
void DS3231_SetTime(RTC_TimeTypeDef *time)
{
I2C_Start();
I2C_SendByte(DS3231_ADDR);
I2C_WaitAck();
I2C_SendByte(DS3231_SECOND);
I2C_WaitAck();
I2C_SendByte(DEC2BCD(time->second));
I2C_WaitAck();
I2C_SendByte(DEC2BCD(time->minute));
I2C_WaitAck();
I2C_SendByte(DEC2BCD(time->hour));
I2C_WaitAck();
I2C_SendByte(DEC2BCD(time->day));
I2C_WaitAck();
I2C_SendByte(DEC2BCD(time->date));
I2C_WaitAck();
I2C_SendByte(DEC2BCD(time->month));
I2C_WaitAck();
I2C_SendByte(DEC2BCD(time->year));
I2C_WaitAck();
I2C_Stop();
}
/**
* @brief 获取时间
*/
void DS3231_GetTime(RTC_TimeTypeDef *time)
{
I2C_Start();
I2C_SendByte(DS3231_ADDR);
I2C_WaitAck();
I2C_SendByte(DS3231_SECOND);
I2C_WaitAck();
I2C_Start();
I2C_SendByte(DS3231_READ);
I2C_WaitAck();
time->second = BCD2DEC(I2C_ReadByte());
I2C_SendAck();
time->minute = BCD2DEC(I2C_ReadByte());
I2C_SendAck();
time->hour = BCD2DEC(I2C_ReadByte());
I2C_SendAck();
time->day = BCD2DEC(I2C_ReadByte());
I2C_SendAck();
time->date = BCD2DEC(I2C_ReadByte());
I2C_SendAck();
time->month = BCD2DEC(I2C_ReadByte());
I2C_SendAck();
time->year = BCD2DEC(I2C_ReadByte());
I2C_SendNotAck();
I2C_Stop();
}
/**
* @brief 读取温度
*/
uint8_t DS3231_ReadTemp(void)
{
uint8_t temp;
I2C_Start();
I2C_SendByte(DS3231_ADDR);
I2C_WaitAck();
I2C_SendByte(0x11); // 温度寄存器
I2C_WaitAck();
I2C_Start();
I2C_SendByte(DS3231_READ);
I2C_WaitAck();
temp = I2C_ReadByte();
I2C_SendNotAck();
I2C_Stop();
return temp;
}
2.3 TM1640驱动头文件(tm1640.h)
c
#ifndef __TM1640_H
#define __TM1640_H
#include "stm32f10x.h"
// TM1640命令定义
#define TM1640_CMD_DATA_AUTO 0x40 // 自动地址增加模式
#define TM1640_CMD_DATA_FIXED 0x44 // 固定地址模式
#define TM1640_CMD_DISPLAY 0x88 // 显示控制命令
#define TM1640_CMD_ADDRESS 0xC0 // 显示地址命令
// 显示亮度级别
typedef enum {
BRIGHTNESS_1_16 = 0x00, // 1/16亮度
BRIGHTNESS_2_16 = 0x01, // 2/16亮度
BRIGHTNESS_4_16 = 0x02, // 4/16亮度
BRIGHTNESS_10_16 = 0x03, // 10/16亮度
BRIGHTNESS_11_16 = 0x04, // 11/16亮度
BRIGHTNESS_12_16 = 0x05, // 12/16亮度
BRIGHTNESS_13_16 = 0x06, // 13/16亮度
BRIGHTNESS_14_16 = 0x07 // 14/16亮度
} BrightnessLevel;
// 数码管段码定义(共阴极)
#define SEG_0 0x3F // 0
#define SEG_1 0x06 // 1
#define SEG_2 0x5B // 2
#define SEG_3 0x4F // 3
#define SEG_4 0x66 // 4
#define SEG_5 0x6D // 5
#define SEG_6 0x7D // 6
#define SEG_7 0x07 // 7
#define SEG_8 0x7F // 8
#define SEG_9 0x6F // 9
#define SEG_A 0x77 // A
#define SEG_B 0x7C // b
#define SEG_C 0x39 // C
#define SEG_D 0x5E // d
#define SEG_E 0x79 // E
#define SEG_F 0x71 // F
#define SEG_OFF 0x00 // 熄灭
#define SEG_DOT 0x80 // 小数点
// 函数声明
void TM1640_Init(void);
void TM1640_WriteCommand(uint8_t cmd);
void TM1640_WriteData(uint8_t addr, uint8_t data);
void TM1640_DisplayNumber(uint8_t pos, uint8_t num, uint8_t dot);
void TM1640_DisplayTime(uint8_t hour, uint8_t minute);
void TM1640_ClearAll(void);
void TM1640_SetBrightness(BrightnessLevel brightness);
#endif /* __TM1640_H */
2.4 TM1640驱动源文件(tm1640.c)
c
#include "tm1640.h"
#include "delay.h"
// GPIO定义
#define TM1640_DIN_PORT GPIOA
#define TM1640_DIN_PIN GPIO_Pin_0
#define TM1640_SCLK_PORT GPIOA
#define TM1640_SCLK_PIN GPIO_Pin_1
/**
* @brief TM1640延时函数
*/
static void TM1640_Delay(void)
{
uint8_t i;
for(i = 0; i < 10; i++);
}
/**
* @brief TM1640初始化
*/
void TM1640_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// 使能GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 配置GPIO为推挽输出
GPIO_InitStructure.GPIO_Pin = TM1640_DIN_PIN | TM1640_SCLK_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(TM1640_DIN_PORT, &GPIO_InitStructure);
// 初始状态
GPIO_SetBits(TM1640_SCLK_PORT, TM1640_SCLK_PIN);
GPIO_SetBits(TM1640_DIN_PORT, TM1640_DIN_PIN);
// 设置自动地址增加模式
TM1640_WriteCommand(TM1640_CMD_DATA_AUTO);
// 设置亮度为中等
TM1640_SetBrightness(BRIGHTNESS_10_16);
// 清屏
TM1640_ClearAll();
}
/**
* @brief 写命令
*/
void TM1640_WriteCommand(uint8_t cmd)
{
uint8_t i;
GPIO_ResetBits(TM1640_SCLK_PORT, TM1640_SCLK_PIN);
TM1640_Delay();
for(i = 0; i < 8; i++)
{
GPIO_ResetBits(TM1640_SCLK_PORT, TM1640_SCLK_PIN);
if(cmd & 0x01)
GPIO_SetBits(TM1640_DIN_PORT, TM1640_DIN_PIN);
else
GPIO_ResetBits(TM1640_DIN_PORT, TM1640_DIN_PIN);
TM1640_Delay();
GPIO_SetBits(TM1640_SCLK_PORT, TM1640_SCLK_PIN);
TM1640_Delay();
cmd >>= 1;
}
GPIO_SetBits(TM1640_SCLK_PORT, TM1640_SCLK_PIN);
GPIO_SetBits(TM1640_DIN_PORT, TM1640_DIN_PIN);
}
/**
* @brief 写数据
*/
void TM1640_WriteData(uint8_t addr, uint8_t data)
{
uint8_t i;
// 发送地址命令
GPIO_ResetBits(TM1640_SCLK_PORT, TM1640_SCLK_PIN);
TM1640_Delay();
for(i = 0; i < 8; i++)
{
GPIO_ResetBits(TM1640_SCLK_PORT, TM1640_SCLK_PIN);
if((TM1640_CMD_ADDRESS | addr) & (1 << i))
GPIO_SetBits(TM1640_DIN_PORT, TM1640_DIN_PIN);
else
GPIO_ResetBits(TM1640_DIN_PORT, TM1640_DIN_PIN);
TM1640_Delay();
GPIO_SetBits(TM1640_SCLK_PORT, TM1640_SCLK_PIN);
TM1640_Delay();
}
// 发送数据
for(i = 0; i < 8; i++)
{
GPIO_ResetBits(TM1640_SCLK_PORT, TM1640_SCLK_PIN);
if(data & (1 << i))
GPIO_SetBits(TM1640_DIN_PORT, TM1640_DIN_PIN);
else
GPIO_ResetBits(TM1640_DIN_PORT, TM1640_DIN_PIN);
TM1640_Delay();
GPIO_SetBits(TM1640_SCLK_PORT, TM1640_SCLK_PIN);
TM1640_Delay();
}
GPIO_SetBits(TM1640_SCLK_PORT, TM1640_SCLK_PIN);
GPIO_SetBits(TM1640_DIN_PORT, TM1640_DIN_PIN);
}
/**
* @brief 显示数字
*/
void TM1640_DisplayNumber(uint8_t pos, uint8_t num, uint8_t dot)
{
uint8_t seg_data;
// 数字段码映射
static const uint8_t seg_table[] = {
SEG_0, SEG_1, SEG_2, SEG_3, SEG_4, SEG_5, SEG_6, SEG_7,
SEG_8, SEG_9, SEG_A, SEG_B, SEG_C, SEG_D, SEG_E, SEG_F
};
seg_data = seg_table[num % 16];
if(dot) seg_data |= SEG_DOT; // 添加小数点
TM1640_WriteData(pos, seg_data);
}
/**
* @brief 显示时间(HH:MM格式)
*/
void TM1640_DisplayTime(uint8_t hour, uint8_t minute)
{
// 第1位:小时十位
TM1640_DisplayNumber(0, hour / 10, 0);
// 第2位:小时个位
TM1640_DisplayNumber(1, hour % 10, 1); // 带小数点作为冒号
// 第3位:分钟十位
TM1640_DisplayNumber(2, minute / 10, 0);
// 第4位:分钟个位
TM1640_DisplayNumber(3, minute % 10, 0);
}
/**
* @brief 清屏
*/
void TM1640_ClearAll(void)
{
uint8_t i;
for(i = 0; i < 16; i++)
{
TM1640_WriteData(i, SEG_OFF);
}
}
/**
* @brief 设置亮度
*/
void TM1640_SetBrightness(BrightnessLevel brightness)
{
TM1640_WriteCommand(TM1640_CMD_DISPLAY | brightness | 0x01); // 开启显示
}
2.5 主程序(main.c)
c
#include "stm32f10x.h"
#include "ds3231.h"
#include "tm1640.h"
#include "delay.h"
#include "usart.h"
RTC_TimeTypeDef current_time;
int main(void)
{
// 系统初始化
SystemInit();
Delay_Init();
USART_Init(115200);
printf("STM32 + DS3231 + TM1640 实时时钟系统\r\n");
// 初始化DS3231
DS3231_Init();
// 初始化TM1640
TM1640_Init();
// 设置初始时间(首次使用时取消注释)
/*
current_time.second = 0;
current_time.minute = 30;
current_time.hour = 14;
current_time.day = 1; // 星期一
current_time.date = 15;
current_time.month = 12;
current_time.year = 23;
DS3231_SetTime(¤t_time);
printf("时间已设置\r\n");
*/
uint8_t last_second = 0;
uint8_t display_mode = 0; // 0:显示时间, 1:显示温度
while(1)
{
// 获取当前时间
DS3231_GetTime(¤t_time);
// 每秒更新显示
if(current_time.second != last_second)
{
last_second = current_time.second;
// 切换显示模式(每30秒切换一次)
if(current_time.second % 30 == 0)
{
display_mode = !display_mode;
}
if(display_mode == 0)
{
// 显示时间 HH:MM
TM1640_DisplayTime(current_time.hour, current_time.minute);
printf("时间: %02d:%02d:%02d\r\n",
current_time.hour, current_time.minute, current_time.second);
}
else
{
// 显示温度
uint8_t temp = DS3231_ReadTemp();
TM1640_DisplayNumber(0, temp / 10, 0);
TM1640_DisplayNumber(1, temp % 10, 1); // 带小数点
TM1640_DisplayNumber(2, 12, 0); // 显示"C"
TM1640_DisplayNumber(3, 0, 0); // 清空最后一位
printf("温度: %d°C\r\n", temp);
}
}
Delay_ms(100); // 100ms刷新一次
}
}
三、I2C底层驱动(i2c.c)
c
#include "i2c.h"
#include "delay.h"
/**
* @brief I2C初始化
*/
void I2C_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_SetBits(GPIOB, GPIO_Pin_6 | GPIO_Pin_7); // 拉高总线
}
/**
* @brief I2C起始信号
*/
void I2C_Start(void)
{
SDA_OUT();
SCL_HIGH();
SDA_HIGH();
Delay_us(4);
SDA_LOW();
Delay_us(4);
SCL_LOW();
}
/**
* @brief I2C停止信号
*/
void I2C_Stop(void)
{
SDA_OUT();
SCL_LOW();
SDA_LOW();
Delay_us(4);
SCL_HIGH();
SDA_HIGH();
Delay_us(4);
}
/**
* @brief 等待应答
*/
uint8_t I2C_WaitAck(void)
{
uint8_t timeout = 0;
SDA_IN();
SCL_HIGH();
Delay_us(1);
while(SDA_READ())
{
if(++timeout > 250)
{
I2C_Stop();
return 1;
}
}
SCL_LOW();
return 0;
}
/**
* @brief 发送字节
*/
void I2C_SendByte(uint8_t byte)
{
uint8_t i;
SDA_OUT();
for(i = 0; i < 8; i++)
{
SCL_LOW();
if(byte & 0x80)
SDA_HIGH();
else
SDA_LOW();
byte <<= 1;
Delay_us(2);
SCL_HIGH();
Delay_us(2);
}
SCL_LOW();
}
/**
* @brief 读取字节
*/
uint8_t I2C_ReadByte(void)
{
uint8_t i, byte = 0;
SDA_IN();
for(i = 0; i < 8; i++)
{
SCL_LOW();
Delay_us(2);
SCL_HIGH();
byte <<= 1;
if(SDA_READ())
byte |= 0x01;
Delay_us(1);
}
SCL_LOW();
return byte;
}
void I2C_SendAck(void)
{
SDA_OUT();
SCL_LOW();
SDA_LOW();
Delay_us(2);
SCL_HIGH();
Delay_us(2);
SCL_LOW();
}
void I2C_SendNotAck(void)
{
SDA_OUT();
SCL_LOW();
SDA_HIGH();
Delay_us(2);
SCL_HIGH();
Delay_us(2);
SCL_LOW();
}
四、关键调试要点
4.1 常见问题排查
| 现象 | 原因 | 解决方法 |
|---|---|---|
| 数码管不亮 | TM1640时序错误 | 检查DIN和SCLK时序,增加延时 |
| 显示乱码 | 段码定义错误 | 确认是共阴还是共阳数码管 |
| 时间不走 | DS3231未初始化 | 检查I2C地址和寄存器配置 |
| 温度读数异常 | 读取温度寄存器错误 | 确认温度寄存器地址为0x11 |
4.2 优化建议
- 省电优化:在无操作时关闭数码管显示,或降低亮度
- 按键设置:添加按键用于调整时间和设置闹钟
- 掉电保护:DS3231自带电池,断电后时间继续走
- 显示特效:添加冒号闪烁效果,增强用户体验
参考代码 STM32+DS3231+TM1640驱动数码管显示数据 www.youwenfan.com/contentcsu/56169.html
五、总结
这套系统实现了:
- 高精度计时:DS3231精度±2ppm,年误差仅±1分钟
- 稳定显示:TM1640驱动4位数码管,亮度可调
- 低功耗:DS3231休眠电流仅200nA
- 易于扩展:可添加温度传感器、蜂鸣器等外设