基于RA6M5的桌面时钟:FSP配置、DS3231时间基准与CTSU触摸交互

基于RA6M5的桌面时钟:FSP配置、DS3231时间基准与CTSU触摸交互 简介一套基于瑞萨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 的开发环境一般是 RASCRA 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开始先写寄存器地址restarttrue表示写完后不释放总线 */ 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 再重新启动总线这样从机才能保持被选中状态。如果这里写 falseDS3231 就认为一次写事务已经结束后续 Read 拿到的数据全部偏移。电赛现场经常出现时间读回来是 0xFF 或数值错位多半就是 restart 参数没有置 true。BCD 转换也要提防坑。DS3231 内部寄存器存的是 BCD 码0x59 表示 59 秒读出来之后必须先按高半字节 ×10 加低半字节换算成十进制才能用于显示或比较。反过来设置时间时要走 dec2bcd可以顺手写一个对称函数。另外要注意月份寄存器 bit5 是世纪位读取时先与 0x1F 相与再转换否则在 2000 年之后月份会多出 16。3.3 校正与排错振荡器停止标志和总线死锁DS3231 首次上电或备用电池失效后内部振荡器可能处于停止状态状态表现在秒寄存器的 bit7CH 位。初始化时除了写时间必须把这一位清零否则秒不回走。后面每次读取若发现秒寄存器 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; }寄存器地址功能掩码/位说明0x00秒bit7 CH 振荡器停止标志0x01分bit7 保留读回始终为 00x02时bit6 12/24 小时制选择0x03星期1 表示周日7 表示周六0x04日期bit5-6 保留0x05月份/世纪bit7 世纪标志0x06年00-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 错误计数连续观察两分钟秒值不回跳、错误计数不增长再谈界面和交互的优化否则后面做的所有逻辑都建立在不可靠的时间基准上。本文还有配套的精品资源点击获取