单片机 LED 与点阵控制

同一套 LED 控制思想在 51、STM32、Arduino 上的三种写法:流水灯、呼吸灯与点阵显示,适合嵌入式入门。

场景:嵌入式入门 技术栈:C / Keil / STM32 HAL / Arduino 来源:GitHub 参考

项目简介

LED 控制是单片机的 Hello World。掌握 IO 口输出、定时器延时、PWM 调光,就等于掌握了嵌入式开发的地基。以下三份代码对应三种主流平台,可直接烧录验证。

代码示例:51 单片机流水灯

c
#include <REGX52.H>

void delay(unsigned int ms) {
    unsigned int i, j;
    for (i = 0; i < ms; i++)
        for (j = 0; j < 120; j++);
}

void main() {
    unsigned char led = 0xFE;   // 1111 1110,P0.0 点亮
    while (1) {
        P0 = led;               // 输出到 P0 口
        delay(200);
        led = (led << 1) | 0x01;
        if (led == 0xFF) led = 0xFE;  // 循环回起点
    }
}

代码示例:Arduino 呼吸灯(PWM)

cpp
const int ledPin = 9;  // 支持 PWM 的引脚

void setup() {
  pinMode(ledPin, OUTPUT);
}

void loop() {
  for (int v = 0; v <= 255; v++) {   // 由暗到亮
    analogWrite(ledPin, v);
    delay(6);
  }
  for (int v = 255; v >= 0; v--) {   // 由亮到暗
    analogWrite(ledPin, v);
    delay(6);
  }
}

代码示例:STM32 HAL 点亮 LED

c
/* 假设 LED 接在 PB1,低电平点亮 */
#include "main.h"

void toggle_led(void) {
    HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_1);
}

void main(void) {
    HAL_Init();
    __HAL_RCC_GPIOB_CLK_ENABLE();

    GPIO_InitTypeDef io = {0};
    io.Pin = GPIO_PIN_1;
    io.Mode = GPIO_MODE_OUTPUT_PP;
    io.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOB, &io);

    while (1) {
        toggle_led();
        HAL_Delay(500);   // 500ms 闪烁
    }
}

操作步骤

  1. 51 版:Keil 建工程选 AT89C52,编译后烧录到开发板 P0 口观察流水灯。
  2. Arduino 版:LED 正极接 D9(串联 220Ω 电阻),上传后即可看到呼吸效果。
  3. STM32 版:STM32CubeMX 初始化 PB1 为输出,生成工程后替换 main 循环体。

完整可运行工程

可直接运行(离线仿真)仿真零第三方依赖固件需实机 / 工具链

运行环境:Python 3.8+(仿真器仅标准库,无需 pip 安装);固件三个版本分别面向 51 单片机(Keil C51)、Arduino(Arduino IDE / PlatformIO)、STM32(STM32CubeMX + HAL)。先用 python sim_led.py --self-test 零配置验证流水灯移位与呼吸灯 PWM 序列,再对照固件烧录。

目录结构说明:src/ 下为三份可直接编译的完整源文件,sim_led.py 与固件共享同一套移位/占空比算法,可用于上板前对拍。

mcu-led/ ├── README.md 运行步骤、配置项与硬件接线说明 ├── platformio.ini Arduino / PlatformIO 工程配置(board、framework、串口速率) ├── requirements.txt 依赖清单(仿真器仅标准库) ├── sim_led.py 离线仿真器:流水灯序列 + 呼吸灯占空比(可 --dump 导出) └── src/ ├── led51.c 51 单片机(REGX52.H)完整流水灯固件 ├── main.cpp Arduino 完整固件:流水灯 + 呼吸灯 + 8x8 点阵扫描 └── stm32_led.c STM32 HAL 完整应用:GPIO 点亮 + TIM3 PWM 呼吸灯

README.md

运行步骤、硬件接线、需先改的配置项与自测命令。

markdown
# mcu-led · 单片机 LED 与点阵控制(可运行工程)

同一套 LED 控制思想在 **51 单片机 / Arduino / STM32** 上的三种完整实现,另附一个**无需硬件、纯标准库就能跑通**的离线仿真器,
用于在电脑上验证流水灯移位逻辑与呼吸灯 PWM 占空比序列。

## 目录结构

