《 万事开头难 》

在嵌入式开发的世界里,STM32 是一个绕不开的名字,而正点原子 则是无数中国开发者入门的“领路人”。最近,我从杂物箱底翻出了一块吃灰多年的“战损版”正点原子战舰V3开发板。屏幕碎了,螺丝生锈了,但当我接通电源,那颗红色的电源指示灯依然亮起时,突然有种莫名的感动。

这么多年过去了,它竟然还能用。

这或许就是硬件工程师的浪漫:只要代码不错,硬件不坏,它就永远忠于你的逻辑。今天,我不想让它继续吃灰,决定用它来写下第一个程序——点亮LED指示灯。这不仅是嵌入式世界的“Hello World”,也是对这块老将的一次致敬。



一、 准备工作:唤醒沉睡的“战舰”
虽然我的板子是战损成色,但正点原子的设计质量确实过硬。如果你手头有任何型号的STM32F103开发板(无论是战舰、精英还是Mini),都可以按照今天的步骤操作。

我们需要准备:

硬件:

STM32F103 开发板(正点原子系列均可,我的是战舰V3)。

ST-Link 或 J-Link 仿真器/下载器(用于烧录程序)。

Micro USB 线(用于供电或串口通信)。

软件:

开发环境: Keil MDK(或者STM32CubeIDE,这里我们为了经典,使用Keil)。

固件库: STM32F1xx的标准外设库(或者HAL库,今天我们以标准库为例,这也是正点原子教程的经典配置)。

参考资料: 开发板的原理图PDF(这是最重要的,后面找引脚全靠它)。

二、 硬件分析:LED在哪里?
任何程序的第一步,都不是写代码,而是看原理图。

打开正点原子战舰V3的原理图,找到LED相关的部分。你会发现,板上通常会集成两个LED:

DS0: 通常连接在 PB5 引脚上。

DS1: 通常连接在 PE5 引脚上。

关键点分析:
查看电路连接,我们会发现这些LED的另一端是接在 VCC 3.3V 上的。这意味着什么呢?

如果单片机引脚输出 低电平(0V),电流从VCC流过LED到达引脚,形成回路,灯亮。

如果单片机引脚输出 高电平(3.3V),两端电压相等,灯灭。

这就是常说的“低电平有效”。弄清楚这个逻辑,代码的方向就不会错。

三、 动手写码:三种方式实现“点灯”
在STM32开发中,我们通常有三种方式操作寄存器:寄存器操作、标准库操作 和 HAL库操作。为了让你更全面地理解,我们分别看一下标准库和寄存器的实现方法。

方法一:标准外设库(最推荐新手)
这是正点原子教程中最经典的方式。库函数将底层的寄存器操作封装成了具有良好可读性的函数。

第一步:创建LED驱动文件

在工程目录的 HARDWARE 文件夹下,新建 led.c 和 led.h。

led.h 代码:



#ifndef __LED_H
#define __LED_H
#include "sys.h"

// 引脚别名定义,方便调用
#define LED0 PBout(5)  // DS0 连接 PB5
#define LED1 PEout(5)  // DS1 连接 PE5

void LED_Init(void);   // 初始化函数

#endif


led.c


#include "led.h"
#include "stm32f10x.h"

void LED_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    
    // 1. 打开时钟:GPIOB和GPIOE是挂载在APB2总线上的
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOE, ENABLE);
    
    // 2. 配置 PB5
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;    // 推挽输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;   // 速度50MHz
    GPIO_Init(GPIOB, &GPIO_InitStructure);
    
    // 3. 配置 PE5
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
    GPIO_Init(GPIOE, &GPIO_InitStructure);
    
    // 4. 默认熄灭所有LED(因为低电平亮,所以给高电平就灭了)
    GPIO_SetBits(GPIOB, GPIO_Pin_5);
    GPIO_SetBits(GPIOE, GPIO_Pin_5);
}
主函数 main.c——实现交替闪烁:


#include "led.h"
#include "delay.h"

