☰
STM32数字时钟实战:从定时器到数码管动态扫描,打好单片机底子
2026/10/3 9:30:01 网站建设 项目流程

做嵌入式这一行,经常有人问我:新手学STM32,第一个项目做什么好?我的回答一直很固定——做个数字时钟。这玩意儿看着不起眼,甚至有点"复古",但它把一个单片机工程师日常要碰的东西全串起来了:GPIO读写、定时器中断、按键消抖、动态扫描显示,哪一样都是后面做复杂项目绕不开的底子。我自己就是用一块STM32F103C8T6最小系统板加几个共阴数码管,把这块地基打实的。

这个项目适合谁?我觉得是两类人。一类是刚接触单片机、想找个正经项目练手的学生;另一类是有点编程底子、但对STM32内部资源不熟、想系统补课的工程师。做完它,你对时钟树、预分频、自动重载、中断优先级这些概念就不再是"听过",而是亲手配过、亲手调过。这篇文章把我自己完整做过的方案、代码思路、调试踩坑都摊开讲,各位照着做就能复现一台自己的数字时钟。

1. 项目定位与硬件方案选型:先想清楚再做,别急着买器件

1.1 为什么用STM32而不是51单片机

用51单片机做时钟是很多教材里的经典实验,代码短、逻辑直白,但有个问题:51的资源太简单,很多东西不用配置,写起来像填空,练不到"系统级"的能力。STM32内部有时钟树、定时器、中断系统、复用功能这些复杂资源,光是让系统跑起来就需要理解时钟是怎么从外部晶振经过PLL倍频到72MHz的,中断优先级是怎么抢占的。这些恰恰是实际项目里天天用的东西。

如果你只是想要一个能走字的钟,直接花几块钱买DS1302模块加LCD1602,代码网上抄一份就能用。但如果想弄明白"单片机的每一秒从哪里来""显示为什么不会闪烁""按键按下去为什么会有毛刺",就必须从底层把这些机制自己摸一遍。这个项目的本质不是那个成品钟,而是完整走一遍从硬件到软件的设计流程,把各个外设挨个激活一遍。

1.2 物料清单与选型理由

我用的这套方案,总成本控制在30块钱以内。具体清单如下:

器件型号/规格作用参考价格
主控板STM32F103C8T6最小系统板核心控制10元左右
数码管4位共阴数码管 0.36英寸显示时分2-3元
按键轻触开关 6x6mm,3个调时、切换几毛一个
晶振8MHz(最小系统板自带)主时钟源已包含
电阻330Ω 1/4W,8个数码管段选限流1元
蜂鸣器无源蜂鸣器,可选整点报时1元
杜邦线母对母、公对母若干连接器件3元

主控选STM32F103C8T6,也就是俗称的"蓝丸"板子,原因很简单:便宜、资料多、引脚够用,而且它和市面上绝大多数STM32入门教程用的是同一颗芯片,遇到问题搜一下就有答案。四位数码管、三个按键、一颗可选蜂鸣器,加上杜邦线就能搭起来,不需要焊接,适合第一版快速验证。

数码管我建议选共阴的。原因有两个:一是F103的GPIO推挽输出对灌电流的承受能力比拉电流稍强,共阴接法段选输出高电平点亮,逻辑直观;二是共阴数码管的段码表网上到处都是,不容易踩坑。如果你手头只有共阳,代码里段码要全部取反,位选逻辑也要反着来,这个后面我会单独提醒。

限流电阻的计算也顺便说一下。F103的GPIO输出高电平约3.3V,红色LED数码管每段正向压降大约1.8V到2.0V,如果单段目标电流取8mA,限流电阻就是R=(3.3-2.0)/0.008≈162Ω,取标称150Ω或180Ω都行。实际用330Ω也完全没问题,单段电流大概4mA,8段全亮约32mA,在芯片GPIO总电流能力范围内,而且亮度足够,寿命也好。电阻大一点换来的是安全余量,这个取舍很值。

1.3 引脚分配与连接思路

引脚分配是很多新手容易忽略的环节。我第一版就是随手乱接,结果后面调试串口、下载程序时全冲突了,拆了重接好几回。这一版我的分配方案如下:

  • PA0-PA7接数码管段选(a-g、dp),推挽输出
  • PB0-PB3接四位数码管位选(公共端COM1-COM4),推挽输出
  • PB4、PB5接两个调时按键,输入上拉
  • PA8接功能切换按键,输入上拉
  • PB6接无源蜂鸣器,推挽输出