```
mcu-led/
├── platformio.ini         PlatformIO 工程配置(依赖清单:framework=arduino)
├── requirements.txt       电脑端依赖清单(仿真器零第三方依赖)
├── src/
│   ├── main.cpp           Arduino 完整固件:流水灯 + 呼吸灯 + 8x8 点阵扫描(无第三方库)
│   ├── led51.c            51 单片机(Keil C51)完整流水灯固件
│   └── stm32_led.c        STM32 HAL 完整应用文件:GPIO 点亮 + TIM3 PWM 呼吸灯
└── sim_led.py             离线仿真:流水灯移位序列 + 呼吸灯占空比(纯 Python 3.8+ 标准库)
```

## 可直接运行(无需硬件)

```bash
python sim_led.py --self-test        # 断言自检:8 步回卷、单点独亮、占空比峰值与单调性
python sim_led.py --run --steps 12   # 打印前 12 步 LED 状态与二进制
python sim_led.py --dump pwm.json    # 导出呼吸灯占空比序列
```

`--self-test` 全部断言通过即输出 `ALL CHECKS PASSED`。这是本工程与固件行为一致的最小用例:
仿真器里的移位公式 `led = (led << 1) | off_bit` 与 `led51.c`、`main.cpp` 中固件代码逐位一致。

## 需先改的配置项

| 位置 | 配置项 | 说明 |
|---|---|---|
| `src/main.cpp` | `LED_PINS[]` | Arduino 引脚号,按你的接线修改(默认 2–9 接 8 颗 LED) |
| `src/main.cpp` | `MATRIX_ROW[]` / `MATRIX_COL[]` | 8x8 点阵行/列引脚,按模块接线修改 |
| `src/main.cpp` | `ACTIVE_LOW` | `true` = 低电平点亮(共阳接法),按你的接法改 |
| `src/led51.c` | `P0` 端口 | 51 单片机流水灯输出口,若接 P1/P2 改端口名即可 |
| `src/stm32_led.c` | `LED_Pin` / `LED_GPIO_Port`、`htim3` | 与 CubeMX 生成的宏/句柄名保持一致 |
| `platformio.ini` | `board` | 默认 `uno`;换板子改成 `nanoatmega328` / `esp32dev` 等 |
| `sim_led.py` | `--pins` / `--cycle` | 仿真灯珠数量与呼吸灯 PWM 周期 |

## 需硬件 / 需外部工具的步骤

