《 万事开头难 》
在嵌入式开发的世界里,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点灯、库函数、寄存器、嵌入式入门
评论 (0)
登录后即可发表评论