基于RA6M5的桌面时钟:FSP配置、DS3231时间基准与CTSU触摸交互
2026/9/16 21:35:25 网站建设 项目流程

简介:一套基于瑞萨MCU的桌面电子时钟设计源码,面向全国大学生电子设计竞赛本科组参赛者及嵌入式学习者,提供可直接运行的完整工程,覆盖时钟驱动、触摸按键、LCD显示、低功耗与串口调试等模块。压缩包共208个文件,以119个h头文件和53个c源文件为核心,辅以CMake构建脚本、链接脚本、工程配置文件及说明文档,便于在Keil或e² studio等环境中导入编译;整体仅1.03MB,结构清晰。程序为实战案例,经测试可直接烧录验证,适合在备赛阶段参考整体框架、学习外设驱动写法或快速搭建功能原型。已有148人学习下载,可作为电赛作品设计与代码组织的实用参考。

1. 桌面时钟比电赛任何一块开发板都难调

准备一台桌面电子时钟,比多数人想象中难。难点不在显示、不在外壳,而在两处:时间走不准,交互做不顺手。晶振温漂一天能差好几秒;机械按键在桌面上用三个月就发涩。这个源码包里的方案,主控选瑞萨 RA6M5,走 FSP/RASC 配置外设栈,外挂 DS3231 硬件 RTC 做时间基准,再用 CTSU 电容触摸替代机械按键,配合 Data Flash 存参数、SCI UART 做调试口。从 r_ctsu.c、rm_touch.c 到 driver_ds3231.c,工程里每个文件都对应一条清晰的外设链路。准备电赛或想把手头开发板做成能一直在桌上跑的小设备,这份代码可以直接拆开对照着改。

2. FSP/RASC 生成工程:从文件清单反推外设链路

2.1 文件分层与职责分配

源码包解压后的文件清单,能直接看出这个工程的分层方式:BSP 层由 bsp_clocks.c 负责时钟树初始化;驱动层是 FSP 生成的 r_ctsu.c、r_iic_master.c、r_sci_uart.c、r_gpt.c、r_flash_lp.c;中间件层 rm_touch.c 在 r_ctsu 之上做触摸识别;应用层 driver_ds3231.c 和 font24.c 分别处理 RTC 芯片与显示字库。这种分层不是作者自由发挥,而是 RASC 配置工具按外设栈自动生成的骨架,先看懂它,再动手改代码才有方向。

文件所属层职责定位
bsp_clocks.cBSP系统时钟树初始化,配置 PLL 与各外设分频
r_ctsu.cFSP 驱动CTSU 电容触摸底层,处理电极扫描时序
rm_touch.cFSP 中间件触摸基线跟踪、阈值判定、按键事件产生
r_iic_master.cFSP 驱动IIC 主机总线,读写 DS3231 等从机
driver_ds3231.c应用驱动DS3231 寄存器封装:时间读写、温度读取
r_gpt.cFSP 驱动通用定时器,工程中作 PWM 输出或心跳时基
r_sci_uart.cFSP 驱动串口,printf 日志输出,现场调试关键
r_flash_lp.cFSP 驱动低功耗 Data Flash,存亮度、闹钟等参数
font24.c应用24×24 点阵字库,配合 OLED/LCD 显示

从清单能注意到一个细节:没有 OLED 或 LCD 的驱动文件。电赛作品里屏幕驱动通常从厂商例程裁剪,真正体现设计的是 RTC、触摸、存储这套系统逻辑,所以源码包的裁剪策略保留了外设链路完整、去掉了屏幕适配,这是合理的。

2.2 RASC 里三步把外设栈配齐

瑞萨 RA6M5 的开发环境一般是 RASC(RA Smart Configurator)配合 Keil MDK 或 e² studio。RASC 里按顺序做三件事就能把外设栈生成出来:新建工程并选择芯片型号;在 Stacks 页面添加 CTSU、IIC Master、UART、GPT、Flash 这些外设栈;点击 Generate Project Content 生成代码。生成完成后,用户代码写在 FSP 注释块之间,否则下次重新生成会被覆盖。

