基于STM32F103C8T6的简易PWM信号发生器设计与实现
基于STM32F103C8T6的简易PWM信号发生器设计与实现
前言
在嵌入式开发中,PWM(Pulse Width Modulation,脉宽调制)是一项应用极为广泛的技术——从电机调速、LED调光到音频放大、无线通信,处处都有它的身影。作为STM32入门学习的经典课题,掌握PWM的生成与控制是深入嵌入式世界的必经之路。
本文记录一个基于STM32F103C8T6的简易PWM信号发生器项目,支持频率和占空比双维度独立调节,配合74HC595驱动的数码管实现参数可视化显示。文章将从硬件设计、软件实现到调试过程完整呈现。
一、设计目标
| 指标 | 参数 |
|---|---|
| 主控芯片 | STM32F103C8T6(Cortex-M3,72MHz) |
| PWM输出通道 | TIM1_CH1 → PA8 |
| 频率范围 | 100Hz ~ 1000Hz,步长100Hz,共10级 |
| 占空比范围 | 1% ~ 100%,步长可设(1~10),共100级 |
| 显示方式 | 两块4位共阳极数码管,通过74HC595移位寄存器驱动 |
| 控制方式 | 4个独立按键(切换模式 / 增加 / 减少 / 调整步进值) |
| 开发环境 | Keil MDK-ARM v5 + STM32Cube HAL 库 |
| 初始状态 | 频率 500Hz,占空比 50% |
二、硬件设计
2.1 系统总览
系统由四部分组成:按键输入模块、PWM输出模块、74HC595移位寄存器模块和数码管显示模块。整体架构如下:
┌──────────────┐ ┌─────────────────────────────────┐
│ 数码管×2 │◄───│ 74HC595 级联 (16位移位寄存器) │
│ (共阳极) │ └──────────────┬──────────────────┘
└──────────────┘ │ 3线控制 (DS/SHCP/STCP)
┌──────────────▼──────────────────┐
│ STM32F103C8T6 │
│ ┌──────────────────────────┐ │
│ │ TIM1_CH1 → PA8 (PWM OUT) │───► 示波器 / 负载
│ └──────────────────────────┘ │
│ ┌──────────────────────────┐ │
│ │ GPIO 按键扫描 │◄──│ K1~K4
│ └──────────────────────────┘ │
└─────────────────────────────────┘
2.2 引脚分配
| 功能 | 引脚 | 说明 |
|---|---|---|
| PWM输出 | PA8 | TIM1 通道1,复用推挽输出 |
| 74HC595-DS | PB7 | 串行数据输入 |
| 74HC595-SHCP | PB5 | 移位寄存器时钟(上升沿移位) |
| 74HC595-STCP | PB6 | 存储寄存器时钟(上升沿锁存输出) |
| 按键K1 | PB0 | 切换频率/占空比显示界面 |
| 按键K2 | PB1 | 增加当前值(频率或占空比) |
| 按键K3 | PA6 | 减少当前值 |
| 按键K4 | PA7 | 调整占空比步进值(1~10) |
2.3 74HC595 级联驱动数码管
74HC595 是一款经典的8位串入并出移位寄存器。本项目采用两片级联的方式,形成16位的移位寄存器——第一片输出段码(决定数码管哪些段亮),第二片输出位码(决定当前点亮哪一位)。
三根线即可控制8位数码管显示,极大节省了GPIO资源:
- DS(Serial Data):串行数据,每次送出一位
- SHCP(Shift Register Clock):上升沿时移位寄存器内容后移一位
- STCP(Storage Register Clock):上升沿时将移位寄存器数据锁存到输出寄存器
2.4 按键电路
四个独立按键一端接GPIO,另一端接地。GPIO配置为内部上拉输入——未按下时读高电平,按下时读低电平。通过按键扫描函数在while循环中轮询检测。
三、软件设计
3.1 工程结构
User/
├── main.c # 主程序
├── stm32f1xx_it.c/.h # 中断服务函数
└── stm32f1xx_hal_conf.h # HAL库配置
Drivers/BSP/
├── KEY/key.c, key.h # 按键扫描模块
├── SMG/smg.c, smg.h # 数码管显示模块(含74HC595驱动)
└── TIMER/gtim.c, gtim.h # PWM定时器模块
Drivers/SYSTEM/ # 正点原子系统文件
├── delay/delay.c, delay.h # 微秒延时
├── sys/sys.c, sys.h # 系统初始化
└── usart/usart.c, usart.h # 串口(用于调试输出)
3.2 PWM 频率与占空比计算
PWM的核心原理并不复杂:定时器从0向上计数到ARR(自动重装载值),在计数过程中与CCR(比较值)做比较,根据比较结果输出高低电平。
PWM模式1,向上计数的工作方式:
- CNT < CCR → 输出高电平
- CNT ≥ CCR → 输出低电平
- CNT 到达 ARR → 归零重新计数
计算公式:
其中 \(F_{TIM}\) = 72MHz(TIM1挂载在APB2总线上)。
本项目固定 ARR = 99(即 ARR+1 = 100),这样占空比直接等于 CCR 的值(1100对应1%100%),简化了计算:
小坑提醒:700Hz时直接用整数除法
720000/700 ≈ 1028.57,需要特殊处理(代码中psc = 720000/700不加-1),否则频率有偏差。
以500Hz / 50%占空比为例:
- \(PSC = 720000 / 500 - 1 = 1439\)
- \(F_{PWM} = 72000000 / (100 \times 1440) ≈ 500Hz\)
- \(CCR = 50\) → 占空比 = 50%
3.3 主程序流程
系统初始化 (HAL库、时钟72MHz、延时、串口)
│
▼
外设初始化 (按键、74HC595、PWM: 500Hz/50%)
│
▼
┌─────────────────┐
│ while(1) 循环 │
│ │
│ ┌───────────┐ │
│ │ 按键扫描 │ │ ← 检测K1~K4,更新频率/占空比/步进
│ └───────────┘ │
│ │ │
│ ┌───────────┐ │
│ │ 数码管显示 │ │ ← 根据当前模式(F/P)刷新显示
│ └───────────┘ │
└─────────────────┘
3.4 按键逻辑详解
| 按键 | 功能 | 行为 |
|---|---|---|
| K1 | 切换显示模式 | 频率界面 ↔ 占空比界面;切换时自动重置步进值 |
| K2 | 增加 | 频率模式:+100Hz;占空比模式:+当前步进 |
| K3 | 减少 | 频率模式:-100Hz;占空比模式:-当前步进 |
| K4 | 步进设置 | 频率模式下固定100Hz步进;占空比模式下步进1→2→...→10→1循环 |
每次调整后,重新初始化PWM定时器以应用新的频率和占空比参数。
3.5 数码管显示界面
频率界面(F_P_switch = 1):
F x x x s s s
↑ ↑
频率标识 频率值(3~4位) 步进值(3位)
占空比界面(F_P_switch = 2):
P x x s s s
↑
占空比标识 占空比值(2~3位) 步进值(3位)
四、核心代码解析
4.1 PWM初始化(gtim.c)
#define GTIM_TIMX_PWM TIM1
#define GTIM_TIMX_PWM_CHY TIM_CHANNEL_1
#define GTIM_TIMX_PWM_CHY_GPIO_PORT GPIOA
#define GTIM_TIMX_PWM_CHY_GPIO_PIN GPIO_PIN_8 /* PA8 */
uint16_t psc = 720000 / 500; /* 预分频数,初始500Hz */
uint16_t pulse = 50; /* 比较值,初始50%占空比 */
void gtim_timx_pwm_chy_init(void)
{
TIM_OC_InitTypeDef timx_oc_pwm_chy = {0};
g_timx_pwm_chy_handle.Instance = GTIM_TIMX_PWM;
g_timx_pwm_chy_handle.Init.Prescaler = psc;
g_timx_pwm_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;
g_timx_pwm_chy_handle.Init.Period = 100 - 1; /* ARR = 99 */
HAL_TIM_PWM_Init(&g_timx_pwm_chy_handle);
timx_oc_pwm_chy.OCMode = TIM_OCMODE_PWM1;
timx_oc_pwm_chy.Pulse = pulse; /* CCR */
timx_oc_pwm_chy.OCPolarity = TIM_OCPOLARITY_HIGH;
HAL_TIM_PWM_ConfigChannel(&g_timx_pwm_chy_handle,
&timx_oc_pwm_chy, GTIM_TIMX_PWM_CHY);
HAL_TIM_PWM_Start(&g_timx_pwm_chy_handle, GTIM_TIMX_PWM_CHY);
}
这里的巧妙之处在于将 ARR 固定为 99,使 (ARR+1) = 100,这样 CCR 的值(1~100)就直接等于占空比百分数,逻辑非常直观。
4.2 按键扫描(key.c)
/* 全局变量 */
uint8_t F_P_switch = 1; /* 1=频率模式, 2=占空比模式 */
int count_pulse = 50; /* 占空比计数值(1~100) */
uint16_t count_frequency = 500; /* 频率计数值(100~1000) */
uint8_t step = 100; /* 当前步进值 */
void key_scan(uint8_t mode)
{
static uint8_t key_up = 1; /* 按键松开标志 */
if (mode) key_up = 1; /* 支持连按 */
if (key_up == 1)
{
/* K1: 切换频率/占空比模式 */
if (HAL_GPIO_ReadPin(H1_PORT, H1_PIN) == GPIO_PIN_RESET)
{
delay_us(100); /* 消抖 */
if (HAL_GPIO_ReadPin(H1_PORT, H1_PIN) == GPIO_PIN_RESET)
{
key_up = 0;
F_P_switch++;
if (F_P_switch > 2) F_P_switch = 1;
if (F_P_switch == 1) step = 100; /* 频率模式:固定步进100 */
else if (F_P_switch == 2) step = 1; /* 占空比模式:默认步进1 */
}
}
/* K2: 增加 */
if (HAL_GPIO_ReadPin(H2_PORT, H2_PIN) == GPIO_PIN_RESET)
{
delay_us(100);
if (HAL_GPIO_ReadPin(H2_PORT, H2_PIN) == GPIO_PIN_RESET)
{
key_up = 0;
if (F_P_switch == 1) count_frequency += step;
else if (F_P_switch == 2) count_pulse += step;
/* 边界限制 */
if (count_frequency >= 1000) count_frequency = 1000;
if (count_pulse >= 100) count_pulse = 100;
/* 更新PWM参数并重新初始化 */
psc = (720000 / count_frequency) - 1;
if (count_frequency == 700) psc = 720000 / count_frequency;
pulse = count_pulse;
gtim_timx_pwm_chy_init();
}
}
/* K3: 减少 (逻辑同K2,方向相反) */
/* K4: 步进设置 (占空比模式: step++, 1~10循环) */
}
/* 所有按键均松开时,恢复 key_up 标志 */
}
4.3 74HC595 驱动与数码管显示(smg.c)
/* 共阳数码管段码表 */
uint8_t segment_code[19] = {
0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, /* 0~7 */
0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E, /* 8~F */
0x8C, 0xFF, 0xBF /* P, 熄灭, - */
};
/* 位码表(共阳极,低电平选通) */
uint8_t digital_code[8] = {0x01, 0x02, 0x04, 0x08,
0x10, 0x20, 0x40, 0x80};
/* 74HC595 串行移位 */
void HC595_ShiftRegister_Operation(uint8_t outdata)
{
uint8_t i;
for (i = 0; i < 8; i++)
{
if (outdata & 0x80) /* 取出最高位 */
HAL_GPIO_WritePin(DS_PORT, DS_PIN, GPIO_PIN_SET);
else
HAL_GPIO_WritePin(DS_PORT, DS_PIN, GPIO_PIN_RESET);
outdata <<= 1;
/* SHCP产生上升沿 → 移位 */
HAL_GPIO_WritePin(SHCP_PORT, SHCP_PIN, GPIO_PIN_RESET);
HAL_GPIO_WritePin(SHCP_PORT, SHCP_PIN, GPIO_PIN_SET);
}
}
/* 锁存输出 */
void HC595_StorageRegister_Operation(uint8_t segment, uint8_t digit)
{
HC595_ShiftRegister_Operation(segment); /* 先送段码 */
HC595_ShiftRegister_Operation(digit); /* 再送位码 */
/* STCP上升沿 → 数据锁存到输出端 */
HAL_GPIO_WritePin(STCP_PORT, STCP_PIN, GPIO_PIN_RESET);
HAL_GPIO_WritePin(STCP_PORT, STCP_PIN, GPIO_PIN_SET);
delay_us(500); /* 短暂延时,让数码管稳定显示 */
}
74HC595的级联操作非常优雅:通过两片串联形成16位移位寄存器,第一字节为段码、第二字节为位码,STCP上升沿时将16位数据一次性锁存输出。整个过程只需3根GPIO。
五、实验结果
测试场景1:默认状态
- 频率:500Hz | 占空比:50%
- 数码管显示正确,示波器波形符合预期
测试场景2:极限占空比
- 频率:500Hz | 占空比:99%
- 波形接近全高,脉冲宽度准确
测试场景3:极限频率 + 极小占空比
- 频率:1000Hz | 占空比:1%
- 窄脉冲清晰可见,频率和占空比均符合预期
误差分析:实测频率误差在 1%~2% 以内,主要来源于整数除法带来的PSC参数截断误差(尤其是700Hz附近的频率点)。如需更高精度,可考虑使用定时器级联或DMA方式。
六、总结与踩坑记录
值得注意的几个点
-
ARR固定为99的技巧:让
ARR+1=100,CCR值直接等于占空比百分数,省去了比例换算,初学者友好。 -
700Hz特殊处理:
720000/700取整后如果直接减一会导致频率偏小,代码中针对这个频点做了特殊处理。 -
74HC595级联的优势:3根线控制8位数码管,相比直接用IO口(需要8+8=16根),大幅节省引脚资源,且74HC595自带锁存功能,动态扫描时无需消影处理。
-
不要随意关闭SWJ/JTAG:调试过程中如果关闭了SWD/JTAG调试接口,会导致ST-Link无法连接。此时需要按住复位键再下载程序来恢复。
-
PCB制作需要耐心:手工转印制板时温度控制在170℃左右,钻孔后需用砂纸打磨孔壁以提高粘附性。飞线虽不可避免,但红白区分电源和信号有助于后期排查。
项目感悟
这个项目从原理图设计、PCB制板到代码编写全部手工完成,虽然功能不算复杂,但覆盖了STM32开发的核心知识点——定时器PWM配置、GPIO输入检测、移位寄存器级联驱动、数码管动态扫描等。对于刚接触STM32的同学来说,是一个非常适合练手的综合性小项目。
参考资料
- 刘军.《例说STM32》. 北京航空航天大学出版社,2018
- STM32F10xxx参考手册_V10(中文版)
- 正点原子 STM32F103 MINI开发指南 V1.3
- 野火《STM32 HAL库开发实战指南——F103系列》
项目代码:https://github.com/ikohagavuz242-dot/stm32-pwm-generator.git
项目日期:2023.11.12 V1.0
欢迎在评论区交流讨论,如果你也在学习STM32,希望这篇文章对你有所帮助!
浙公网安备 33010602011771号