int main(void)
{
    delay_init();      // 初始化延时函数(正点原子SYSTEM文件夹提供)
    LED_Init();      // 初始化LED引脚
    
    while(1)
    {
        // 效果:LED0亮,LED1灭
        GPIO_ResetBits(GPIOB, GPIO_Pin_5);  // PB5 置低电平,LED0亮
        GPIO_SetBits(GPIOE, GPIO_Pin_5);    // PE5 置高电平,LED1灭
        delay_ms(500);                       // 延时500ms
        
        // 效果:LED0灭,LED1亮
        GPIO_SetBits(GPIOB, GPIO_Pin_5);    // PB5 置高电平,LED0灭
        GPIO_ResetBits(GPIOE, GPIO_Pin_5);  // PE5 置低电平,LED1亮
        delay_ms(500);
    }
}
烧录代码后,你就会看到板子上的两个LED开始红蓝交替闪烁,仿佛在呼吸。

方法二:寄存器操作(了解底层)
如果你想知道库函数内部到底发生了什么,可以直接操作寄存器。

初始化代码(寄存器版):


void LED_Init(void)
{
    // 1. 使能时钟:RCC_APB2ENR 寄存器的第3位是GPIOB,第6位是GPIOE
    RCC->APB2ENR |= (1 << 3);   // 使能PORTB时钟
    RCC->APB2ENR |= (1 << 6);   // 使能PORTE时钟
    
    // 2. 配置GPIO模式:CRL寄存器控制低8位引脚
    // PB5 对应 CRL 的 20-23 位 (5 * 4 = 20)
    GPIOB->CRL &= ~(0xF << 20); // 先清除这4位
    GPIOB->CRL |= (3 << 20);    // 设置为通用推挽输出,50MHz (0011)
    
    // PE5 同理
    GPIOE->CRL &= ~(0xF << 20);
    GPIOE->CRL |= (3 << 20);
    
    // 3. 默认灭灯:通过 BSRR 寄存器或 ODR 寄存器
    GPIOB->BSRR = (1 << 5);     // 置高,灭灯
    GPIOE->BSRR = (1 << 5);
}
虽然寄存器代码看起来晦涩,但执行效率最高,能让你深刻理解《STM32参考手册》中那些寄存器的含义。

四、 进阶思考:从“点灯”到“优雅点灯”
灯亮了吗?亮了。但这只是开始。在实际的项目开发中,我们很少会直接在 main 函数的 while(1) 里写死延时,因为 delay_ms() 会阻塞CPU,在这500ms里,CPU什么也干不了。

如果你想让你的程序看起来更“工业级”,可以尝试以下进阶玩法:

使用定时器: 利用STM32的SysTick定时器或者通用定时器,产生一个10ms或1ms的中断。在中断中设置标志位,主循环查询标志位进行翻转。这样CPU在延时期间可以去处理其他任务。

引入状态机: 不要再用 GPIO_SetBits 和 GPIO_ResetBits 直接控制。定义一个 LED_State 变量,在定时中断里根据状态改变电平。这种思想在复杂的通信协议解析中非常实用。

尝试HAL库: 现在ST主推HAL库。使用STM32CubeMX工具,图形化配置时钟和引脚,直接生成代码。你只需要在 while(1) 里调用 HAL_GPIO_TogglePin 函数即可。代码的移植性会大大增强。

五、 总结
看着这块屏幕碎裂但依旧坚挺的战舰V3,不禁感慨。虽然现在STM32G4、H7系列早已成为主流,开发工具也从Keil逐渐过渡到CubeIDE,但Cortex-M3内核的F103依然是无数工程师心中的“经典”。通过点亮第一个LED,我们不仅学会了GPIO的输出控制,更重温了当初那份对底层硬件的探索热情。

对于嵌入式初学者来说,不要嫌弃开发板老旧,能跑通代码、能理解逻辑的板子就是好板子。如果你的手头也有这么一块吃灰的板子,不妨拿出来,接通电源,让它为你再闪烁一次。

SEO关键词: STM32F103、正点原子、战舰开发板、LED点灯、库函数、寄存器、嵌入式入门