1. 两天的定位从“会点灯”走向“会控制”嵌入式学习进入第六天和第七天说实话有点兴奋也有点慌。前五天基本把 GPIO 点灯、按键输入、外部中断这套基础操作跑通了但心里很清楚这些离“嵌入式”真正的样子还差得远。我这六天第七天的目标很明确Day 6 搞定定时器输出 PWM做一个呼吸灯Day 7 搞定串口通信让开发板和电脑之间真正“对上话”。两天下来最大的感受是外设不难难的是理解“芯片内部时钟怎么走、中断怎么来、数据怎么流转”这一整套协作逻辑。这两天适合谁一个是和我一样从零开始学嵌入式、已经能点灯但不知道该往哪走的人另一个是单纯只会调用库函数、想回头补寄存器底层逻辑的人。我会把寄存器配置、计算推导、踩坑排查全都写出来包括很多教程里不会提的细节。看完你至少能独立完成一个 PWM 呼吸灯和一个串口命令行控制的小实验而且每一步都明白自己在干什么。先说结论第六天第七天是整个入门阶段的分水岭。前五天你操作的是“引脚”后两天你开始操作“时间”和“数据”。这两个东西恰好是嵌入式系统最核心的抽象概念。2. Day 6核心实战定时器与PWM呼吸灯2.1 定时器参数推导先算清楚再写代码我用的主控是一颗常见的 32 位 ARM 内核 MCU外部晶振 8MHz通过 PLL 把主频配置到 72MHz。这颗芯片里定时器 2 挂在 APB1 总线上这里有个特别容易踩坑的点APB1 的外设时钟被预分频成了 36MHz但定时器的时钟源会再翻倍所以 TIM2 的输入时钟仍然是 72MHz。如果你拿着“外设时钟就是总线时钟”的思维去配置计算出来的 PWM 频率会整整偏高一倍。我们要输出 1kHz 的 PWM周期 1ms。定时器的计数时钟由两部分决定PSC预分频器对 72MHz 进行分频。PSC 71 表示 72 分频计数时钟变成 72MHz / 72 1MHz也就是每计数一次耗时 1us。ARR自动重装载寄存器决定计数多少个脉冲后产生更新事件。ARR 999 表示从 0 数到 999 共 1000 次耗时 1000us 1ms。所以 PWM 频率公式是f fclk / ((PSC1) * (ARR1))。代入就是 72MHz / (72 * 1000) 1kHz。之后占空比由 CCR 这个比较寄存器控制。计数器的值从 0 增长到 ARR 的过程中会不断和 CCR 比较小于 CCR 时输出有效电平大于等于 CCR 时输出无效电平。CCR 500 就是 50% 占空比。想调占空比只需要改 CCR 的值这就是呼吸灯能实现的原理基础。2.2 寄存器配置步骤让 GPIO 输出一路 PWM直接给代码基于寄存器操作不依赖库封装。每一行的作用我会在注释里讲清楚// 1. 开启 TIM2 时钟GPIOB 时钟以及复用功能时钟 RCC-APB1ENR | RCC_APB1ENR_TIM2EN; RCC-APB2ENR | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN; // 2. 将 PB0 配置为复用推挽输出 // CNF[1:0] 10 表示复用推挽MODE[1:0] 10 表示输出速度 2MHz GPIOB-CRL ~(GPIO_CRL_CNF0 | GPIO_CRL_MODE0); GPIOB-CRL | (GPIO_CRL_CNF0_1 | GPIO_CRL_MODE0_1); // 3. 配置 TIM2 通道3PB0 对应 TIM2_CH3 TIM2-PSC 71; // 72分频计数时钟 1MHz TIM2-ARR 999; // 自动重载值得到 1kHz PWM TIM2-CCR3 500; // 初始占空比 50% // 4. 设置 PWM 模式1向上计数时CNT CCR 输出有效电平 TIM2-CCMR2 | TIM_CCMR2_OC3M_1 | TIM_CCMR2_OC3M_2; TIM2-CCMR2 | TIM_CCMR2_OC3PE; // 预装载使能防止更新瞬间跳变 // 5. 使能通道输出 TIM2-CCER | TIM_CCER_CC3E; // 6. 启动定时器 TIM2-CR1 | TIM_CR1_CEN;几个细节必须强调引脚复用映射一定要查芯片手册不是随便一个引脚都能输出 PWM。PB0 在这颗芯片上刚好能复用为 TIM2_CH3换个芯片这可能是别的外设。配置 GPIO 时如果忘了设成“复用推挽”引脚就出不了 PWM 波形这是最常见的问题。预装载位 OC3PE 要开不然更新 CCR 的瞬间输出可能跳一帧毛刺呼吸效果会闪一下。2.3 呼吸灯实现与效果调优配置好 PWM 之后呼吸灯的逻辑就简单了在循环里让 CCR 从 0 逐渐加到 999再从 999 逐渐减到 0循环往复。关键在于刷新速度。如果刷新太慢能看到明显台阶太快又看不到亮度层次。我实测下来每 2ms 刷新一个步进1000 步一个完整周期大概 2 秒视觉上非常柔和。uint16_t duty 0; uint8_t direction 1; while (1) { TIM2-CCR3 duty; if (direction) { duty; if (duty 999) direction 0; } else { duty--; if (duty 0) direction 1; } delay_ms(2); }调试时建议先用固定占空比验证 PWM 是否正常比如直接把 CCR 设成 500观察 LED 是不是半亮。如果不亮优先查波形。我用逻辑分析仪看过1kHz 的方波很干净。没有示波器或者逻辑分析仪的可以用一个笨办法把占空比从 0 缓慢调到 100如果中间 LED 亮度有变化说明 PWM 输出基本正常。我踩过的一个典型坑是代码逻辑没问题但 LED 一直暗亮而不是呼吸。后来发现 GPIO 的翻转速度配置太低波形上升沿特别缓。把 MODE 位从 2MHz 改成 50MHz 之后效果马上正常了。低速模式下引脚驱动能力跟不上 PWM 切换这个问题在低主频外设上尤其明显。3. Day 7核心实战串口通信与命令行控制3.1 串口为什么是嵌入式的“调试之眼”到了 Day 7学习主角变成了串口。这东西做嵌入式必须掌握它相当于开发板的“嘴巴和耳朵”把调试信息发出来把控制指令收进去。PWM 这种外设还能靠 LED 亮度判断但等你以后接传感器、调算法没有串口打印日志几乎是寸步难行。Day 7 要达到三个目标能单向发送数据把一串字符发给电脑。重定向 printf让所有调试信息直接用 printf 打印。能接收电脑发来的指令实现“串口命令行控制”。这三个目标从易到难最后做出来的效果非常直观电脑上发一个 led1板载 LED 打开发 led0 就关闭发 help 就打印支持的命令列表。整个过程比单纯点灯有成就感多了。3.2 初始化串口波特率、GPIO复用、收发标志串口通信的核心参数是波特率也就是每秒传输多少个符号。我用的是 USART1挂在 APB2 总线上APB2 时钟为 72MHz。波特率寄存器 BRR 的配置方式是BRR 值约等于外设时钟除以波特率。比如 115200 波特率BRR 72000000 / 115200 625写成十六进制就是 0x0271。这里需要科普一下UART 一帧数据由起始位、数据位、停止位组成。一发一收之间的物理电平默认空闲是高电平发送第一个比特时拉低产生起始位然后再发 8 个数据位最后拉高一个停止位。但芯片内部为了保证采样精度把每个比特再细分成 16 个采样点所以时钟分频时要额外除以 16。换算到最后BRR 寄存器直接写入的值就是“时钟频率 / 波特率”。初始化代码// 1. 开启 GPIOA 时钟和 USART1 时钟 RCC-APB2ENR | RCC_APB2ENR_IOPAEN | RCC_APB2ENR_USART1EN; // 2. 配置 PA9 为 TX 复用推挽输出PA10 为 RX 浮空输入 // PA9 对应 USART1_TXPA10 对应 USART1_RX GPIOA-CRH ~(GPIO_CRH_CNF9 | GPIO_CRH_MODE9 | GPIO_CRH_CNF10 | GPIO_CRH_MODE10); GPIOA-CRH | (GPIO_CRH_CNF9_1 | GPIO_CRH_MODE9_1); // TX: 复用推挽 GPIOA-CRH | GPIO_CRH_CNF10_0; // RX: 浮空输入 // 3. 配置串口参数 USART1-BRR 72000000 / 115200; // 波特率 115200 USART1-CR1 USART_CR1_UE | USART_CR1_TE | USART_CR1_RE; // 使能、发送、接收 USART1-CR1 | USART_CR1_RXNEIE; // 接收中断使能 // 4. 开启 NVIC 中断 NVIC_EnableIRQ(USART1_IRQn); NVIC_SetPriority(USART1_IRQn, 1);TX 引脚必须配置成复用推挽RX 引脚一般用浮空输入。这个组合我在多种芯片上验证过属于通用配置。3.3 重定向 printf让代码自己“说人话”串口能发数据之后我们想用 printf 而不是自己手动写一个字节函数。这需要做“重定向”把 C 标准库 printf 的输出导向串口。int fputc(int ch, FILE *f) { while (!(USART1-SR USART_SR_TXE)); // 等待发送寄存器空 USART1-DR ch; return ch; }在工程里勾选使用 MicroLIB 之后printf 就会调用这个 fputc从而实现向串口打印。有一个经典坑必须提醒如果你不重定向 fputcprintf 默认走的是半主机模式也就是通过调试器输出。在嵌入式环境下程序执行到 printf 时会直接卡死或者完全不输出。很多刚入门的朋友折腾半天其实问题就出在这里。还有一个细节是发送一个字符串时最后一个字符发出后别立刻关闭或复位串口要等待发送完成标志 TC。不然最后一帧数据可能被截断。断点调试时特别注意在while (!(USART1-SR USART_SR_TC));这个位置停住如果还继续看后面的代码最后一个字符永远发不出去。3.4 中断接收与简易命令行解析接收比发送复杂一些因为数据什么时候来是不确定的。程序不可能一直死等所以要用中断收到一个字节就进一次中断把数据存到缓冲区然后回到主循环继续干活。volatile char rx_buf[64]; volatile uint8_t rx_len 0; volatile uint8_t cmd_ready 0; void USART1_IRQHandler(void) { if (USART1-SR USART_SR_RXNE) { char ch USART1-DR; if (ch \r || ch \n) { rx_buf[rx_len] \0; cmd_ready 1; rx_len 0; } else if (rx_len sizeof(rx_buf) - 1) { rx_buf[rx_len] ch; } } }主循环里检测 cmd_ready如果为 1 就解析 rx_buf然后清零继续while (1) { if (cmd_ready) { cmd_ready 0; if (strcmp((char *)rx_buf, led1) 0) { GPIOB-BSRR GPIO_BSRR_BS1; // 打开 LED printf(LED ON\r\n); } else if (strcmp((char *)rx_buf, led0) 0) { GPIOB-BSRR GPIO_BSRR_BR1; // 关闭 LED printf(LED OFF\r\n); } else if (strcmp((char *)rx_buf, help) 0) { printf(Commands: led1, led0, help\r\n); } else { printf(Unknown: %s\r\n, rx_buf); } } }中断里只做“存数据”这一件事解析和响应都放到主循环这是一个非常重要的设计习惯。如果中断里做太多事后面再来数据时可能来不及处理导致丢数据。我现在所有串口接收都是这个套路中断存、主循环处理复杂系统再加一个环形缓冲。字符串命令对简单实验够用但想做得更稳推荐环形缓冲能解决 PC 发来超长数据时缓冲区被覆盖的问题。串口收到的数据一定要记得清 RXNE 标志。这个标志的清除方式很特别先读 SR再读 DR硬件自动清。我见过有人读完 DR 之后以为要写 SR 手动清零反而把状态寄存器改坏了。中断服务函数里读取USART1-SR和USART1-DR这个顺序已经满足了清除要求。4. 两天踩坑实录与调试心法4.1 遇到过的问题和排查思路现象可能原因排查方法PWM 完全没有输出GPIO 不是复用模式定时器时钟没开检查寄存器配置量引脚电平PWM 频率不对忽略了定时器时钟翻倍机制用逻辑分析仪测实际频率反推时钟树呼吸灯出现闪烁CCR 更新瞬间产生毛刺开启预装载位 OC3PEprintf 程序卡死没有重定向 fputc 或没开 MicroLIB勾选 MicroLIB确认 fputc 已实现串口输出乱码波特率不匹配或者主频和代码不一致发 0x55 看波形确认 TX 是否正常方波能发不能收RX 引脚没配置输入中断没使能RX 和 TX 接反查 GPIO、中断交换两根线测试接收数据偶发丢失中断服务里处理太多事RXNE 未及时清拔掉干扰源中断里只存数据屏幕显示问题首字符丢失发送后没有等待 TXE 就写入下一个字符每次写 DR 前轮询 TXE 标志这个表格里的每一项基本上都是我这两天真实遇到过的问题。最折磨人的是那种“代码看起来完全没问题”的 bug十个里有八个出在时钟配置或 GPIO 复用上。所以遇到问题第一步永远是查时钟树和引脚复用。4.2 几个值得刻进习惯的调试细节第一个习惯先把现象拆小。PWM 不输出先固定 CCR 成 500 看 LED 有没有半亮串口收不到先发一个固定字符 0x55。很多问题在拆细之后立刻暴露。第二个习惯优先怀疑硬件层配置。软件逻辑写错通常很快能发现但“引脚复用没配对”“时钟分频算错”这类问题靠盯代码很难看出来。这时候一份芯片参考手册比调试器有用。第三个习惯用好串口本身来调试。Day 7 实现了 printf 重定向后我会在关键状态切换处打日志比如“进入 PWM 模式”“收到指令 led1”。这些日志比断点好用因为它不打断实时性能看到系统真实的运行节奏。第四个习惯记笔记。我这两天最值的操作是把两个实验的配置步骤、计算过程、踩坑原因都记在本地文档里。原因只有一个嵌入式外设太多两个月后你肯定忘光细节届时这份笔记可比任何教程都靠谱。4.3 学习节奏方面的个人建议Day 6 和 Day 7 连着做其实这个节奏非常合适。PWM 用到了定时器的底层机制串口用到了中断和状态机两者都是嵌入式开发的“骨架知识”。但我不建议你一天之内硬塞这两块。我是分两天做的Day 6 专注定时器和 PWMDay 7 专注串口。中间隔了一晚第二天再看定时器配置很多东西突然通透了。学嵌入式不是比速度是比理解深度。如果你卡在某个外设好几天还没进展我的建议是先把代码放一边重新画一遍时钟树把外设时钟怎么来的、引脚怎么复用的写清楚。八成问题能在画图过程中自己解决。5. Day 8怎么继续往下走5.1 先别急着上 RTOS把状态机和缓冲搞扎实很多教程学完串口就切入 RTOS我个人的建议是再等等。定时器和串口这两天的实践本质上是在训练三种思维时间基准思维、事件驱动思维、数据缓冲思维。这些是比某个具体外设更底层的东西。比如 Day 7 的串口中断接收如果你把“中断里存数据主循环处理”这套逻辑吃透了后面学 RTOS 的消息队列就会觉得特别眼熟。学嵌入式最忌讳的是“芯片换了一颗又一颗但遇到复杂问题还是不知道怎么排查”。根基不牢后面全飘。5.2 可选的三个延伸方向如果你想趁热打铁接下来有三个方向性价比很高一个是 ADC。用串口打印传感器电压值把模拟世界和数字世界串起来这个反馈感很强。一个是 I2C 或 SPI。接一个外部传感器模块比如温湿度传感器理解器件寻址、寄存器和时序。这是嵌入式连接外部世界的主流方式。一个是看门狗和低功耗。这是实战产品里绕不开的话题能把前两天学的定时器、中断用起来同时理解系统可靠性保障。我自己 Day 8 打算做 ADC 加串口打印把电位器电压变化实时发到电脑上。其实后面想学的东西很多但每次只往前推进一小步理解就扎实一分。这两天最大的收获是终于能说一句“我手里的开发板能跟我对话了”。从一个 LED 的亮灭到一串串跳动的电平波形再到电脑终端里的文字反馈——整个链条通起来的那一刻你会觉得前几天的枯燥配置都值了。