- **Arduino**:`pio run -t upload`(需安装 PlatformIO)或用 Arduino IDE 打开 `src/main.cpp` 直接上传;无硬件时可用 [Wokwi](https://wokwi.com/) 在线仿真。
- **51 单片机**:Keil C51(µVision)新建工程导入 `src/led51.c`,或使用 [SDCC](https://sdcc.sourceforge.net/) 编译后烧录。
- **STM32**:STM32CubeMX 生成工程 → 用 `src/stm32_led.c` 中的 `MX_GPIO_Init()` / `MX_TIM3_Init()` / `led_breath_task()` 替换 `Core/Src/main.c` 中同名部分 → Keil/IAR/CubeIDE 编译下载。

## 依赖清单

- 电脑端:Python 3.8+,**零第三方依赖**(见 `requirements.txt`)
- Arduino 端:PlatformIO(`framework = arduino`),**无第三方库**
- 51 端:Keil C51 或 SDCC
- STM32 端:STM32CubeMX + STM32F1 HAL 驱动(`stm32f1xx_hal.h`)

## 原理速览

| 现象 | 底层机制 |
|---|---|
| 流水灯 | 端口字节循环左移,移位溢出位被熄灭位(`1`/`0` 取决于接法)回填 |
| 呼吸灯 | 定时器 PWM:占空比按三角波在 0→240→0 之间变化,人眼积分成亮度渐变 |
| 点阵显示 | 逐行扫描(滚屏刷新),单行点亮时间 ÷ 行数即为占空比;扫描频率 > 60Hz 无闪烁 |

platformio.ini

PlatformIO 工程配置:板型、框架与串口速率(换板只需改 board)。

ini
; PlatformIO 工程配置 —— 依赖清单
; 用法:pio run -t upload   (需先安装 PlatformIO Core 或 VSCode 插件)
; 换板子只需改 board;本工程仅使用 Arduino 官方 framework,无第三方库依赖。
[env:uno]
platform = atmelavr
board = uno
framework = arduino

; 若使用 ESP32,取消下面注释并注释掉上面的 uno 环境
; [env:esp32dev]
; platform = espressif32
; board = esp32dev
; framework = arduino

monitor_speed = 115200

requirements.txt

依赖清单:仿真器仅需 Python 标准库。

text
# mcu-led 电脑端依赖清单
# 离线仿真器 sim_led.py 仅使用 Python 标准库(argparse / json / sys),无需安装任何包。
# 本文件保留 Python 版本要求,便于后续扩展真实串口联调:
#   pip install -r requirements.txt
#
# Python >= 3.8
# 若需要直接读取开发板串口输出(可选,仿真器本身不需要):
# pyserial>=3.5

sim_led.py

离线仿真器:流水灯移位 + 呼吸灯 PWM 占空比,支持 --self-test / --dump。

python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""mcu-led 离线仿真器:无需硬件,在电脑上复现流水灯移位与呼吸灯 PWM 占空比序列。

与固件的一致性:
  · 移位公式  led = (led << 1) | off_bit  —— 与 src/led51.c、src/main.cpp 完全相同
  · 占空比序列 0 -> cycle-step -> 0       —— 与 src/stm32_led.c 的 led_breath_task 相同

用法:
  python sim_led.py --self-test            断言自检(最小用例,全部通过输出 ALL CHECKS PASSED)
  python sim_led.py --run --steps 12       打印前 12 步 LED 状态
  python sim_led.py --dump pwm.json        导出呼吸灯占空比序列
  python sim_led.py --run --active-low --pins 8
"""
import argparse
import json
import sys


def shift_sequence(steps, pins=8, active_low=True):
    """流水灯端口字节序列。

    初始只有第 0 位点亮:active_low 时 0xFE(1111 1110),否则 0x01。
    每步左移一位并回填“熄灭位”:active_low 回填 1,否则回填 0;
    全部熄灭(active_low: 0xFF / 否则 0x00)时回到起点,与固件中的 if 判断一致。
    """
    mask = (1 << pins) - 1
    off_bit = 1 if active_low else 0
    led = (mask ^ 0x01) if active_low else 0x01
    full_off = mask if active_low else 0
    seq = []
    for _ in range(steps):
        seq.append(led)
        led = ((led << 1) | off_bit) & mask
        if led == full_off:
            led = (mask ^ 0x01) if active_low else 0x01
    return seq


def breath_duty(cycle=256, step=16):
    """呼吸灯 PWM 占空比三角波:0, step, ..., cycle-step(峰值), cycle-2*step, ..., step。

    峰值在三角波顶点只出现一次,与固件 runBreath() / led_breath_task() 的循环边界
    (上升段 d<240,下降段从 240-step 起)逐点对齐。
    """
    if step <= 0 or cycle % step:
        raise ValueError('step 必须整除 cycle 且大于 0')
    up = list(range(0, cycle, step))                 # 0 .. cycle-step(含峰值)
    down = list(range(cycle - 2 * step, 0, -step))   # cycle-2*step .. step
    return up + down


def led_positions(value, pins=8, active_low=True):
    """把一个端口字节翻译成“第几颗灯亮”的列表。"""
    lit = []
    for b in range(pins):
        bit = (value >> b) & 0x01
        if (bit == 0) if active_low else (bit == 1):
            lit.append(b)
    return lit


def self_test():
    ok = True

    # 1) 8 步回卷 + 每步恰好一颗灯亮
    seq = shift_sequence(8)
    assert seq[0] == 0xFE, seq
    assert seq[1] == 0xFD, seq
    for i, v in enumerate(seq):
        n = len(led_positions(v))
        if n != 1:
            print('  [FAIL] 第 %d 步点亮灯数 = %d(应为 1)' % (i, n))
            ok = False
    nxt = shift_sequence(9)[8]
    if nxt != 0xFE:
        print('  [FAIL] 第 9 步未回到起点:%s' % hex(nxt))
        ok = False
    print('  [OK] 流水灯:8 步覆盖全部灯珠、每步单点独亮、第 9 步回卷到 0xFE')

    # 2) 高电平点亮(Arduino 常见接法)行为
    seq_h = shift_sequence(8, active_low=False)
    if seq_h[0] != 0x01 or shift_sequence(9, active_low=False)[8] != 0x01:
        print('  [FAIL] 高电平点亮模式回卷异常:%s' % seq_h)
        ok = False
    if any(len(led_positions(v, active_low=False)) != 1 for v in seq_h):
        print('  [FAIL] 高电平点亮模式存在非单点步')
        ok = False
    print('  [OK] 高电平点亮模式:初始 0x01,第 9 步回卷,单点独亮')

    # 3) 呼吸灯占空比:峰值、单调性、首尾归零
    duty = breath_duty()
    half = len(duty) // 2          # 峰值所在下标(上升段末位)
    assert duty[0] == 0, duty[:3]
    assert duty[half] == 240 and max(duty) == 240, (duty[half], max(duty))
    assert duty[-1] == 16, duty[-3:]
    if not all(duty[i] < duty[i + 1] for i in range(half)):
        print('  [FAIL] 上升段非严格递增')
        ok = False
    if not all(duty[i] > duty[i + 1] for i in range(half, len(duty) - 1)):
        print('  [FAIL] 下降段非严格递减')
        ok = False
    print('  [OK] 呼吸灯:上升/下降各 %d 级,峰值 %d/255,首尾为 0 与 %d' % (half, max(duty), duty[-1]))

    # 4) 固件同参数交叉校验
    if len(shift_sequence(1000)) != 1000:
        print('  [FAIL] 长序列长度异常')
        ok = False
    print('  [OK] 长序列(1000 步)无异常,可作为固件长时间运行的位序列参照')

    if ok:
        print('ALL CHECKS PASSED')
        return 0
    print('SELF TEST FAILED')
    return 1


def main():
    ap = argparse.ArgumentParser(description='mcu-led 离线仿真器(流水灯 / 呼吸灯)')
    ap.add_argument('--self-test', action='store_true', help='运行断言自检')
    ap.add_argument('--run', action='store_true', help='打印 LED 序列')
    ap.add_argument('--steps', type=int, default=9, help='打印步数,默认 9')
    ap.add_argument('--pins', type=int, default=8, help='灯珠数量,默认 8')
    ap.add_argument('--active-low', action='store_true', default=True, help='低电平点亮(默认)')
    ap.add_argument('--active-high', dest='active_low', action='store_false', help='高电平点亮')
    ap.add_argument('--cycle', type=int, default=256, help='呼吸灯 PWM 周期,默认 256')
    ap.add_argument('--step', type=int, default=16, help='占空比步进,默认 16')
    ap.add_argument('--dump', default='', help='导出占空比序列到 JSON 文件')
    a = ap.parse_args()

    if a.self_test:
        return self_test()

    if a.dump:
        data = {'cycle': a.cycle, 'step': a.step, 'duty': breath_duty(a.cycle, a.step)}
        with open(a.dump, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
        print('已导出 %d 级占空比 -> %s' % (len(data['duty']), a.dump))

    if a.run or not a.dump:
        print('流水灯序列(pins=%d, %s):' % (a.pins, '低电平点亮' if a.active_low else '高电平点亮'))
        for i, v in enumerate(shift_sequence(a.steps, a.pins, a.active_low)):
            print('  步 %2d  端口=0x%02X  %s  点亮=P%d' % (
                i, v, format(v, '0%db' % a.pins), (led_positions(v, a.pins, a.active_low) or [-1])[0]))
        d = breath_duty(a.cycle, a.step)
        print('呼吸灯占空比:%d 级,%s -> %d -> %s' % (len(d), d[0], max(d), d[-1]))
    return 0


if __name__ == '__main__':
    sys.exit(main())

src/led51.c

51 单片机完整固件:KEIL C51 工程可直接编译,含接线注释与延时函数。

c
/* 51 单片机(STC89C52 / AT89C52 等)完整流水灯固件
 * 工具链:Keil C51 (µVision) 或 SDCC,晶振 11.0592MHz / 12MHz 均可
 * 依赖:仅 REGX52.H(Keil 自带头文件),无第三方库
 * 需先改的配置项:LED_PORT(默认 P0,低电平点亮)。若 LED 接 P1/P2,把 P0 改成对应端口名。
 */
#include <REGX52.H>

#define LED_PORT P0          /* 输出端口:接 8 颗 LED(低电平点亮,共阳接法) */
#define STEP_MS  200         /* 每步延时(毫秒) */

/* 约 1ms 的软件延时(12MHz 晶振,i/j 循环按实际主频微调) */
void delay_ms(unsigned int ms)
{
    unsigned int i, j;
    for (i = 0; i < ms; i++)
        for (j = 0; j < 120; j++);
}

void main(void)
{
    unsigned char led = 0xFE;    /* 1111 1110:P0.0 点亮,其余熄灭 */

    while (1)
    {
        LED_PORT = led;          /* 输出到 P0 口 */
        delay_ms(STEP_MS);
        led = (led << 1) | 0x01; /* 左移一位,最低位补 1(熄灭) */
        if (led == 0xFF)         /* 全部熄灭 -> 回到起点,形成循环 */
            led = 0xFE;
    }
}

/* 可选:定时器 0 中断版流水灯(精度更高,不占用 CPU 空转)
 * 若使用本段,请删除上面的 main() 并把 TIMER_ENABLE 置 1。
 * 12MHz 晶振、50ms@50Hz 定时:初值 0x3CB0(约 50ms)。
 */
#if 0
#include <REGX52.H>
static unsigned char led = 0xFE;

void timer0_isr(void) interrupt 1
{
    TH0 = 0x3C;  TL0 = 0xB0;      /* 重装初值 */
    LED_PORT = led;
    led = (led << 1) | 0x01;
    if (led == 0xFF) led = 0xFE;
}

void main(void)
{
    TMOD &= 0xF0; TMOD |= 0x01;   /* 定时器 0,模式 1(16 位) */
    TH0 = 0x3C;  TL0 = 0xB0;
    ET0 = 1;  EA = 1;  TR0 = 1;   /* 开中断并启动 */
    while (1);
}
#endif

src/main.cpp

Arduino 完整固件:流水灯 + 呼吸灯 + 8x8 点阵扫描,无第三方库。

cpp
/* Arduino 完整固件:流水灯 + 呼吸灯 + 8x8 点阵扫描
 * 平台:Arduino UNO(AVR)或 ESP32(改动见 platformio.ini)
 * 依赖:仅 Arduino 官方核心,无第三方库
 * 上电后:先跑 2 轮流水灯,再跑 2 轮呼吸灯,随后循环显示点阵图案。
 * 需先改的配置项:LED_PINS[]、MATRIX_ROW[]、MATRIX_COL[]、ACTIVE_LOW
 */
#include <Arduino.h>

/* ===== 配置区(按你的接线修改) ===== */
static const uint8_t LED_PINS[] = {2, 3, 4, 5, 6, 7, 8, 9};  // 8 颗 LED 依次接 D2..D9
static const uint8_t LED_COUNT   = sizeof(LED_PINS) / sizeof(LED_PINS[0]);
static const uint8_t MATRIX_ROW[] = {A0, A1, A2, A3, A4, A5, 10, 11}; // 点阵行(扫描输出)
static const uint8_t MATRIX_COL[] = {12, 13, A6, A7, 22, 23, 24, 25}; // 点阵列(低电平选中)
static const bool    ACTIVE_LOW   = false;   // LED 接法:false=高电平点亮(LED 正极接引脚)
static const uint8_t BREATH_PWM_PIN = 9;     // 呼吸灯 PWM 引脚(需支持 PWM)

/* 8x8 点阵图案:1=点亮(心形),按行存 8 字节 */
static const uint8_t PATTERN[8] = {
  0b00000000,
  0b01100110,
  0b11111111,
  0b11111111,
  0b01111110,
  0b00111100,
  0b00011000,
  0b00000000
};

static inline void ledWrite(uint8_t index, bool on) {
  digitalWrite(LED_PINS[index], ACTIVE_LOW ? (on ? LOW : HIGH) : (on ? HIGH : LOW));
}

/* ---------- 1. 流水灯:端口字节循环左移(与 sim_led.py 逻辑一致) ---------- */
void runWaterfall(uint8_t rounds, uint16_t stepMs) {
  uint8_t led = ACTIVE_LOW ? 0xFE : 0x01;           // 初始只有一颗点亮
  const uint8_t mask = (LED_COUNT >= 8) ? 0xFF : (uint8_t)((1 << LED_COUNT) - 1);
  const uint8_t offBit = ACTIVE_LOW ? 1 : 0;
  for (uint16_t i = 0; i < rounds * LED_COUNT; ++i) {
    for (uint8_t b = 0; b < LED_COUNT; ++b) ledWrite(b, (led >> b) & 0x01);
    delay(stepMs);
    led = (uint8_t)((led << 1) | offBit) & mask;     // 移位 + 回填熄灭位
    if (led == (ACTIVE_LOW ? mask : 0)) led = ACTIVE_LOW ? 0xFE : 0x01;
  }
  for (uint8_t b = 0; b < LED_COUNT; ++b) ledWrite(b, false);
}

/* ---------- 2. 呼吸灯:PWM 占空比三角波(与 sim_led.py breath_duty 一致) ---------- */
void runBreath(uint16_t cycles, uint8_t step) {
  for (uint16_t c = 0; c < cycles; ++c) {
    for (uint8_t d = 0; d < 240; d += step)          { analogWrite(BREATH_PWM_PIN, d); delay(6); }
    for (int d = 240 - step; d > 0; d -= step)       { analogWrite(BREATH_PWM_PIN, d); delay(6); }
  }
  analogWrite(BREATH_PWM_PIN, 0);
}

/* ---------- 3. 8x8 点阵:逐行扫描显示图案 ---------- */
void showPattern(uint16_t frames, uint16_t rowUs) {
  for (uint16_t f = 0; f < frames; ++f) {
    for (uint8_t r = 0; r < 8; ++r) {
      digitalWrite(MATRIX_ROW[r], HIGH);                 // 选中当前行
      for (uint8_t c = 0; c < 8; ++c) {
        bool on = (PATTERN[r] >> (7 - c)) & 0x01;        // 高位在左
        digitalWrite(MATRIX_COL[c], on ? LOW : HIGH);    // 列低电平点亮
      }
      delayMicroseconds(rowUs);                          // 整帧 8 行 × rowUs,>60Hz 无闪烁
      digitalWrite(MATRIX_ROW[r], LOW);
      for (uint8_t c = 0; c < 8; ++c) digitalWrite(MATRIX_COL[c], HIGH);
    }
  }
}

void setup() {
  Serial.begin(115200);
  for (uint8_t i = 0; i < LED_COUNT; ++i) pinMode(LED_PINS[i], OUTPUT);
  for (uint8_t i = 0; i < 8; ++i) pinMode(MATRIX_ROW[i], OUTPUT);
  for (uint8_t i = 0; i < 8; ++i) pinMode(MATRIX_COL[i], OUTPUT);
  pinMode(BREATH_PWM_PIN, OUTPUT);
  Serial.println(F("mcu-led ready: waterfall -> breath -> matrix"));
}

void loop() {
  runWaterfall(2, 120);
  runBreath(2, 16);
  showPattern(300, 1200);
}

src/stm32_led.c

STM32 HAL 完整应用文件:GPIO 点亮 + TIM3 PWM 呼吸灯,含 HAL 初始化调用位置说明。

c
/* STM32 HAL 应用文件:GPIO 点亮/闪烁 + TIM3 PWM 呼吸灯
 * 适用:STM32F1 系列(其他系列把 stm32f1xx_hal.h 换成对应系列头文件即可)
 * 依赖:STM32CubeMX 生成的 HAL 工程(stm32f1xx_hal.h、main.h)
 * 用法:在 CubeMX 中勾选 PA5=GPIO_Output、PB0=TIM3_CH3=PWM Generation,生成工程后,
 *       把本文件的三个函数复制进 Core/Src/main.c,并在 main() 的初始化段后调用 led_demo()。
 * 需先改的配置项:LED_GPIO_Port / LED_Pin(默认 PA5)、htim3(CubeMX 中的定时器句柄名)。
 */
#include "stm32f1xx_hal.h"      /* CubeMX 生成工程时自动提供 */

/* ===== 需与 CubeMX 配置保持一致的宏 ===== */
#define LED_GPIO_Port  GPIOA
#define LED_Pin        GPIO_PIN_5
extern TIM_HandleTypeDef htim3;      /* CubeMX 生成的 TIM3 句柄 */
#define BREATH_CHANNEL TIM_CHANNEL_3 /* PWM 输出通道 */

/* ---------- 1. GPIO 初始化:推挽输出 ---------- */
void MX_GPIO_Init(void)
{
    GPIO_InitTypeDef gpio = {0};
    __HAL_RCC_GPIOA_CLK_ENABLE();            /* 使能 GPIOA 时钟 */
    gpio.Pin   = LED_Pin;
    gpio.Mode  = GPIO_MODE_OUTPUT_PP;        /* 推挽输出 */
    gpio.Pull  = GPIO_NOPULL;
    gpio.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(LED_GPIO_Port, &gpio);
    HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);  /* 上电默认熄灭 */
}

/* ---------- 2. TIM3 PWM 初始化:1kHz、初始占空比 0 ---------- */
void MX_TIM3_Init(void)
{
    TIM_OC_InitTypeDef oc = {0};
    __HAL_RCC_TIM3_CLK_ENABLE();

    htim3.Instance               = TIM3;
    htim3.Init.Prescaler         = 71;       /* 72MHz / 72 = 1MHz 计数时钟 */
    htim3.Init.CounterMode       = TIM_COUNTERMODE_UP;
    htim3.Init.Period            = 999;      /* 1MHz / 1000 = 1kHz PWM */
    htim3.Init.ClockDivision     = TIM_CLOCKDIVISION_DIV1;
    htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
    if (HAL_TIM_PWM_Init(&htim3) != HAL_OK) { Error_Handler(); }

    oc.OCMode     = TIM_OCMODE_PWM1;
    oc.Pulse      = 0;                       /* 占空比 = Pulse / (Period+1) */
    oc.OCPolarity = TIM_OCPOLARITY_HIGH;
    oc.OCFastMode = TIM_OCFAST_DISABLE;
    if (HAL_TIM_PWM_ConfigChannel(&htim3, &oc, BREATH_CHANNEL) != HAL_OK) { Error_Handler(); }
    HAL_TIM_PWM_Start(&htim3, BREATH_CHANNEL);
}

/* ---------- 3. 呼吸灯:占空比三角波 0 -> 240 -> 0(与 sim_led.py 一致) ---------- */
void led_breath_task(uint16_t cycles, uint8_t step)
{
    for (uint16_t c = 0; c < cycles; ++c) {
        for (uint16_t duty = 0; duty < 240; duty += step) {
            __HAL_TIM_SET_COMPARE(&htim3, BREATH_CHANNEL, duty);  /* 0..999 映射到 0..100% */
            HAL_Delay(6);
        }
        for (int duty = 240 - step; duty > 0; duty -= step) {
            __HAL_TIM_SET_COMPARE(&htim3, BREATH_CHANNEL, (uint16_t)duty);
            HAL_Delay(6);
        }
    }
    __HAL_TIM_SET_COMPARE(&htim3, BREATH_CHANNEL, 0);
}

/* ---------- 4. 组合演示:闪烁 3 次后进入呼吸循环 ---------- */
void led_demo(void)
{
    MX_GPIO_Init();
    MX_TIM3_Init();
    for (int i = 0; i < 3; ++i) {
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
        HAL_Delay(300);
    }
    while (1) { led_breath_task(2, 16); }
}

/* 说明:Error_Handler() 由 CubeMX 生成的 main.c 提供;
 * 单独移植本文件时请自行实现(例如 while(1) 死循环 + 串口打印错误码)。
 */

可直接运行 / 需先改的配置项:

  • 可直接运行:python sim_led.py --self-test(零依赖、零配置,10 秒内通过);python sim_led.py --run --pins 8 --steps 20 查看流水灯序列。
  • 需先改·Arduino:src/main.cpp 顶部 LED_PIN_START / BREATH_PWM_PIN / MATRIX_* 按实际接线调整。
  • 需先改·STM32:src/stm32_led.c 中 LED_GPIO_Port/LED_Pin 与 TIM3 通道,需与 CubeMX 生成的 main.c 引脚一致。
  • 需先改·51:src/led51.c 的端口号与晶振频率(影响延时长短)。
  • PlatformIO 使用:改 platformio.ini 的 board 为你的板型后 pio run -t upload。
复制结果为 # ===== 文件名 ===== 分隔的多文件文本,按文件名分别保存即可;也可逐块用右上角“复制代码”单独取用。

来源参考