战损版的坚守:STM32F103正点原子开发板入门——点亮人生第一盏LED
关键词: STM32F103,LED点灯,库函数,寄存器,嵌入式入门,手把手实战
翻出吃灰多年的正点原子战损版STM32开发板,手把手带你写第一个程序点亮LED。包含标准库代码、寄存器操作和硬件原理分析,嵌入式入门必看
=======《 万事开头难》=======
在嵌入式开发的世界里,STM32 是一个绕不开的名字,而正点原子 则是无数中国开发者入门的“领路人”。最近,我从杂物箱底翻出了一块吃灰多年的“战损版”正点原子战舰V3开发板。屏幕碎了,螺丝生锈了,但当我接通电源,那颗红色的电源指示灯依然亮起时,突然有种莫名的感动。
这么多年过去了,它竟然还能用。
这或许就是硬件工程师的浪漫:只要代码不错,硬件不坏,它就永远忠于你的逻辑。今天,我不想让它继续吃灰,决定用它来写下第一个程序——点亮LED指示灯。这不仅是嵌入式世界的“Hello World”,也是对这块老将的一次致敬。
一、 准备工作:唤醒沉睡的“战舰”
虽然我的板子是战损成色,但正点原子的设计质量确实过硬。如果你手头有任何型号的STM32F103开发板(无论是战舰、精英还是Mini),都可以按照今天的步骤操作。
我们需要准备:
硬件:
- STM32F103 开发板(正点原子系列均可,我的是战舰V3)。
- ST-Link 或 J-Link 仿真器/下载器(用于烧录程序)。
- Micro USB 线(用于供电或串口通信,供电也可以用配套的电源直接供电)。
软件:
- 开发环境: Keil MDK(或者STM32CubeIDE,这里我们使用Keil MDK)。点击这里进入官方下载页面。
- 固件库: STM32F1xx的标准外设库(或者HAL库,今天我们以标准库为例,这也是正点原子教程的经典配置)。
- 参考资料: 开发板的原理图PDF(这是最重要的,后面找引脚全靠它)。
软件的具体安装步骤这里就直接略过,如果你安装的是版本比较高比如Version 5.43a,可能会出现下面的对话框:
上面的名称类似于这种:D:\Users\Administrator\AppData\Local\Keil_v5\ARM\cmsis-toolbox\bin\cpackget.exe
它的任务是在后台帮你自动下载和安装各种芯片厂商(如ST、NXP)的支持包。如果下载特别慢的话,可以直接关掉,不影响软件的正常安装。不过不是什么特殊的原因尽量不要关闭,后续安装也比较麻烦。
安装完成后打开软件时出现下面的错误提示:
这个错误提示 >>> TOOLS.INI - Section '[ARM]': missing 'PATH' entry ! <<< 说明 Keil 安装目录下的 TOOLS.INI 配置文件出了问题,它里面关于 ARM 编译器的路径信息丢失了或格式不对。
解决方法:手动修复 TOOLS.INI 文件
打开你的 Keil 安装根目录,通常默认路径是:
C:\Users\Administrator\AppData\Local\Keil_v5 或
D:\Users\Administrator\AppData\Local\Keil_v5。
在该文件夹下,找到名为 TOOLS.INI 的文件。
备份文件(重要)
在修改前,建议你复制一份这个文件,粘贴到桌面或文件夹里作为备份。万一改错了可以还原。
用记事本打开并编辑
右键点击 TOOLS.INI,选择用记事本打开。
[UV2]
ORGANIZATION="1"
NAME="1", "1"
EMAIL="1"
ARMSEL=1
USERTE=1
TOOL_VARIANT=mdk_std
RTEPATH="D:\Users\Administrator\AppData\Local\Arm\Packs"
CMSIS_TOOLBOX=D:\Users\Administrator\AppData\Local\Keil_v5\ARM\cmsis-toolbox
[ARM]
LIC0=X96CK-ZI184-BSAYX-HHI0A-N30T8-50MF7
在文件中找到 [ARM] 开头的这一段。如果找不到,也可以直接拉到文件末尾附近看看。
检查并修改:确保 [ARM] 下面有一行 PATH 语句,并且路径指向你的 ARM 编译器文件夹。
正确的格式应该是:PATH="D:\Users\Administrator\AppData\Local\Keil_v5"(请将路径替换为你自己的安装路径)。
举个例子,如果你的 Keil 安装在 D 盘,这一段看起来应该是这样:
[UV2]
ORGANIZATION="1"
NAME="1", "1"
EMAIL="1"
ARMSEL=1
USERTE=1
TOOL_VARIANT=mdk_std
RTEPATH="D:\Users\Administrator\AppData\Local\Arm\Packs"
CMSIS_TOOLBOX=D:\Users\Administrator\AppData\Local\Keil_v5\ARM\cmsis-toolbox
[ARM]
PATH="D:\Users\Administrator\AppData\Local\Keil_v5"
VERSION=5.43a
LIC0=X96CK-ZI184-BSAYX-HHI0A-N30T8-50MF7
删除多余的配置节(如果有)
有些第三方软件(如某些仿真工具)可能会在文件末尾或别处添加一个错误的配置节,比如 [KARM] 或 [undefined toolset],这也会导致冲突。
如果看到这些不属于 [ARM] 或 [C51] 的奇怪段落,可以放心地将其整段(从[到下一空行前)删除。
保存并重启 Keil
点击记事本的 文件 -> 保存,然后关闭记事本。
再次双击打开 Keil uVision,错误提示应该就消失了。
随便打开一个项目测试时,编译提示:
Rebuild started: Project: PodControlGroundEnd
*** Target 'Target 1' uses ARM-Compiler 'Default Compiler Version 5' which is not available.
*** Please review the installed ARM Compiler Versions:
'Manage Project Items - Folders/Extensions' to manage ARM Compiler Versions.
'Options for Target - Target' to select an ARM Compiler Version for the target.
*** Rebuild aborted.
Build Time Elapsed: 00:00:00
这部分处理方法由于有太多图片,放到这里就分不清主次了,放到了另一个链接里,点击这里查看。
二、 硬件分析:LED在哪里?
任何程序的第一步,都不是写代码,而是看原理图。
打开正点原子战舰V3的原理图,找到LED相关的部分。你会发现,板上通常会集成两个LED:
- DS0: 通常连接在 PB5 引脚上。
- DS1: 通常连接在 PE5 引脚上。
关键点分析:
查看电路连接,我们会发现这些LED的另一端是接在 VCC 3.3V 上的。这意味着什么呢?
- 如果单片机引脚输出 低电平(0V),电流从VCC流过LED到达引脚,形成回路,灯亮。
- 如果单片机引脚输出 高电平(3.3V),两端电压相等,灯灭。
- 这就是常说的“低电平有效”。弄清楚这个逻辑,代码的方向就不会错。
三、 动手写码:三种方式实现“点灯”
在STM32开发中,我们通常有三种方式操作寄存器:寄存器操作、标准库操作 和 HAL库操作。为了让你更全面地理解,我们分别看一下标准库和寄存器的实现方法。
方法一:标准外设库(最推荐新手)
这是正点原子教程中最经典的方式。库函数将底层的寄存器操作封装成了具有良好可读性的函数。
第一步:创建LED驱动文件
在工程目录的 HARDWARE 文件夹下,新建 gpio.c 和 gpio.h。
gpio.h代码:
#ifndef _GPIO_H_
#define _GPIO_H_
#include"stm32f10x_gpio.h"
#define PA1 GPIOA->BSRR
#define PA0 GPIOA->BRR
#define GPIOA_ODR_A (GPIOA_BASE+0x0c)
#define GPIOA_IDR_A (GPIOA_BASE+0x08)
#define GPIOB_ODR_A (GPIOB_BASE+0x0c)
#define GPIOB_IDR_A (GPIOB_BASE+0x08)
#define GPIOC_ODR_A (GPIOC_BASE+0x0c)
#define GPIOC_IDR_A (GPIOC_BASE+0x08)
#define GPIOD_ODR_A (GPIOD_BASE+0x0c)
#define GPIOD_IDR_A (GPIOD_BASE+0x08)
#define GPIOE_ODR_A (GPIOE_BASE+0x0c)
#define GPIOE_IDR_A (GPIOE_BASE+0x08)
#define GPIOF_ODR_A (GPIOF_BASE+0x0c)
#define GPIOF_IDR_A (GPIOF_BASE+0x08)
#define GPIOG_ODR_A (GPIOG_BASE+0x0c)
#define GPIOG_IDR_A (GPIOG_BASE+0x08)
#define BitBand(Addr,BitNum) *((volatile unsigned long *)((Addr&0xF0000000)+0x2000000+((Addr&0xfffff)<<5)+(BitNum<<2)))
#define PAout(n) BitBand(GPIOA_ODR_A,n)
#define PAin(n) BitBand(GPIOA_IDR_A,n)
#define PBout(n) BitBand(GPIOB_ODR_A,n)
#define PBin(n) BitBand(GPIOB_IDR_A,n)
#define PCout(n) BitBand(GPIOC_ODR_A,n)
#define PCin(n) BitBand(GPIOC_IDR_A,n)
#define PDout(n) BitBand(GPIOD_ODR_A,n)
#define PDin(n) BitBand(GPIOD_IDR_A,n)
#define PEout(n) BitBand(GPIOE_ODR_A,n)
#define PEin(n) BitBand(GPIOE_IDR_A,n)
#define PFout(n) BitBand(GPIOF_ODR_A,n)
#define PFin(n) BitBand(GPIOF_IDR_A,n)
#define PGout(n) BitBand(GPIOG_ODR_A,n)
#define PGin(n) BitBand(GPIOG_IDR_A,n)
// 引脚别名定义,方便调用
#define LED0 PBout(5) // DS0 连接 PB5
#define LED1 PEout(5) // DS1 连接 PE5
void LED_Init(void);
#endifgpio.c代码:
#include"gpio.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);
}新建 delay.c 和 delay.h。
delay.h代码:
#ifndef __DELAY_H
#define __DELAY_H
#include "gpio.h"
void delay_init(void);
void delay_ms(u16 nms);
void delay_us(u32 nus);
#endifdelay.c代码:#include "delay.h"
static u8 fac_us=0; //us延时倍乘数
static u16 fac_ms=0; //ms延时倍乘数,在ucos下,代表每个节拍的ms数
//初始化延迟函数
//当使用OS的时候,此函数会初始化OS的时钟节拍
//SYSTICK的时钟固定为HCLK时钟的1/8
//SYSCLK:系统时钟
void delay_init()
{
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); //选择外部时钟 HCLK/8
fac_us=SystemCoreClock/8000000; //为系统时钟的1/8
fac_ms=(u16)fac_us*1000; //每个ms需要的systick时钟数
}
//延时nus
//nus为要延时的us数.
void delay_us(u32 nus)
{
u32 temp;
SysTick->LOAD=nus*fac_us; //时间加载
SysTick->VAL=0x00; //清空计数器
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ; //开始倒数
do
{
temp=SysTick->CTRL;
}while((temp&0x01)&&!(temp&(1<<16))); //等待时间到达
SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk; //关闭计数器
SysTick->VAL =0X00; //清空计数器
}
//延时nms
//注意nms的范围
//SysTick->LOAD为24位寄存器,所以,最大延时为:
//nms<=0xffffff*8*1000/SYSCLK
//SYSCLK单位为Hz,nms单位为ms
//对72M条件下,nms<=1864
void delay_ms(u16 nms)
{
u32 temp;
SysTick->LOAD=(u32)nms*fac_ms; //时间加载(SysTick->LOAD为24bit)
SysTick->VAL =0x00; //清空计数器
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ; //开始倒数
do
{
temp=SysTick->CTRL;
}while((temp&0x01)&&!(temp&(1<<16))); //等待时间到达
SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk; //关闭计数器
SysTick->VAL =0X00; //清空计数器
} #include "delay.h"
#include "gpio.h"
int main(void)
{
delay_init(); // 初始化延时函数
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(300); // 延时300ms
// 效果:LED0灭,LED1亮
GPIO_SetBits(GPIOB, GPIO_Pin_5); // PB5 置高电平,LED0灭
GPIO_ResetBits(GPIOE, GPIO_Pin_5); // PE5 置低电平,LED1亮
delay_ms(300);
}
}或用位绑定(位带)方式实现
#include "delay.h"
#include "gpio.h"
int main(void)
{
delay_init(); //延时函数初始化
LED_Init(); //初始化与LED连接的硬件接口
while(1)
{
LED0=0;
LED1=1;
delay_ms(300); //延时300ms
LED0=1;
LED1=0;
delay_ms(300); //延时300ms
}
}程序编写好后,接下来就是将程序下载进开发板。这里用STLINK下载,用到四根线2、4、7、9。下面这个引脚定义图,纯手工绘制。
这里供电是有USB供电,所以STILINK中的3.3V就没有连接,完全有USB供电。GND取的是离其他线最近的一个,三根线放到一起,这样连接会比较牢固,不易脱落,实际连接的引脚是7、8、9。
编译下载后,你将看到两个LED开始交替闪烁。恭喜!你已经迈出了嵌入式开发的第一步。
方法二:寄存器操作(了解底层)
如果你想知道库函数内部到底发生了什么,可以直接操作寄存器。
初始化代码(寄存器版):
#include"gpio.h"
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的输出控制,更重温了当初那份对底层硬件的探索热情。
对于嵌入式初学者来说,不要嫌弃开发板老旧,能跑通代码、能理解逻辑的板子就是好板子。如果你的手头也有这么一块吃灰的板子,不妨拿出来,接通电源,让它为你再闪烁一次。
评论 (0)
登录后即可发表评论