/* 用户代码开始 */ extern volatile uint8_t touch_event_flag; extern volatile uint16_t g_iic_error_count; void main(void) { /* 1. 打开外设栈,顺序固定:总线类先开,传感器类后开 */ R_IIC_MASTER_Open(&g_i2c_master_ctrl, &g_i2c_master_cfg); R_CTSU_Open(&g_touch_ctrl, &g_touch_cfg); R_GPT_Open(&g_timer_pwm_ctrl, &g_timer_pwm_cfg); R_SCI_UART_Open(&g_uart_ctrl, &g_uart_cfg); R_FLASH_LP_Open(&g_flash_ctrl, &g_flash_cfg); /* 2. 启动外设 */ R_CTSU_Start(&g_touch_ctrl); R_GPT_Start(&g_timer_pwm_ctrl); R_SCI_UART_Start(&g_uart_ctrl); /* 3. 主循环:轮询标志位,不做阻塞等待 */ while (1) { if (touch_event_flag) { handle_touch_event(); touch_event_flag = 0; } ds3231_poll(); /* 非阻塞查询秒变化,触发显示刷新 */ R_BSP_SoftwareDelay(1, BSP_DELAY_UNITS_MILLISECONDS); } } /* 用户代码结束 */

这段初始化顺序有两个讲究。第一,IIC 这类总线外设必须最先 Open,因为 CTSU 和 RTC 的寄存器配置依赖总线通道;第二,主循环里用标志位而不是直接在回调里做业务处理,CTSU 的触摸回调跑在中断上下文,在里面读写 DS3231 或者操作 IIC 会拖慢中断响应,极端情况下还会和主循环的 IIC 事务抢总线。标志位 + 轮询的处理方式在电赛代码里最常见,也最容易调试。

2.3 时钟树与外设分频的关系

RASC 生成工程后,bsp_clocks.c 里写的是系统时钟的最终配置结果。RA6M5 的主时钟通常用外部晶振倍频到 200 MHz,再经分频器输出 PCLK、ICLK 等外设时钟。IIC 的 SCL 频率、GPT 的计数时钟、SCI UART 的波特率,全部由这套分频关系决定。在 Keil 里调试时,打开 Register 窗口核对一下 PLL 是否锁定、各外设时钟是否被意外关闭,能省掉大量现场排查时间。外设栈生成时如果没有显式设置分频系数,RASC 会按默认值配置,但把 SCL 从 400 kHz 改到 100 kHz 来兼容某些老旧 RTC 模块,是在 RASC 里改 IIC 外设栈的频率参数就能做到的。

3. DS3231 驱动:IIC 时序与时间基准的可靠设计

3.1 为什么不用片上 RTC,而是外挂 DS3231

桌面时钟的核心指标不是显示花样,是时间误差。RA6M5 片内确实带 RTC 外设,但它的精度完全取决于外部 32.768 kHz 晶振,普通晶振温漂在 ±20 ppm 量级,折算下来一天误差约 1.7 秒,放桌面上跑一周就漂了十几秒,电赛评测时评委拿手机一对时间就露馅。DS3231 把晶振和温度补偿电路封装在一起,0 到 40℃ 范围内精度 ±2 ppm,折合每天误差约 0.17 秒,同时带 VBAT 备电引脚,断电后时间照走。多花几块钱换一个能稳定跑的时间基准,对电赛作品来说是非常划算的取舍。

3.2 IIC 连续读时序与 BCD 转换

DS3231 的从机地址是 0x68,这是 7 位地址写法,左移一位后才是标准 IIC 写地址 0xD0。读时间时先写寄存器地址 0x00,再连续读 7 个字节,对应秒、分、时、星期、日、月、年。FSP 的 IIC 主机驱动里,Write 和 Read 分开调用,中间靠 restart 参数衔接。