分配时有几个原则。第一,把PA13、PA14留出来,那两脚是SWD下载口,接了别的负载会影响调试器通信。第二,PA9、PA10留出来接USB转TTL串口,调试时打印时间变量非常有用,如果这两个脚被按键占了,后面想用串口就尴尬了。第三,PB4和PB5默认是JTAG引脚,要作为普通IO用,必须在代码里禁用JTAG或者做引脚重映射,这个很容易漏,漏了以后按键怎么按都没反应。

接线其实不复杂:段选那8根线从PA0到PA7,一一对应数码管的a到dp;位选4根线从PB0到PB3,分别接四个位的公共端;按键一端接GPIO,另一端接GND,靠内部上拉把空闲状态拉高,按下时读到低电平。蜂鸣器接PB6到电源之间,用NPN三极管驱动或者直接买带驱动的有源模块,别把蜂鸣器直接怼到GPIO上,电流可能不够。

2. 开发环境搭建:CubeMX配置时钟树与GPIO的实操步骤

2.1 工具链怎么选:HAL库还是标准库

做STM32开发,第一步就面临工具链选择。我的建议很明确:新项目用HAL库加CubeMX,理由有三个。第一,代码生成快,外设初始化图形化配置,鼠标点几下就出来了,省去大量手写寄存器的时间。第二,HAL库是目前ST官方主推的方向,生态资料最全,网上搜问题基本都是HAL风格的代码。第三,CubeMX生成的代码框架统一,工程结构清晰,对新手友好。

标准库不是不能用,很多老教程、老项目还在用,而且标准库代码确实更贴近寄存器底层,适合想深挖细节的人。但官方已经停止更新,新学的同学没必要再往旧技术栈上投入。我自己的做法是:用HAL库做工程,但遇到问题时会去翻参考手册和寄存器描述,搞清楚HAL封装背后到底操作了什么。这个习惯很重要,因为HAL封装的函数看起来很简单,一旦出问题,不理解底层就会束手无策。

工具版本上,我用的是Keil MDK 5.x加STM32CubeMX 6.x,再配上STM32CubeF1固件包。如果你不想用Keil,也可以用STM32CubeIDE,ST官方出品,免费、全集成,编译和调试都挺好用,唯一的问题是有些老电脑跑起来有点卡。两者流程差不多,下面以Keil为例讲。

2.2 CubeMX配置关键步骤

用CubeMX建工程,核心就三步:选芯片、配时钟、配引脚。

第一步,新建工程,在芯片搜索框输入STM32F103C8,选择LQFP48封装的C8T6。第二步,配置RCC,在System Core里把HSE设为Crystal/Ceramic Resonator,因为蓝丸板上有8MHz外部晶振;LSE也就是32.768kHz那个晶振,最小系统板上通常没焊,直接不使能,省得配置出问题。第三步,配置时钟树。把HCLK输入72MHz,CubeMX会自动帮你算好PLL配置,一般是HSE 8MHz经过PLL×9得到72MHz。这里要注意一个点:APB1总线时钟是36MHz,但APB1上的定时器时钟会自动×2变成72MHz,所以后面配TIM3时,定时器输入时钟是72MHz,不是36MHz,这个算错会导致定时时间翻倍。

然后是GPIO配置。PA0-PA7设为GPIO_Output,速度选High,因为动态扫描数码管需要比较快的翻转速度,选Low可能产生拖影。PB0-PB3同样设为GPIO_Output。PB4、PB5、PA8设为GPIO_Input,下拉或上拉选择Pull-up,按键接GND所以空闲时要拉高。这里有个坑:PB4、PB5默认复用为JTAG的NJTRST和JTDO,CubeMX里虽然能配成普通IO,但生成代码后往往还需要在System Core的SYS里把Debug设为Serial Wire,否则这两脚可能不听话。

最后配置TIM3。在Timers里选中TIM3,Clock Source选Internal Clock,预分频PSC设为7199,计数模式选Up,自动重载ARR设为9999,然后使能TIM3中断。此时生成的定时器中断频率刚好是1Hz,配合中断回调就能作为秒脉冲。生成代码前,在Project Manager里把Toolchain选为MDK-ARM,其他默认就行。

2.3 工程结构规划:小项目也要养成模块化习惯

