从“能编译”到“能控住”:STM32F103 风扇控制器的 Keil 移植与实机调试
将基于 Astra 风格 OLED UI 的风扇控制器移植到 STM32F103C8T6 与 Keil MDK,并记录 PWM 极性、测速接线、10k 上拉和小比例进度条等实机调试结论。
这次 Fan Speed 工程的目标并不复杂:用 STM32F103C8T6 驱动一块 128x64 SSD1306 OLED,通过旋转编码器和按键调节风扇占空比,同时采集测速信号并在屏幕上显示转速。
真正花时间的部分,不是把几个外设初始化出来,而是把原有工程变成一个可以直接在 Keil 中使用的项目,并让代码最终服从真实硬件,而不是服从“常见接线”的想象。
硬件与功能H2#
核心硬件包括:
- STM32F103C8T6 最小系统板
- SSD1306 128x64 OLED,IIC 接口
- 旋转编码器与两个按键
- NMB-MAT 2415KL-04W-B86 四线风扇
- PB8 输出 PWM 控制信号
- PA6 采集风扇测速信号
- PWM 控制线上使用 10k 电阻上拉到 3.3V
界面保留了 Astra 风格的过渡动画和小屏交互,同时加入占空比、进度条和 RPM 显示。
从 CMake 工程迁移到 KeilH2#
原工程以 CMake 组织,但实际开发环境更适合 Keil MDK,因此我在 MDK-ARM 目录下补齐了完整工程:
FanController.uvprojx:Keil 工程配置STM32F103C8T6.sct:ARMCC 5 分散加载文件startup_stm32f103c8t6.s:启动文件platform_interrupts.c:必要的中断入口
验证工具链为 Keil MDK 5.33 和 ARM Compiler 5.06u7。最终工程能够完成全量编译和链接,并生成 FanController.hex 与 FanController.bin。
迁移中最容易被忽略的是 SysTick。工程即使能够链接,如果 SysTick_Handler 没有调用 HAL_IncTick(),HAL 的延时和超时逻辑仍会停住:
void SysTick_Handler(void)
{
HAL_IncTick();
}另一个问题来自 U8G2:当前源码中缺少 SSD1306 全缓冲 IIC 初始化函数 u8g2_Setup_ssd1306_i2c_128x64_noname_f。补齐对应 setup 后,显示部分才完成最终链接。
这次移植采用了比较稳妥的验证顺序:先编译业务代码,再分批编译 U8G2,最后做完整链接。这样可以把启动文件、库函数和业务代码的问题分开定位。
接线不能只看颜色H2#
最初按照常见四线风扇的颜色约定判断 PWM 和测速线,结果与实机现象不一致。最终根据台架测试确认,这一只 2415KL-04W-B86 的实际接法是:
这里的结论只针对本次使用的风扇和实测接线,不应直接套用到其他四线风扇。颜色可以作为线索,但最终应由型号资料、示波器波形和实际响应共同确认。
PWM 控制线使用开漏思路时,曾尝试省掉外部上拉,但该风扇的控制变得不可靠。最终保留 10k 电阻上拉到 3.3V。减少外围器件当然有吸引力,不过已经被实机证明必要的器件,不值得为了“更简洁”强行删除。
PWM 极性与线性调节H2#
实测后将 PWM 极性确定为非反相:
#define FAN_PWM_ACTIVE_LOW 0U这样界面中的百分比越高,输出占空比越高,风扇转速也随之上升。编码器每次调整 5%,在 0% 到 100% 之间形成清晰的离散档位,比过细的步进更适合这块小屏和旋钮的交互。
界面设置值直接映射到 PWM,占空比不再经过额外的非线性变换。这样做也让调试更直观:显示 40% 时,就能明确知道定时器正在输出约 40% 的命令。
测速信号与快速校验H2#
PA6 使用 TIM3_CH1 采集测速脉冲。代码默认风扇每转输出 2 个脉冲,因此转速和输入频率的关系为:
RPM = 频率(Hz) x 60 / 2例如 4000 RPM 对应的测速频率大约是 133 Hz。这个换算很适合现场快速判断:当 OLED 显示值、示波器频率和实际风声互相矛盾时,可以先用它检查测速链路是否合理。
为了区分真实转速和输入噪声,程序还保留了原始周期、滤波后周期以及被拒绝脉冲数等调试变量。相比只盯着最终 RPM,这些中间量更容易判断问题究竟来自 PWM 命令、接线、上拉还是测速干扰。
128x64 界面的边界问题H2#
小屏界面最容易在极端值上出错。进度条在 2% 等很小数值时,圆角矩形的最小宽度会导致横向溢出;RPM 文本靠近右边缘时,也可能留下看起来像竖线的残影。
最终的处理方式是:
- 进度条宽度很小时改用普通实心矩形,不强行绘制圆角
- 对填充宽度做边界保护
- RPM 文本按实际字符宽度右对齐
- 优先检查 0%、2%、5% 和 100% 等边界画面
这类问题在中间占空比下往往完全看不出来,因此 OLED 界面验证不能只停留在 50% 这样的“正常画面”。
最终结果H2#
最终工程完成了以下内容:
- 可直接使用的 Keil MDK 工程
- SSD1306 IIC 全缓冲显示
- 旋转编码器 5% 档位调节
- PB8 非反相 PWM 输出
- PA6 输入捕获测速
- 面向噪声排查的测速调试变量
- 适配极小百分比和右边缘文本的 OLED 界面
编译后的固件 ROM 约 16.77KB,RAM 约 2.50KB,适合 STM32F103C8T6 的资源规模。
这次调试最重要的经验,是让软件模型持续接受硬件反馈:先建立一个能编译、能链接的工程基线,再用真实波形和现象逐项修正接线、极性、滤波和界面。对于嵌入式项目,“常见做法”只能提供起点,实机结果才是最后的约束。

界面源代码参考:AstraThreshold/oled-ui-astra