#define DS3231_ADDR (0x68 << 1) /* 7位地址左移,FSP内部会再处理 */ static uint8_t bcd2dec(uint8_t bcd) { return (bcd >> 4) * 10 + (bcd & 0x0F); } bool ds3231_get_time(ds3231_time_t *t) { uint8_t buf[7] = {0}; /* 从寄存器0x00开始,先写寄存器地址,restart=true表示写完后不释放总线 */ R_IIC_MASTER_Write(&g_i2c_master_ctrl, (uint8_t[]){0x00}, 1, true); /* 紧接着发读命令,连续读7字节 */ if (R_IIC_MASTER_Read(&g_i2c_master_ctrl, buf, 7, false) != FSP_SUCCESS) { return false; } /* 掩码处理:秒的bit7是CH振荡器停止位,时的bit6是12/24小时制 */ t->sec = bcd2dec(buf[0] & 0x7F); t->min = bcd2dec(buf[1] & 0x7F); t->hour = bcd2dec(buf[2] & 0x3F); t->day = bcd2dec(buf[3] & 0x3F); t->month = bcd2dec(buf[5] & 0x1F); t->year = bcd2dec(buf[6]); return true; }

这个函数最关键的是 Write 和 Read 之间的衔接。R_IIC_MASTER_Write的最后一个参数是 restart,置 true 时控制器在写完寄存器地址后会生成 Repeated START 条件,而不是先发 STOP 再重新启动总线,这样从机才能保持被选中状态。如果这里写 false,DS3231 就认为一次写事务已经结束,后续 Read 拿到的数据全部偏移。电赛现场经常出现时间读回来是 0xFF 或数值错位,多半就是 restart 参数没有置 true。

BCD 转换也要提防坑。DS3231 内部寄存器存的是 BCD 码,0x59 表示 59 秒,读出来之后必须先按高半字节 ×10 加低半字节换算成十进制,才能用于显示或比较。反过来设置时间时要走 dec2bcd,可以顺手写一个对称函数。另外要注意月份寄存器 bit5 是世纪位,读取时先与 0x1F 相与再转换,否则在 2000 年之后月份会多出 16。

3.3 校正与排错:振荡器停止标志和总线死锁

DS3231 首次上电或备用电池失效后,内部振荡器可能处于停止状态,状态表现在秒寄存器的 bit7(CH 位)。初始化时除了写时间,必须把这一位清零,否则秒不回走。后面每次读取若发现秒寄存器 bit7 又被置位,说明振荡器确实停过,日志里要主动上报。

bool ds3231_check_and_reset_osc(void) { uint8_t sec_reg = 0; /* 读0x00寄存器 */ R_IIC_MASTER_Write(&g_i2c_master_ctrl, (uint8_t[]){0x00}, 1, true); R_IIC_MASTER_Read(&g_i2c_master_ctrl, &sec_reg, 1, false); if (sec_reg & 0x80) { /* 这位写0才能启动振荡器 */ sec_reg &= 0x7F; R_IIC_MASTER_Write(&g_i2c_master_ctrl, (uint8_t[]){0x00, sec_reg}, 2, false); return true; /* 发生过振荡停止,需要上层感知 */ } return false; }
寄存器地址功能掩码/位说明
0x00bit7 = CH 振荡器停止标志
0x01bit7 = 保留,读回始终为 0
0x02bit6 = 12/24 小时制选择
0x03星期1 表示周日,7 表示周六
0x04日期bit5-6 保留
0x05月份/世纪bit7 = 世纪标志
0x0600-99,需加 2000 基准
0x11-0x12温度有符号 10 位,分辨率 0.25℃

IIC 总线死锁是另一个高频故障。FSP 的 IIC 主机在读写中途如果被高优先级中断打断,SDA 可能停在低电平,后续所有通信全部失败。工程里建议加一个错误计数,在 R_IIC_MASTER_Read/Write 返回错误时累加,主循环里发现连续失败超过 3 次就执行 9 个时钟周期伪脉冲来释放总线。这个机制在电赛现场非常实用,因为外部传感器模块经常用杜邦线连接,杜邦线松动就会造成总线异常。