很多新手做小项目喜欢把所有代码都塞进main.c,一个文件几百行,当时觉得爽,过两天自己都不想看。这个习惯必须从一开始就改掉。数字时钟虽小,但我建议按功能拆成模块:

  • main.c:主循环逻辑,负责调度
  • bsp_timer.c / bsp_timer.h:定时器初始化和中断回调
  • bsp_display.c / bsp_display.h:数码管驱动,段码表、扫描显示
  • bsp_key.c / bsp_key.h:按键扫描、消抖、状态机
  • bsp_buzzer.c / bsp_buzzer.h:蜂鸣器控制(可选)

为什么要模块化?第一,可读性好,每个文件的职责单一,找问题直接定位到对应文件。第二,可复用性强,以后做温湿度计、做闹钟,这些模块拷过去改改就能用。第三,方便调试,比如显示有问题,单独测bsp_display,不需要在主逻辑里翻半天。我后来做更复杂的项目时,深刻体会到前期这点模块化工作省下的时间,远超当时多敲的那几行代码。

3. 核心代码实现:定时器、数码管、按键逐个击破

3.1 时间基准的产生:1秒到底从哪来

数字时钟的核心是准确的1秒时基。STM32里产生周期中断最常用的方式就是定时器更新中断。以TIM3为例,前面CubeMX已经配置了PSC=7199、ARR=9999,这个数字不是随便拍的,而是算出来的:

定时器输入时钟是72MHz,预分频后计数频率为72MHz/(PSC+1)=72MHz/7200=10kHz,也就是每计一个数用0.1ms。自动重载值ARR=9999,意味着计数器从0数到9999共10000个数,产生一次更新事件。所以更新事件频率就是10kHz/10000=1Hz。综合公式是f_update = 72MHz / ((7199+1)*(9999+1)) = 1Hz。以后你想改成1kHz或者其他频率,套这个公式就行。

中断回调函数这样写:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM3) { TimeUpdate(); } }

TimeUpdate负责时间进位逻辑:

void TimeUpdate(void) { g_time.second++; if (g_time.second >= 60) { g_time.second = 0; g_time.minute++; if (g_time.minute >= 60) { g_time.minute = 0; g_time.hour++; if (g_time.hour >= 24) { g_time.hour = 0; } } } }

进位顺序很重要,必须从低位到高位逐级处理。这里有个新手常犯的错误:先把second清零,再检查second是否达到60,结果永远进不了位。正确逻辑是先加1,再判断是否到达上限,到达了才清零并往上一级进位。

有一点我必须强调:中断服务函数里只做最轻量的事情。这个例子里只有几个变量加法和比较,没问题。但如果你把数码管扫描、按键处理都塞进中断里,中断就会拖得特别长,轻则影响其他中断响应,重则整个系统卡死。时间更新放中断,显示和按键放主循环,这是典型的"前后台系统"结构,也是很多嵌入式系统的基础模型。

3.2 数码管动态扫描显示:从段码表到消影

四位数码管如果每个位都独立控制需要很多引脚,所以工程上基本都用动态扫描:所有位的段选引脚并联,位选引脚分别控制,同一时刻只点亮一位,快速轮流点亮四个位,利用人眼的视觉暂留效应,看起来就像四位数同时亮着。

段码表是数码管驱动的第一个关键点。共阴数码管中,a段接bit0、b段接bit1,一直到g段接bit6、dp接bit7。显示数字0时,a、b、c、d、e、f亮,g不亮,对应二进制0011 1111,也就是0x3F。把0到9全部算出来:

