基于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_{PWM} = \frac{F_{TIM}}{(ARR+1) \times (PSC+1)} \]

\[Duty = \frac{CCR}{ARR+1} \times 100\% \]

其中 \(F_{TIM}\) = 72MHz(TIM1挂载在APB2总线上)。

本项目固定 ARR = 99(即 ARR+1 = 100),这样占空比直接等于 CCR 的值(1100对应1%100%),简化了计算:

\[PSC = \frac{720000}{Freq} - 1 \]

小坑提醒: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方式。


六、总结与踩坑记录

值得注意的几个点

  1. ARR固定为99的技巧:让 ARR+1=100,CCR值直接等于占空比百分数,省去了比例换算,初学者友好。

  2. 700Hz特殊处理720000/700 取整后如果直接减一会导致频率偏小,代码中针对这个频点做了特殊处理。

  3. 74HC595级联的优势:3根线控制8位数码管,相比直接用IO口(需要8+8=16根),大幅节省引脚资源,且74HC595自带锁存功能,动态扫描时无需消影处理。

  4. 不要随意关闭SWJ/JTAG:调试过程中如果关闭了SWD/JTAG调试接口,会导致ST-Link无法连接。此时需要按住复位键再下载程序来恢复。

  5. PCB制作需要耐心:手工转印制板时温度控制在170℃左右,钻孔后需用砂纸打磨孔壁以提高粘附性。飞线虽不可避免,但红白区分电源和信号有助于后期排查。

项目感悟

这个项目从原理图设计、PCB制板到代码编写全部手工完成,虽然功能不算复杂,但覆盖了STM32开发的核心知识点——定时器PWM配置、GPIO输入检测、移位寄存器级联驱动、数码管动态扫描等。对于刚接触STM32的同学来说,是一个非常适合练手的综合性小项目。


参考资料

  1. 刘军.《例说STM32》. 北京航空航天大学出版社,2018
  2. STM32F10xxx参考手册_V10(中文版)
  3. 正点原子 STM32F103 MINI开发指南 V1.3
  4. 野火《STM32 HAL库开发实战指南——F103系列》

项目代码https://github.com/ikohagavuz242-dot/stm32-pwm-generator.git

项目日期:2023.11.12 V1.0


欢迎在评论区交流讨论,如果你也在学习STM32,希望这篇文章对你有所帮助!

posted @ 2026-06-01 20:28  成风破浪  阅读(17)  评论(0)    收藏  举报