4. CTSU 触摸状态机与 GPT 呼吸灯:交互层的工程化实现

4.1 自电容原理与 rm_touch 中间件的价值

CTSU 是瑞萨的电容触摸单元,RA6M5 上用的是自电容方案。每个触摸通道接一块铜箔电极,电极对地形成寄生电容,手指靠近时电容变大,CTSU 通过电荷转移方式测量这个变化量。比机械按键好的地方在于:表面无开孔、防水、寿命长,适合桌面设备长期使用。但裸的 r_ctsu.c 只能输出原始电容值,不能直接当按键用,温度漂移和湿度变化都会让基线移动,所以工程里用 rm_touch.c 这个 FSP 中间件包了一层,它负责基线跟踪和按键事件判定。

基线跟踪是中间件的核心功能。手指按住电极,数据上升很快,会被判定为触摸;环境缓慢变潮,数据同样会上涨,但上涨速度慢,rm_touch 就把基线跟着抬高,避免误触发。理解这一点后,调试方向就很明确:快速靠近有反应说明硬件通路正常,缓慢环境变化导致误触发时,先查灵敏度配置而不是怀疑硬件。

4.2 RASC 中 CTSU 参数配置与实测校准

RASC 里添加 CTSU 外设栈时,会自动附带 rm_touch 中间件,需要在中间件属性页配三个关键参数:测量次数、灵敏度、滞后阈值。测量次数是单次按键检测中对同一电极扫描取平均的次数,取值越大越抗噪,响应越慢;灵敏度是基线偏移多少倍被判定为按下;滞后阈值用于释放判定,防止在临界点反复抖动。

参数推荐范围调整方向
Scan Count(测量次数)4 ~ 8环境干扰大则增加
Touch Sensitivity2 ~ 4面板厚则减小数值提高灵敏度
Release Threshold1.5 ~ 2.0与按下阈值保持滞后差
Filter(滤波寄存器)2数值越大越平滑但延迟增加

实际调试时不要对着参数盲调。工程里建议在 UART 调试口周期打印每个通道的当前数据和基线值,手掌悬停在电极上方慢慢靠近,观察数据从几百跳到几千的拐点,以这个拐点的 1/2 到 1/3 作为按下阈值。电赛现场最常见的错误是拿体积大的触摸模块直接替换原电极,导致寄生电容翻几倍,原参数全部失效,重新标定一遍只需要五分钟。

4.3 回调置标志、主循环跑状态机

rm_touch 中间件在检测到触摸事件后调用注册的回调函数,但回调运行在中断上下文,做状态迁移和显示刷新都不合适。工程里的标准做法是回调里只置一个标志位,主循环轮询到标志后再处理。

/* 触摸回调,中断上下文,只做置位 */ void touch_callback(ctsu_callback_args_t *p_args) { if (p_args->event == RM_TOUCH_EVENT_SINGLE_CLICK) { touch_event_flag = 1; } } /* 主循环中处理触摸事件,带短按/长按状态机 */ static void handle_touch_event(void) { static uint32_t press_ticks = 0; static uint8_t state = 0; switch (state) { case 0: /* 空闲态:检测到按下进入消抖 */ press_ticks = g_tick_count; state = 1; break; case 1: /* 按下确认态:持续按住时间超过500ms判定长按 */ if (g_tick_count - press_ticks > 500) { brightness_up(); /* 长按调背光 */ state = 2; } else { state = 0; } break; case 2: /* 释放检测 */ state = 0; break; } }

状态机的好处是短按、长按、双击可以共用一个回调,业务逻辑集中在主循环里,printf 调试也方便。注意 g_tick_count 通常由另一个 GPT 定时器中断维护,这个中断不能和 IIC 抢优先级。建议 GPT 心跳中断优先级设为最低,触摸回调优先级居中,IIC 错误恢复逻辑放在主循环里做,这样最稳定。

4.4 GPT 定时器做 PWM 呼吸灯与背光控制