const uint8_t seg_code[10] = { 0x3F, // 0 0x06, // 1 0x5B, // 2 0x4F, // 3 0x66, // 4 0x6D, // 5 0x7D, // 6 0x07, // 7 0x7F, // 8 0x6F, // 9 };

这个表不用死记,自己推一遍比背十遍都管用。如果是共阳数码管,把每个值按位取反就行,即0xC0、0xF9、0xA4、0xB0、0x99、0x92、0x82、0xF8、0x80、0x90。

扫描函数我的写法是这样的:

uint8_t disp_buf[4]; // 待显示的四位数字 void Display_Scan(void) { static uint8_t pos = 0; uint8_t com_table[4] = {0x01, 0x02, 0x04, 0x08}; // 第一步:关闭所有位选,消影 GPIOB->ODR &= ~0x0F; // 第二步:送当前位的段码 GPIOA->ODR = (GPIOA->ODR & 0xFF00) | seg_code[disp_buf[pos]]; // 第三步:打开当前位 GPIOB->ODR |= com_table[pos]; pos++; if (pos >= 4) { pos = 0; } }

这个函数在主循环里反复调用,每次刷新一位,四位一轮。为了稳定不闪烁,每个位停留大约2ms,一轮8ms左右,刷新率125Hz,远高于人眼闪烁感知阈值。如果你用delay函数每扫描一位延时2ms,注意延时函数本身也会占用CPU,别在中断里调用。

关于消影,我要多说几句。顺序必须是"先关所有位选,再送段码,再打开当前位",这个顺序错了就会出现"鬼影"。鬼影的成因是:切换位的瞬间,旧的段码数据还没撤掉,新位就已经导通,于是旧数字残影投射到了新位上。先全部熄灭,等于把显示内容断开,再换数据,再导通新位,就彻底避免了这个现象。有时候你看到下一轮扫描有个淡影,多半就是消影没做干净。

3.3 按键扫描与软件消抖:按下一次不能当三次

按键的物理特性决定了它必须消抖。轻触开关内部是金属簧片,按下瞬间会来回弹跳,电平在高低之间震荡,持续大约10到20ms。如果不处理,一次按键会被识别成好几次。消抖方案很多,最基础的就是延时重读:检测到按键为低电平后,延时20ms,再读一次,如果还是低电平,才认为真的按下了。

#define KEY_MODE_PIN GPIO_PIN_8 #define KEY_ADD_PIN GPIO_PIN_4 #define KEY_SUB_PIN GPIO_PIN_5 uint8_t Key_Scan(Time_t *t) { uint8_t ret = 0; if (HAL_GPIO_ReadPin(GPIOA, KEY_MODE_PIN) == GPIO_PIN_RESET) { HAL_Delay(20); if (HAL_GPIO_ReadPin(GPIOA, KEY_MODE_PIN) == GPIO_PIN_RESET) { ret |= KEY_MODE_PRESSED; } } // 其他按键同理 return ret; }

比延时消抖更高级一点的是定时器扫描消抖:每隔10ms扫描一次按键,连续两次读到的状态相同才认为状态稳定,这样不阻塞CPU,还能顺便做按键释放检测。数字时钟这个项目用延时消抖就够了,但你要知道有更好的方案,做复杂系统时按键多了,阻塞式消抖会拖垮整个流程。

按键动作只有两类:按下沿和释放沿。处理功能建议检测"按下沿",也就是从空闲变成按下的那一刻,而不是检测电平。原因很简单:如果检测到低电平就执行加一,你按住不放,它会一直加;如果只检测按下沿,每按一次只加一,按住也不会重复触发。

调时逻辑我建议用一个简单的状态机,用枚举定义:

typedef enum { MODE_RUN, // 正常运行 MODE_SET_MIN, // 调分钟 MODE_SET_HOUR // 调小时 } ClockMode_t;

按键一负责在三个模式间循环切换,按键二在调时模式下负责加一。这个状态机虽然简单,但它是以后做菜单系统的雏形,值得体会一下"用状态而不是用标志位"的思路。

3.4 主循环整体逻辑:前后台系统的骨架

主循环是整个程序的调度中心,我的写法大概是:

int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM3_Init(); g_time.hour = 12; g_time.minute = 0; g_time.second = 0; HAL_TIM_Base_Start_IT(&htim3); while (1) { // 刷新显示 Display_Update(&g_time); Display_Scan(); // 扫描按键 uint8_t key = Key_Scan(&g_time); HandleKey(key, &g_time); // 可选的整点报时逻辑 if (g_time.second == 0 && g_time.minute == 0) { Buzzer_Beep(1); } } }

这个结构就是典型的前后台系统:定时器中断是前台,负责维护最严格的时间基准;while循环是后台,负责显示、按键这些对时序要求不高的任务。很多实时操作系统(RTOS)的本质也是在这套模型上加了一层调度器。你把数字时钟这个项目做透,其实已经把嵌入式系统最底层的架构理念掌握了。

Display_Update负责把当前时间拆成四位数字,比如12点34分,hour/10=1、hour%10=2、minute/10=3、minute%10=4,分别存入disp_buf。调时模式下,可以让当前正在调的那一位闪烁,实现方式是在扫描时每隔一定周期把该位置为不显示,视觉上看起来就是闪动。这个功能虽然小,但很能体现你对显示逻辑的掌控力。

4. 调试实录:显示闪烁、走时不准、按键乱跳的排查过程

4.1 数码管鬼影与闪烁问题

第一版上电测试时,数码管显示出来的数字带着明显的残影,数字切换时还能看到"拖尾"。排查思路是从显示顺序入手。我先确认了段码表没错、引脚对应没错,最后发现就是扫描顺序的问题:旧位还没有熄灭,新位的段码已经送过去了,结果旧位的数字残留到了新位上。解决办法就是前面说的三步走:先全部熄灭,再送段码,再打开当前位。

闪烁问题的排查方向和鬼影不同。闪烁通常是因为刷新率太低,也就是每个位停留时间太长。我见过有人把单位置1延时写了50ms,四位一轮就是200ms,刷新率只有5Hz,肉眼看到的就是明显的"一格一格跳"。把每位置延时改到2ms左右,刷新率125Hz,问题立刻消失。另外还要检查一件事:是否所有延时都放在中断或主循环里被其他长任务阻塞了。如果主循环里有个延时特别久的函数,扫描函数长时间得不到调用,闪烁就必然发生。

4.2 走时不准的根源与软件校准

定时器方案做到后面一定会遇到走时偏差问题。我第一次测试,连续跑了两小时,比手机时间快了约2秒,折算下来一天要快24秒左右,这作为桌面时钟绝对不合格。原因主要在于主时钟源的精度。

STM32F103C8T6蓝丸板上的8MHz晶振大多是普通贴片晶振,精度通常在几十ppm量级,而且部分板子没有配负载电容,实际谐振频率会偏离标称值。另外一个隐藏问题:掉电后时间归零,重新上电需要手动调时。这两个问题叠加,让我决定做两步处理。

第一是软件校准。校准思路是:先实测走时偏差,比如实测1小时快了1.5秒,也就是快0.025秒/分钟。然后修改时间更新逻辑,每N次中断跳过或增加一个tick,让时间平均下来变准。具体的实现可以是维护一个补偿计数器,每40分钟额外少算1秒,或者每天在某个固定时刻回拨固定秒数。这种方法能把误差压到每天1到2秒以内,对于非精密场景足够了。

第二是更彻底的方案:换用外部RTC芯片,比如DS3231。DS3231内置温补晶振,年误差能做到一两分钟级别,还带电池备份,断电后时间不丢。如果你的目标是做一个真正能长期使用的时钟,DS3231是必须的投资。这个升级我放在下一节详细讲。

4.3 按键误触发与连加问题

按键的问题最常见的是"按一次加了好几次"。排查后发现原因有两个:一是没用消抖,金属弹片的毛刺被当成了多次按下;二是按下处理写成了电平触发,按住不放就持续加一。第一次做的朋友大概率两个坑都会踩。

消抖的具体做法前面已经给了代码,这里补充一个经验:消抖延时不是越大越好。20ms是常见值,但如果用的按键品质差,可能需要30ms;反过来延时太长,手速快时会感觉按键"反应不过来"。另外,处理完一次按键后,要等按键释放才能处理下一次按下。实现方式是在状态机里加一个等待释放的状态,否则快速连按会被消抖算法吞掉。

调试按键还有一个方法:用串口打印。按下按键时打印当前时间和按键状态,看起来是不是符合预期。我当时就是用这个方法发现原来是PB4引脚没有正确禁用JTAG,导致按键状态一直读不到。这种问题不看串口输出,光盯着数码管看,很难定位。

4.4 常见问题速查表

现象可能原因排查与解决
程序烧录失败BOOT0跳线不对、SWD引脚被占用BOOT0接GND;检查PA13/PA14没有外接负载
数码管全亮且乱码段码表错误、引脚接反依次显示单个数字0-9,逐个位验证
显示闪烁扫描刷新率太低、主循环被阻塞每位置2ms左右;检查是否有长延时阻塞扫描
时间不走定时器未启动、中断未使能检查HAL_TIM_Base_Start_IT;确认NVIC已使能
走时偏差大晶振精度差、使用HSI内部时钟确认使用HSE;软件校准;换DS3231
按键没反应上拉没配、JTAG引脚冲突确认输入上拉;PB4/PB5需配置Serial Wire调试模式
按键连加没消抖、电平触发写成持续触发加20ms消抖;用按下沿触发;处理完等释放
断电时间丢失定时器方案无掉电保持换DS3231或加后备电池

4.5 串口和示波器是调试的法宝

做这类项目,强烈建议从一开始就预留串口。用USB转TTL模块接PA9、PA10,通过printf打印时间变量、按键事件,调试效率翻倍。CubeMX里重定向printf的细节不多,核心是把fputc重定向到串口,网上教程很多。我习惯在每次按键操作后打印一行日志,格式大概是"mode=1, hour=12, min=05, sec=00",这样每次改动都有迹可循。

如果有示波器或者逻辑分析仪,可以抓一下扫描输出波形,你会直观看到动态扫描的时序。没有也不要紧,我教你一个笨办法:把数码管扫描速度人为调慢,比如每个位延时500ms,你会看到一个位一个位地点亮,顺序对不对、段码对不对一目了然。这个"慢动作调试法"在嵌入式调试里非常实用,比对着代码猜快多了。

5. 从简易单机时钟到智能桌面设备:三个靠谱的升级方向

5.1 升级外部RTC:彻底解决走时和掉电问题

定时器方案最大的两个痛点,走时精度和掉电保存,都是外部RTC芯片的主场。DS3231是性价比最高的选择,内置温补晶振和电池座,走时精度年误差在1到2分钟,断电能靠纽扣电池维持好几年。硬件连接就是I2C总线,SCL接PB6、SDA接PB7,注意DS3231是5V逻辑的模块时可能要加电平转换或者选3.3V版本,否则I2C通信会不稳定。

软件上改用HAL库的I2C接口,初始化I2C外设后,按DS3231的寄存器映射读写秒、分、时、日、月、年。这个升级会带你接触I2C协议、寄存器读写、BCD编码转换,又是一套新知识。调时逻辑也要跟着改:以前是直接修改g_time变量,现在需要写回DS3231寄存器,但整体的应用层逻辑不用大动,这正是模块化带来的好处。

5.2 升级显示方案:从数码管到OLED

数码管的优点是直观、便宜,缺点也很明显:信息容量小,只能显示数字,要做菜单、图标非常吃力。0.96寸OLED屏幕,I2C或SPI接口,128x64分辨率,能显示中文、曲线、甚至简单的动画,做出来的效果立刻从"实验室"变成"产品级"。

换OLED后,时间信息以文本形式显示,用户界面可以做菜单,比如第一屏显示时间,第二屏显示温度,第三屏显示设置。这个升级会带出几个新问题:字库怎么取模、菜单状态机怎么设计、I2C多设备怎么共存。每一个问题都是嵌入式开发的高频技能。我做第三版时把OLED、DS3231、DHT22温湿度传感器整合到一起,做成了一个桌面信息屏,功能比数字时钟丰富得多,但核心骨架还是当初那个数字时钟打下的底子。

5.3 加传感器与低功耗:把时钟变成环境监测终端

加了DS3231和OLED之后,这个项目已经具备小型产品的雏形了。继续往里加东西的方向很多,我列几个我觉得最顺手的:

  • 温度采集:DS18B20单总线或者AHT20 I2C,在OLED上显示实时温湿度
  • 光线传感器:光敏电阻加ADC采集环境亮度,自动调节OLED亮度或者数码管亮度
  • 整点报时:无源蜂鸣器播放不同频率的组合音,区分整点和半点
  • 低功耗改造:STM32进入STOP模式,由RTC闹钟每秒钟唤醒一次刷新显示,电池供电能用很久

这些扩展的共性是从"能用"走向"好用",每一步都涉及新的外设、新的协议、新的设计权衡。我一直觉得,嵌入式学习最好的路径不是刷教程,而是把一个项目往深了做,每加一个功能就会遇到新的问题,每解决一个问题,你的能力就实实在在涨一截。数字时钟只是个起点,但它是个非常好的起点。

最后再分享一点个人体会。这个项目我自己前后做过三版:第一版是定时器加共阴数码管,按键没有消抖,按一下跟抽风似的,串口也没预留,调试全靠猜;第二版加了模块化、串口打印和软件消抖,稳定了很多,但每天走时还是偏快,断电还得重新调;第三版换成DS3231配OLED,才算真正敢把它放在桌面上当一个能用的钟。每一步都是在前一个版本的坑上长出来的,这大概就是嵌入式学习的常态。做数字时钟不是目的,借它把STM32的底子打牢才是。如果你也想练手,就从这篇文章的方案开始,按自己的需求改,做完之后你会发现,再看那些复杂项目,心里有底多了。

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

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

立即咨询