我的stm32开发之路-实践篇-嵌入式的hello-world
我的stm32开发之路-实践篇-嵌入式的hello-world
前言
俗话说,万事开头难,可能当一个想接触嵌入式的开发者实现了第一个"hello-world"时才是真正的接触到入门的门槛。本文的目标就是跨过这个开头难,配置一个基于命令行的构建系统(使用GCC ARM工具链)和openocd来编译、烧录和调试一个LED点亮的项目。
本文改造了野火开发板配套的资料(请自行到 野火官方网站 下载STM32F103ZE_霸道开发板的《野火【STM32F103开发板-霸道】资料》)的点亮LED的项目配置。本文最大特点是真正从0开始指导新手开发者不使用Keil MDK将开发板上的LED灯点亮。因为嵌入式开发都是针对特定的物理设备,如果你的硬件设备与本人不同,且也想只使用命令行进行开发,也是可以参考本文中涉及的方法的,相信会有一定的帮助。
下面是本文的开发配置:
- 硬件: 野火stm32f103霸道v2开发板 + fileDAP(CMSIS-DAP设备)
- 操作系统: ubuntu24.04
- 初始工具: bash + vim
- 基础项目: 野火【STM32F103开发板-霸道】资料/0-开机例程源码/开机测试例程_20211130/开机测试例程/GPIO输出—使用固件库点亮LED灯/12-GPIO输出—使用固件库点亮LED灯
预备知识
术语
以下的术语有的并不会在此文中涉及,可后面自行了解。
ARM=Advanced RISC Machines
gcc=GNU compiler collectiono
gdb=GNU debugger
eabi=Embedded Application Binary Interface
openocd=Open On-Chip Debugger
CMSIS=Cortex Microcontroller Software Interface Standard CMSIS是ARM公司制定的统一软件开发框架
DAP=Debug Access Port 是硬件调试接口的具体实现,通过USB提供标准化的调试通道,支持SWD和JTAG两种协议
NXP=Next Experience
SWD=Serial Wire Debug
JTAG=Joint Test Action Group
SWDIO=Serial Wire Data Input/Output 是数据线
SWCLK=Serial Wire Clock 是时钟线
tcl=tool command language
srst=Synchronous Reset
arst=Asynchronous Reset
TCK=Test Clock
TMS=Test Mode Select
TDI=Test Data In
TDO=Test Data Out
DP=Debug Port ARM架构的调试接口(如SWD接口)的根寄存器组,包含控制、状态和识别寄存器
IDR=Identification Register 位于DP中的寄存器,用于读取目标设备的调试接口标识(如厂商、版本号等)
BSP=Board Support Package
关键概念对比
| 术语 | 物理位置 | 功能描述 | 示例 |
|---|---|---|---|
| Debugger | 开发PC上 | 调试控制软件 | OpenOCD/GDB |
| Probe | USB调试硬件 | 协议转换器 | CMSIS-DAP/J-Link |
| On-Chip | 目标处理器内部 | 芯片级调试硬件单元 | ARM CoreSight, JTAG TAP |
开发环境搭建
安装工具链
# ARM交叉编译工具链及openocd
sudo apt install gcc-arm-none-eabi binutils-arm-none-eabi gdb-multiarch openocd
物理连接(应有CMSIS-DAP设备)及检查DAP识别
DAP使用排线连接开发板,DAP使用usb连接PC,连接PC后DAP的绿灯会常亮。
通过在PC上敲入如下命令检查设备:
lsusb | grep -i "CMSIS"
从上面的输出中可获得由冒号分开的vid:pid,即vendor id和product id,这里先记录在纸上,后面会用到。
项目组织
目录结构
目录结构请根据我如下展示的目录结构自行调整,注意注释说明,会涉及将原目录下的一些文件进行替换。注意后面的注释,非常重要。
├── Libraries # 此目录中的文件应全部在STM32的官方标准库STM32F10x_StdPeriph_Lib_V3.5.0中寻找并放到对应的路径下。
│ ├── CMSIS # 核心,提供系统级支持,具有通用性。STM32 开发的核心部分
│ │ ├── core_cm3.c # ARM Cortex-M3 内核的核心支持文件。注意此文件版本在项目中有处源码bug需修改,即__STREXB和__STREXH函数里面的"=r"要改成"=&r",否则make时会报错Error:registers may not be the same--`strexbr0,r0,[r1]'
│ │ ├── core_cm3.h
│ │ ├── startup # 包含针对不同STM32F10x型号的启动文件,注意我们的项目使用官方的startup/gcc_ride7目录而不是startup/arm目录,否则make时会报错Error: junk at end of line, first unrecognized character is `*‘等
│ │ │ ├── startup_stm32f10x_cl.s
│ │ │ ├── startup_stm32f10x_hd.s # 本应用使用此汇编代码文件来启动芯片
│ │ │ ├── startup_stm32f10x_hd_vl.s
│ │ │ ├── startup_stm32f10x_ld.s
│ │ │ ├── startup_stm32f10x_ld_vl.s
│ │ │ ├── startup_stm32f10x_md.s
│ │ │ ├── startup_stm32f10x_md_vl.s
│ │ │ └── startup_stm32f10x_xl.s
│ │ ├── stm32f10x.h # STM32F10x 系列微控制器的主头文件
│ │ ├── system_stm32f10x.c # 系统初始化文件,SystemInit()函数在此
│ │ └── system_stm32f10x.h
│ └── FWlib # 驱动,用于外设访问,具有通用性。STM32 标准外设库(Standard Peripheral Library)的核心部分
│ ├── inc
│ │ ├── misc.h
│ │ ├── stm32f10x_adc.h
│ │ ├── stm32f10x_bkp.h
│ │ ├── stm32f10x_can.h
│ │ ├── stm32f10x_cec.h
│ │ ├── stm32f10x_crc.h
│ │ ├── stm32f10x_dac.h
│ │ ├── stm32f10x_dbgmcu.h
│ │ ├── stm32f10x_dma.h
│ │ ├── stm32f10x_exti.h
│ │ ├── stm32f10x_flash.h
│ │ ├── stm32f10x_fsmc.h
│ │ ├── stm32f10x_gpio.h
│ │ ├── stm32f10x_i2c.h
│ │ ├── stm32f10x_iwdg.h
│ │ ├── stm32f10x_pwr.h
│ │ ├── stm32f10x_rcc.h
│ │ ├── stm32f10x_rtc.h
│ │ ├── stm32f10x_sdio.h
│ │ ├── stm32f10x_spi.h
│ │ ├── stm32f10x_tim.h
│ │ ├── stm32f10x_usart.h
│ │ └── stm32f10x_wwdg.h
│ └── src
│ ├── misc.c
│ ├── stm32f10x_adc.c
│ ├── stm32f10x_bkp.c
│ ├── stm32f10x_can.c
│ ├── stm32f10x_cec.c
│ ├── stm32f10x_crc.c
│ ├── stm32f10x_dac.c
│ ├── stm32f10x_dbgmcu.c
│ ├── stm32f10x_dma.c
│ ├── stm32f10x_exti.c
│ ├── stm32f10x_flash.c
│ ├── stm32f10x_fsmc.c
│ ├── stm32f10x_gpio.c
│ ├── stm32f10x_i2c.c
│ ├── stm32f10x_iwdg.c
│ ├── stm32f10x_pwr.c
│ ├── stm32f10x_rcc.c
│ ├── stm32f10x_rtc.c
│ ├── stm32f10x_sdio.c
│ ├── stm32f10x_spi.c
│ ├── stm32f10x_tim.c
│ ├── stm32f10x_usart.c
│ └── stm32f10x_wwdg.c
├── Makefile # 构建系统的核心配置文件
├── openocd.cfg # OpenOCD 调试工具的配置文件,调试器配置
├── stm32_flash.ld # 链接器脚本文件,内存布局定义,芯片特定,确保程序正确加载到微控制器的存储器中
├── README.md
└── User # 应用,项目特定
├── led # BSP的LED驱动
│ ├── bsp_led.c # LED初始化、控制函数实现
│ └── bsp_led.h # LED控制函数声明及宏定义
├── main.c
├── stm32f10x_conf.h # 外设库配置文件,来自官方STM32F10x_StdPeriph_Lib_V3.5.0/Project/STM32F10x_StdPeriph_Template
├── stm32f10x_it.c # 中断服务程序(Interrupt Service Routines),来自官方STM32F10x_StdPeriph_Lib_V3.5.0/Project/STM32F10x_StdPeriph_Template
└── stm32f10x_it.h
配置OpenOCD支持DAP仿真器
下面为openocd.cfg的配置
# 配置调试探头(Debugger硬件接口),文件位置/usr/share/openocd/scripts/interface/cmsis-dap.cfg,可自行查看
source [find interface/cmsis-dap.cfg]
# 选择调试协议为SWD
transport select swd
# 指定调试器的USB VID/PID,
# 替换为 CMSIS-DAP 实际ID,使用lsusb根据实际情况配置,0x1fc9是NXP的VID
cmsis_dap_vid_pid 0x1fc9 0x5601
# 设置调试时钟速度,单位kHz
adapter speed 1000
# 配置On-Chip调试模块(目标芯片),文件位置/usr/share/openocd/scripts/target/stm32f1x.cfg,可自行查看
source [find target/stm32f1x.cfg]
# 复位配置,禁用硬件复位,使用软件复位
reset_config none
# 初始化调试会话
init
创建Makefile
# 目标名称
TARGET = BH-F103
# 工具链配置
CC = arm-none-eabi-gcc
AS = arm-none-eabi-as
LD = arm-none-eabi-ld
OBJCOPY = arm-none-eabi-objcopy
SIZE = arm-none-eabi-size
# MCU配置
MCU = -mcpu=cortex-m3 -mthumb
DEFS = -DSTM32F10X_HD -DUSE_STDPERIPH_DRIVER
# 包含路径
INCLUDES = -ILibraries/CMSIS \
-IUser \
-ILibraries/FWlib/inc \
-IUser/led
# 编译选项
CFLAGS = $(MCU) $(DEFS) $(INCLUDES) \
-Os -Wall -fdata-sections -ffunction-sections
# 链接脚本(创建新的链接脚本)
LDSCRIPT = stm32_flash.ld
LDFLAGS = $(MCU) -T$(LDSCRIPT) -Wl,--gc-sections \
-Wl,-Map=$(TARGET).map -specs=nano.specs
# 源文件
SRC_C = $(wildcard Libraries/CMSIS/*.c) \
$(wildcard Libraries/FWlib/src/*.c) \
User/main.c \
User/stm32f10x_it.c \
User/led/bsp_led.c
SRC_S = Libraries/CMSIS/startup/startup_stm32f10x_hd.s
# 构建目录
BUILD_DIR = build
OBJS = $(addprefix $(BUILD_DIR)/,$(notdir $(SRC_C:.c=.o))) \
$(addprefix $(BUILD_DIR)/,$(notdir $(SRC_S:.s=.o)))
vpath %.c $(sort $(dir $(SRC_C)))
vpath %.s $(sort $(dir $(SRC_S)))
all: $(BUILD_DIR)/$(TARGET).elf
$(BUILD_DIR)/%.o: %.c | $(BUILD_DIR)
$(CC) -c $(CFLAGS) $< -o $@
$(BUILD_DIR)/%.o: %.s | $(BUILD_DIR)
$(AS) $(MCU) $< -o $@
$(BUILD_DIR)/$(TARGET).elf: $(OBJS)
$(CC) $(LDFLAGS) $^ -o $@
$(SIZE) $@
$(BUILD_DIR):
mkdir -p $@
flash: $(BUILD_DIR)/$(TARGET).elf
openocd -f openocd.cfg -c "program $< verify reset exit"
clean:
rm -rf $(BUILD_DIR)
创建链接脚本stm32_flash.ld
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 64K
}
SECTIONS
{
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} >FLASH
.text :
{
. = ALIGN(4);
*(.text*)
*(.rodata*)
. = ALIGN(4);
} >FLASH
_sidata = .;
.data : AT(_sidata)
{
. = ALIGN(4);
_sdata = .;
*(.data*)
. = ALIGN(4);
_edata = .;
} >RAM
.bss :
{
. = ALIGN(4);
_sbss = .;
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
} >RAM
_estack = ORIGIN(RAM) + LENGTH(RAM);
}
其他重要代码
Libraries中的代码和User中的部分代码全是官方提供的,不需要我们自行编写,但User目录下中此应用重要的三个文件代码还是有必要放在这里展示的。
main.c文件
// main.c
#include "stm32f10x.h"
#include "bsp_led.h"
#define SOFT_DELAY Delay(0x0FFFFF);
void Delay(__IO u32 nCount);
int main(void)
{
LED_GPIO_Config();
while (1)
{
LED1_ON;
SOFT_DELAY;
LED1_OFF;
LED2_ON;
SOFT_DELAY;
LED2_OFF;
LED3_ON;
SOFT_DELAY;
LED3_OFF;
LED_RED;
SOFT_DELAY;
LED_GREEN;
SOFT_DELAY;
LED_BLUE;
SOFT_DELAY;
LED_YELLOW;
SOFT_DELAY;
LED_PURPLE;
SOFT_DELAY;
LED_CYAN;
SOFT_DELAY;
LED_WHITE;
SOFT_DELAY;
LED_RGBOFF;
SOFT_DELAY;
}
}
void Delay(__IO uint32_t nCount)
{
for(; nCount != 0; nCount--);
}
led/bsp_led.h文件
// led/bsp_led.h
#ifndef __LED_H
#define __LED_H
#include "stm32f10x.h"
#define LED1_GPIO_PORT GPIOB
#define LED1_GPIO_CLK RCC_APB2Periph_GPIOB
#define LED1_GPIO_PIN GPIO_Pin_5
#define LED2_GPIO_PORT GPIOB
#define LED2_GPIO_CLK RCC_APB2Periph_GPIOB
#define LED2_GPIO_PIN GPIO_Pin_0
#define LED3_GPIO_PORT GPIOB
#define LED3_GPIO_CLK RCC_APB2Periph_GPIOB
#define LED3_GPIO_PIN GPIO_Pin_1
#define ON 0
#define OFF 1
#define LED1(a) if (a) \
GPIO_SetBits(LED1_GPIO_PORT,LED1_GPIO_PIN);\
else \
GPIO_ResetBits(LED1_GPIO_PORT,LED1_GPIO_PIN)
#define LED2(a) if (a) \
GPIO_SetBits(LED2_GPIO_PORT,LED2_GPIO_PIN);\
else \
GPIO_ResetBits(LED2_GPIO_PORT,LED2_GPIO_PIN)
#define LED3(a) if (a) \
GPIO_SetBits(LED3_GPIO_PORT,LED3_GPIO_PIN);\
else \
GPIO_ResetBits(LED3_GPIO_PORT,LED3_GPIO_PIN)
#define digitalHi(p,i) {p->BSRR=i;}
#define digitalLo(p,i) {p->BRR=i;}
#define digitalToggle(p,i) {p->ODR ^=i;}
#define LED1_TOGGLE digitalToggle(LED1_GPIO_PORT,LED1_GPIO_PIN)
#define LED1_OFF digitalHi(LED1_GPIO_PORT,LED1_GPIO_PIN)
#define LED1_ON digitalLo(LED1_GPIO_PORT,LED1_GPIO_PIN)
#define LED2_TOGGLE digitalToggle(LED2_GPIO_PORT,LED2_GPIO_PIN)
#define LED2_OFF digitalHi(LED2_GPIO_PORT,LED2_GPIO_PIN)
#define LED2_ON digitalLo(LED2_GPIO_PORT,LED2_GPIO_PIN)
#define LED3_TOGGLE digitalToggle(LED3_GPIO_PORT,LED3_GPIO_PIN)
#define LED3_OFF digitalHi(LED3_GPIO_PORT,LED3_GPIO_PIN)
#define LED3_ON digitalLo(LED3_GPIO_PORT,LED3_GPIO_PIN)
#define LED_RED \
LED1_ON;\
LED2_OFF\
LED3_OFF
#define LED_GREEN \
LED1_OFF;\
LED2_ON\
LED3_OFF
#define LED_BLUE \
LED1_OFF;\
LED2_OFF\
LED3_ON
#define LED_YELLOW \
LED1_ON;\
LED2_ON\
LED3_OFF
#define LED_PURPLE \
LED1_ON;\
LED2_OFF\
LED3_ON
#define LED_CYAN \
LED1_OFF;\
LED2_ON\
LED3_ON
#define LED_WHITE \
LED1_ON;\
LED2_ON\
LED3_ON
#define LED_RGBOFF \
LED1_OFF;\
LED2_OFF\
LED3_OFF
void LED_GPIO_Config(void);
#endif /* __LED_H */
led/bsp_led.c文件
// led/bsp_led.c
#include "bsp_led.h"
void LED_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd( LED1_GPIO_CLK | LED2_GPIO_CLK | LED3_GPIO_CLK, ENABLE);
GPIO_InitStructure.GPIO_Pin = LED1_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(LED1_GPIO_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = LED2_GPIO_PIN;
GPIO_Init(LED2_GPIO_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = LED3_GPIO_PIN;
GPIO_Init(LED3_GPIO_PORT, &GPIO_InitStructure);
GPIO_SetBits(LED1_GPIO_PORT, LED1_GPIO_PIN);
GPIO_SetBits(LED2_GPIO_PORT, LED2_GPIO_PIN);
GPIO_SetBits(LED3_GPIO_PORT, LED3_GPIO_PIN);
}
编译及烧录
编译程序
make后会生成一个elf文件
make clean
make
在一个单独的终端启动OpenOCD以烧录及调试
openocd -f openocd.cfg
烧录到flash
可以通过telnet或gdb两种方法国进行烧录
两种方法的对比
| 特性 | Telnet | GDB |
|---|---|---|
| 适用场景 | 快速烧录、简单调试(无需源码级调试)。 | 源码级调试(设置断点、单步执行、查看变量等)。 |
| 操作复杂度 | 简单,直接输入命令(如 flash write_image)。 | 稍复杂,需通过 GDB 客户端连接并加载固件。 |
| 灵活性 | 仅支持 OpenOCD 提供的命令(如擦除、烧录、复位)。 | 支持 GDB 调试命令 + OpenOCD 的 Telnet 命令(通过 monitor 执行)。 |
| 是否需要固件文件 | 需要 .bin 或 .elf 文件。 | 需要 .elf 文件(包含调试信息)。 |
| 是否需要调试器支持 | 是(需配置调试器接口,如 ST-Link、J-Link)。 | 是(需调试器支持 GDB 协议)。 |
本文选择gdb的方法进行烧录
新开一个终端使用GDB烧录
- 启动GDB并连接openocd:
gdb-multiarch build/BH-F103.elf
- 烧录固件,在GDB中执行:
target extended-remote :3333 # 连接到openocd的gdb服务
monitor reset halt # 复位并暂停目标设备
load # 加载.elf文件到目标设备,load命令会自动erase Flash并烧录.elf文件
monitor reset run # 这一步执行后LED灯就会点亮
detach
quit
若需手动控制烧录地址,可使用flash write_image(通过monitor命令)
总结
本文详细介绍了如何只使用命令行来点亮一个LED灯,可供感兴趣的朋友参考。
3405

被折叠的 条评论
为什么被折叠?