桌面时钟的背光如果只有亮和灭两档,深夜用起来很刺眼。r_gpt.c 在工程里承担 PWM 输出职责,把 GPT 配置成 PWM 模式后,用占空比控制背光亮度。呼吸灯效果其实就是在主循环里周期改变占空比,配合一个小延时。

void backlight_breathe_step(uint8_t *p_brightness, uint8_t *p_direction) { /* 0~100渐变,到达边界后反转方向 */ if (*p_direction) { (*p_brightness)++; if (*p_brightness >= 100) { *p_direction = 0; } } else { (*p_brightness)--; if (*p_brightness <= 20) { *p_direction = 1; } } R_GPT_DutyCycleSet(&g_timer_pwm_ctrl, *p_brightness, GPT_IO_PIN_GTIOCA); }

R_GPT_DutyCycleSet 的第二个参数是占空比千分比,100 表示 10%,所以渐变步长用 1 的话,肉眼看到的是平滑呼吸。占空比更新后,对应引脚的输出立即变化,不需要重新 Start 定时器。亮度值建议掉电前写入 Data Flash,下次开机先读回来做主循环初始亮度,而不是每次开机都从 100% 开始闪一下。

5. Data Flash 参数持久化与 UART 故障诊断

5.1 Data Flash 写擦时序与掉电保存

亮度、闹钟时间这类参数必须存进非易失存储。RA6M5 有独立 Data Flash,由 r_flash_lp.c 驱动,但使用前要清楚两个规则:Flash 只能按位写 0,必须先擦除才能写入有效数据;擦除的最小单位比字节大得多,按块擦除后整块变 0xFF,改一个字节也要先擦除整块。

/* 写入前必须擦除,擦除后回读验证 */ static void flash_save_brightness(uint8_t brightness) { R_FLASH_LP_Erase(&g_flash_ctrl, BRIGHTNESS_SAVE_ADDR, 1); while (R_FLASH_LP_StatusGet(&g_flash_ctrl) != FSP_SUCCESS) { /* 等待擦除完成 */ } R_FLASH_LP_Write(&g_flash_ctrl, BRIGHTNESS_SAVE_ADDR, (uint32_t)&brightness, 1); while (R_FLASH_LP_StatusGet(&g_flash_ctrl) != FSP_SUCCESS) { /* 等待写入完成 */ } }

片段里的R_FLASH_LP_Write第三个参数传的是变量地址而不是变量值,FSP 的写接口需要源地址和数据长度。擦除和写入都建议带超时计数,防止 Flash 控制器异常时主循环死等。生命周期也要注意,Data Flash 擦写寿命通常在一万次量级,亮度调节每次变化都写一次显然不行,常见做法是只在退出设置界面或掉电前统一写一次。

5.2 UART 打印 CTSU 原始值与 IIC 错误计数

现场排错最快的手段是串口。工程里 r_sci_uart.c 负责的 UART 口,一般被用来重新映射 printf,之后像开发 PC 程序一样打印日志。调试触摸阈值时打印每个通道的当前值和基线值;调试 RTC 时打印读回的秒寄存器原始数据和转换后的时间字符串;还可以打印 IIC 错误计数,每分钟一次,观察总线健康度。日志格式建议统一成模块名: 数值 [单位],这样用串口助手过滤时更清晰。

int _write(int fd, char *pBuffer, int size) { R_SCI_UART_Write(&g_uart_ctrl, (uint8_t *)pBuffer, (uint32_t)size); return size; }

printf 重定向后,调试代码可以这样写:printf("CTSU ch%d raw=%d base=%d\r\n", ch, raw, base);上线前把这条日志注释掉即可。电赛现场判断系统是否健康,最有效的动作不是看屏幕,而是打开串口助手的定时显示:每秒打印一次 DS3231 秒变化和 IIC 错误计数,连续观察两分钟,秒值不回跳、错误计数不增长,再谈界面和交互的优化,否则后面做的所有逻辑都建立在不可靠的时间基准上。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询