#第八届立创电赛# 基于瑞萨R7FA2E1A72DFL的11x7点阵屏时钟设计与实现
基于瑞萨R7FA2E1A72DFL的11x7点阵屏时钟设计与实现
最近在捣鼓点阵屏,想做个桌面小时钟。市面上成品很多,但自己动手做一个,看着它从无到有亮起来,那种成就感是完全不一样的。这次我用的是瑞萨的R7FA2E1A72DFL这款MCU,驱动两块总共11x7分辨率的橙色点阵屏,效果还挺酷的。
这个项目很适合刚接触嵌入式或者想参加电子设计竞赛的朋友。你会学到怎么用最少的IO口驱动一堆LED(点阵屏本质就是LED阵列),如何设计扫描显示算法让数字“动”起来,以及一些电源管理的小技巧。咱们不搞太复杂的,就从原理到代码,一步步把手把手把这个时钟做出来。
1. 硬件设计:从点阵屏到驱动电路
做硬件项目,第一步永远是搞清楚你要驱动的“主角”是什么。咱们这个项目的主角,就是这两块橙色的点阵屏。
1.1 点阵屏与驱动方案选择
我用的点阵屏是11列 x 7行的结构,单个屏有77个LED。两块屏拼在一起显示时间,就需要驱动154个LED。如果你天真地想用MCU的IO口直接去控制每一个LED的亮灭,那算一下:两块屏,每块有11+7=18个引脚(11个阳极,7个阴极),但为了独立控制,你需要1172 = 154个IO口!这显然不现实,就算最“土豪”的MCU也没这么多脚。
所以,我们必须请外援——驱动芯片。这里我选择了非常经典且便宜的74HC595移位寄存器。这个小芯片有什么好处呢?
- 节省IO口:一片595只需要3个IO(数据、时钟、锁存)就能控制8个输出。而且它可以无限级联,级联再多也只用这3个IO。
- 驱动能力强:它的每个输出引脚能提供35mA以上的电流(具体看型号),驱动LED小菜一碟。
- 价格便宜:一片几毛钱,成本可控。
我用了5片74HC595级联,这样就有了5 * 8 = 40个可控输出口。而我们实际只需要控制11*2 + 7 = 29个点阵屏引脚(两块屏的阳极共22个,阴极共7个),还绰绰有余。
注意:点阵屏引脚分为共阳和共阴。我用的这款,11针那排是阳极(正极),7针那排是阴极(负极)。理解这一点对后续电路连接和编程至关重要。
硬件连接上,我把5片595的输出,高位(靠后的595)的14个输出(除去4个空闲)连接到了7个阴极上,低位(靠前的595)的22个输出连接到了22个阳极上。这样,通过程序控制595输出的高低电平,就能精确控制每一个LED的亮灭了。
1.2 核心驱动原理:行扫描法
硬件连好了,怎么让它们显示出数字“1”、“2”呢?最直接的想法:想让数字“1”亮起来,就把构成“1”的那几列LED对应的阳极设为高电平,阴极设为低电平,不就行了?
我们来试试。假设显示数字“1”,需要下图中第三列的LED亮起。
如果同时把第三列阳极置高,第1-5行阴极置低,会发生什么?结果是第三列第1到5行的所有LED全亮了,成了一个亮条,而不是我们想要的“1”的形状。
问题出在“同时”上。所有该亮的LED一起亮,它们共享了阳极和阴极,电流路径就乱了,无法实现单独控制。这就引出了点阵屏驱动的核心算法:行扫描(或列扫描)。
我们的屏有7行(阴极)。行扫描的思路是:我们永远只点亮一行。
- 首先,只让第0行(最上面一行)的阴极有效(低电平),其他6行阴极无效(高电平)。
- 然后,检查在这一行里,哪些列的LED需要点亮(比如显示数字“2”时,第0行没有需要点亮的),就给这些列的阳极高电平。
- 保持这个状态一个极短的时间(比如1-2毫秒)。
- 接着,切换到第1行,重复步骤2-3。
- 如此循环,快速扫过所有7行。
由于人眼有“视觉暂留”效应,当这个扫描速度足够快(比如每秒扫描50次以上),我们看到的就是一个稳定、完整的数字图像了。这就像电影院放电影,也是一帧一帧快速播放,我们看起来就是连续的画面。
1.3 供电与低功耗设计
既然是时钟,最好能便携或者断电后还能走时。我设计了电池供电和USB供电自动切换的电路。
- 充电管理:使用了TP4054充电芯片,负责给锂电池充电,设置充电电流为500mA。接线简单,外围元件少。
- 电源路径管理:这是个小巧思。电路里用了一个PMOS管(Q1)和二极管(D1)。
- 当插入USB时:USB的5V电压使PMOS管的G极为高电平,PMOS关闭。此时,系统由USB 5V经二极管D1供电,同时USB电源也给电池充电。
- 当拔掉USB时:PMOS管的G极被拉低到地,PMOS导通。此时,系统由电池通过PMOS管供电。 这样就实现了无感切换,插上USB就用USB电,拔掉自动用电池电,保证时钟永不间断。
1.4 其他外围电路
- 蜂鸣器:我用的是无源蜂鸣器。无源蜂鸣器内部没有振荡源,需要给它一定频率的方波(PWM)才会响。好处是可以通过改变PWM频率来播放不同音调,比如整点报时、按键音效。电路很简单,一个三极管放大MCU的PWM信号驱动即可。
2. 软件架构:让点阵屏“活”起来
硬件是躯体,软件是灵魂。接下来我们看看如何用瑞萨的MCU编程,驱动点阵屏显示时间。
2.1 开发环境与基础配置
我使用Keil MDK进行开发。瑞萨提供了非常好用的图形化配置工具FSP (Flexible Software Package) Configurator。在FSP里,你可以像搭积木一样配置MCU的各种外设,它会自动生成底层初始化代码,大大降低了开发门槛。
在这个项目里,我主要配置了以下几个外设:
- UART:用于连接电脑串口,打印调试信息,方便查问题。
- RTC:实时时钟,这是时钟项目的核心,负责走时。
- ADC:可以用来监测电池电压。
- TIM0/TIM7:定时器。TIM0用来产生精确的毫秒级中断,作为系统“心跳”。TIM7用来做串口接收的超时判断。
- Flash:用于存储一些掉电不丢失的数据,比如时间设置、闹钟等。
配置好后,在代码里直接调用R_UART_Open()、R_RTC_Open()这样的API函数即可使用,非常方便。
踩坑提示:在FSP生成的工程里使用
printf函数打印到串口,需要自己重写fputc函数。网上有些针对其他ARM芯片的写法可能不适用,需要根据瑞萨的HAL库来调整,具体可以参照官方例程。
2.2 系统“心跳”:毫秒定时器
我习惯在项目里开一个1毫秒中断的定时器(比如TIM0),用它来维护一个全局的毫秒计时变量。这样做的好处太多了:
- 替代低效延时:不用
delay_ms()这类函数,它会让CPU空等,浪费资源。用定时器,CPU可以安心做其他事。 - 精准定时:可以轻松实现“每隔100毫秒做某事”、“按键长按2秒触发”等功能。
- 安全:在中断服务函数等场合,绝对不能使用阻塞式延时,毫秒定时器是唯一选择。
volatile uint32_t system_tick_ms = 0; // 全局系统滴答计数器 // 在1ms定时器中断服务函数里 void timer0_callback(timer_callback_args_t *p_args) { system_tick_ms++; // 每1ms加1 } // 获取当前滴答数 uint32_t get_tick(void) { return system_tick_ms; } // 实现非阻塞延时 void delay_ms_nonblocking(uint32_t ms) { uint32_t start_tick = get_tick(); while((get_tick() - start_tick) < ms) { // 这里可以插入一些低功耗的等待指令,或者什么都不做 } }2.3 点阵显示的核心:字库与扫描函数
这是整个软件最核心也最有意思的部分。我们要把“12:34”这样的时间,变成点阵屏上一行行点亮的数据。
第一步:制作字库我们需要为0-9这10个数字以及冒号“:”定义点阵数据。根据之前说的行扫描原理,我们为每个字符定义7行数据(对应7行阴极),每行数据表示在这一行上,哪些列的阳极需要为高电平。
以数字“2”为例,我们把它拆成7行来看,每一行需要点亮的列是不同的。把这些信息用二进制表示,再转换成十六进制,就得到了字库数组。
// 数字0-9以及冒号“:”的阳极字库数据 (共7行,每行数据对应一行扫描线) // 数据格式:每个数字占3列,数据为11列(两块屏)的阳极状态,高位在前 // 例如:数字“0”的第一行数据 0x00, 0x00, 0x00, 0xE0, 0x00 // 表示前两列空,第2-4列点亮(构成0的顶部),后面空... const uint8_t digit_font[11][7][5] = { // 数字0的数据 { {0x00, 0x00, 0x00, 0xE0, 0x00}, // 第0行 {0x00, 0x00, 0x00, 0x20, 0x40}, // 第1行 // ... 第2-6行 }, // 数字1的数据 { {0x00, 0x00, 0x00, 0x40, 0x00}, // 第0行 // ... 第1-6行 }, // ... 数字2-9和冒号的数据 };同时,我们还需要一个阴极选择数组。因为每次只点亮一行,所以这个数组很简单,就是7个数据,每个数据对应让其中一行阴极为低电平。
// 阴极选择数据,每次选通一行 (共7行,对应5个595的40位输出) // 高14位控制7个阴极(每个阴极占2位?需根据硬件连接调整),低位为阳极数据预留 const uint8_t row_select_data[7][5] = { {0xF0, 0xFF, 0xFF, 0xFF, 0xFE}, // 选通第0行 (阴极) {0xF0, 0xFF, 0xFF, 0xFF, 0xFD}, // 选通第1行 // ... 第2-6行 };第二步:时间拆解与数据组合有了字库,显示前需要把当前时间(比如12:34)拆解成单个数字:1, 2, :, 3, 4。然后根据扫描到的当前行号,从这5个字符的字库中取出对应行的数据,拼合成一个完整的、要发送给5片595的40位数据。
// 假设当前时间已存储在 hour, minute 变量中 uint8_t hour_tens = hour / 10; // 小时的十位 uint8_t hour_ones = hour % 10; // 小时的个位 uint8_t min_tens = minute / 10; // 分钟的十位 uint8_t min_ones = minute % 10; // 分钟的个位 // 在行扫描中断函数中 (假设当前扫描行号为 current_row) uint8_t display_buffer[5] = {0}; // 存储要发送的5字节数据 // 1. 先放入小时十位数字对应行的数据(可能需要移位对齐,因为屏幕左侧有空白) // 2. 与小时个位数字对应行的数据合并 // 3. 与冒号对应行的数据合并 // 4. 与分钟十位、个位数字对应行的数据合并 // 5. 最后,与当前行的阴极选择数据 row_select_data[current_row] 进行“或”操作, // 这样合成后的数据既包含了阳极点亮信息,也包含了阴极选通信息。 // 6. 将合成的 display_buffer 通过SPI或GPIO模拟时序发送给74HC595级联链。第三步:扫描显示中断最后,我们需要一个定时中断(比如每2ms一次),在中断服务函数里执行以下步骤:
- 关闭当前显示(消隐,防止拖影)。
- 根据
current_row变量,组合出第current_row行要显示的所有数据(如上一步所述)。 - 将组合好的40位数据发送到74HC595。
- 打开显示(锁存595数据到输出引脚)。
current_row加1,如果超过6(第7行),则归零。
这样,一个完整的、稳定的动态扫描显示驱动就完成了。主循环只需要负责更新要显示的时间变量,显示驱动会在中断里自动完成所有刷新工作,互不干扰。
3. 调试心得与注意事项
做完这个项目,有几个坑点值得分享,你以后自己做的时候可以避开:
- 电流与亮度:点阵屏所有LED全亮时,电流不小。务必计算一下总电流,确保你的电源(尤其是电池和USB口)能供得上。可以在限流电阻上做文章,调整亮度,也是在省电。
- 扫描频率:行扫描的频率不能太低,否则会闪烁;也不能太高,否则每行点亮时间太短,亮度不足。一般设置在50Hz~200Hz之间(即整体刷新率)。我的经验是,7行点阵,每行点亮1-2ms,整体刷新率在70-140Hz左右,效果不错。
- 消隐很重要:在切换行数据的时候,一定要先关闭显示(让所有LED灭掉),再发送新数据,最后再打开显示。这个操作被称为“消隐”,能有效防止切换瞬间的错乱亮灯(鬼影)。
- 字库对齐:定义字库数据和硬件连接顺序一定要对应上!哪个595是高位,哪个是低位;数据是先发送字节的高位还是低位(MSB/LSB);屏幕的物理列序和你的数据列序是否一致。这里最容易出错,建议写个简单的测试函数,让每一列LED依次点亮,来验证你的控制逻辑是否正确。
- 瑞萨FSP的使用:FSP配置虽然方便,但生成的代码结构需要花点时间熟悉。特别是中断回调函数的注册和使用,和标准库有些区别。多看看官方提供的示例工程,上手会快很多。
这个点阵屏时钟项目虽然不大,但涵盖了嵌入式开发的几个关键环节:MCU外设使用、驱动电路设计、显示算法、电源管理。把它吃透,你对嵌入式系统的理解会上一个台阶。最重要的是,当你看到自己编写的代码让一个个光点组成跳动的数字时,那种感觉,棒极了。
