BITECE实习之STM32
一、配置环境
1、IDE安装
我采用JetBrian开发的Clion进行开发,该软件对非商业用途免费,进入官网CLion
Free for non-commercial use即可直接下载安装。
2、CubeMX与CubeCLT
我采用keysking开发的FubeMX协助进行环境安装。首先安装FubeMX,目录随意。安装完成后打开:

从此处先安装STM32CubeMX,也可以在官网进行下载。下载好安装包后按默认路径安装,一路确定即可。
之后继续从此处安装STM32CubeCLT,与上述操作一样地,下载好安装包后按默认路径安装,一路确定即可。
此处安装可以参考爽!手把手教你用CLion开发STM32【大人,时代变啦!!!】,只需参考CubeMX与CubeCLT安装部分即可。
3、OpenOCD安装
OpenOCD则在官网Download OpenOCD for Windows下载并安装,同样下载好安装包后按默认路径安装,一路确定即可。
4、Clion配置
打开Clion:

点击设置(Settings):

之后按照安装路径参考图片进行配置:

注意选择编译器时,使用arm-none-eabi-gcc.exe和,arm-none-eabi-g++.exe。
之后修改CMake配置:

然后记住安装上述三个工具的路径,配置嵌入式开发环境:

最后在高级设置中启用调试器:

此处参考文献:
爽!手把手教你用CLion开发STM32【大人,时代变啦!!!】 - bilibili
【教程】配置 CLion 优雅开发 STM32 - 略无慕艳意 - 博客园
但要注意,上述配置过程中会遇到一些问题,需要参考本文后续叠加起来综合使用。
二、新建项目:
首先在Clion中点击新建嵌入式的项目:

注意不要直接新建,先点击启动STM32CubeMX,然后点击Start My project from MCU:

由于学校下发的材料为基于STM32F103VET6的野火指南者开发板,输入芯片规格,选中芯片后点击Start Project:

按照如图方式选择调试模式,注意一定要选择Serial Wire:

选中后可以发现左侧GPIO标识变绿。最后设置项目名和工具链,注意工具链一定选择CMake:

最后点击右上角GENERATE CORE即可:
记住路径后关闭窗口,回到Clion,将路径输入,可以发现此时可以创建项目了:

配置好Debug和Release两个CMake工具:

在此处点编辑配置:

进行OpenOCD配置:

注意此处要自己在任意位置新建一个.cfg文件,里面输入:
# 野火FireDAP仿真器配置(基于CMSIS-DAP)
source [find interface/cmsis-dap.cfg]
# 选择SWD接口
transport select swd
# 通信速率(1000kHz,兼容大多数情况)
adapter speed 1000
# 目标芯片配置
source [find target/stm32f1x.cfg]
# STM32F103VET6 Flash配置(512KB)
set FLASH_SIZE 0x80000
# 复位配置(关键:启用系统复位)
reset_config srst_only srst_nogate
# 核心逻辑:利用before_init回调,在init执行前强制配置端口
# 解决CLion命令顺序导致的"tcl port must be before init"错误
proc before_init {} {
# 禁用tcl和gdb端口(使用正确的语法:tcl port 而非 tcl_port)
tcl port disabled
gdb port disabled
}
最后点击确定即可。
代码在Core目录下,其中Src存放.c或.cpp文件,Inc存放.h文件:

此时如果连接开发板,点击绿色三角进行运行,若控制台显示如下良好,则表示配置完成。
[0mOpen On-Chip Debugger 0.12.0 (2024-09-16)
. [https://github.com/sysprogs/openocd]
Licensed under GNU GPL v2
libusb1 d52e355daa09f17ce64819122cb067b8a2ee0d4b
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
before_init
DEPRECATED! use 'tcl port' not 'tcl_port'
DEPRECATED! use 'gdb port', not 'gdb_port'
DEPRECATED! use 'tcl port' not 'tcl_port'
[0mInfo : CMSIS-DAP: SWD supported
Info : CMSIS-DAP: Atomic commands supported
Info : CMSIS-DAP: Test domain timer supported
Info : CMSIS-DAP: FW Version = 2.0.0
Info : CMSIS-DAP: Interface Initialised (SWD)
Info : SWCLK/TCK = 1 SWDIO/TMS = 1 TDI = 0 TDO = 1 nTRST = 0 nRESET = 1
Info : CMSIS-DAP: Interface ready
Info : clock speed 1000 kHz
Info : SWD DPIDR 0x1ba01477
Info : [stm32f1x.cpu] Cortex-M3 r1p1 processor detected
Info : [stm32f1x.cpu] target has 6 breakpoints, 4 watchpoints
Info : [stm32f1x.cpu] Examination succeed
Info : [stm32f1x.cpu] gdb port disabled
[stm32f1x.cpu] halted due to breakpoint, current mode: Thread
xPSR: 0x01000000 pc: 0x0800219c msp: 0x20010000
** Programming Started **
Info : device id = 0x10036414
Info : flash size = 512 KiB
Warn : Adding extra erase range, 0x080031ac .. 0x080037ff
** Programming Finished **
shutdown command invoked
三、开发
本节中左右代码已上传至Github,仓库地址https://github.com/lamaper/BIT_ECE_STM32。
本节中所有作业来自上课课件,课件已经上传至BIT101仓库,本课程编号:100120050。
本节中采用的硬件是学校派发的野火指南者开发板,本文采用HAL库开发而不是标准库。硬件官方参考文档:[野火]STM32 HAL库开发实战指南-基于F103系列开发板—文档,其中给出了开发板外设地址:
/*片上外设基地址 */
#define PERIPH_BASE ((unsigned int)0x40000000)
/*总线基地址,GPIO都挂载到APB2上 */
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
/*GPIOB外设基地址*/
#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)
/* GPIOB寄存器地址,强制转换成指针 */
#define GPIOA_CRL *(unsigned int*)(GPIOA_BASE+0x00)
#define GPIOA_CRH *(unsigned int*)(GPIOA_BASE+0x04)
#define GPIOA_IDR *(unsigned int*)(GPIOA_BASE+0x08)
#define GPIOA_ODR *(unsigned int*)(GPIOA_BASE+0x0C)
#define GPIOA_BSRR *(unsigned int*)(GPIOA_BASE+0x10)
#define GPIOA_BRR *(unsigned int*)(GPIOA_BASE+0x14)
#define GPIOA_LCKR *(unsigned int*)(GPIOA_BASE+0x18)
/*RCC外设基地址*/
#define RCC_BASE (AHBPERIPH_BASE + 0x1000)
/*RCC的AHB1时钟使能寄存器地址,强制转换成指针*/
#define RCC_APB2ENR *(unsigned int*)(RCC_BASE+0x18)
作业-寄存器亮灯
作业内容:使用相应软件操作STM32开发板,用GPIO控制LED发光,用寄存器方式,分别地发射绿光、蓝光,用寄存器方式,控制LED发射黄光、紫光、白光,在黄、紫、白光中任选一种,提交工程压缩包。
首先查询官方操作手册,获取不同颜色LED灯的针脚信息以操作其发出不同颜色的光,创建文件led.h用于记录宏,之后创建LED控制宏以确保LED正常亮灭、定义一些快速操作的宏,方便后面快速调用颜色:
#ifndef __LED_H
#define __LED_H
/**
* @author lamaper(Guo Jun Qi 1120241725)
*/
#include "main.h"
/* ---------- LED引脚与时钟定义 ---------- */
// 红色LED(LED1)
#define LED1_GPIO_PORT GPIOB
#define LED1_GPIO_PIN GPIO_PIN_5
#define LED1_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
// 绿色LED(LED2)
#define LED2_GPIO_PORT GPIOB
#define LED2_GPIO_PIN GPIO_PIN_0
#define LED2_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
// 蓝色LED(LED3)
#define LED3_GPIO_PORT GPIOB
#define LED3_GPIO_PIN GPIO_PIN_1
#define LED3_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
/* ---------- LED控制宏(低电平点亮) ---------- */
#define LED_ON GPIO_PIN_RESET // 低电平点亮
#define LED_OFF GPIO_PIN_SET // 高电平熄灭
// 基础控制宏
#define LED1_SetState(state) HAL_GPIO_WritePin(LED1_GPIO_PORT, LED1_GPIO_PIN, (state))
#define LED2_SetState(state) HAL_GPIO_WritePin(LED2_GPIO_PORT, LED2_GPIO_PIN, (state))
#define LED3_SetState(state) HAL_GPIO_WritePin(LED3_GPIO_PORT, LED3_GPIO_PIN, (state))
// 快捷操作宏
#define LED1_On() LED1_SetState(LED_ON)
#define LED1_Off() LED1_SetState(LED_OFF)
#define LED1_Toggle() HAL_GPIO_TogglePin(LED1_GPIO_PORT, LED1_GPIO_PIN)
#define LED2_On() LED2_SetState(LED_ON)
#define LED2_Off() LED2_SetState(LED_OFF)
#define LED2_Toggle() HAL_GPIO_TogglePin(LED2_GPIO_PORT, LED2_GPIO_PIN)
#define LED3_On() LED3_SetState(LED_ON)
#define LED3_Off() LED3_SetState(LED_OFF)
#define LED3_Toggle() HAL_GPIO_TogglePin(LED3_GPIO_PORT, LED3_GPIO_PIN)
/* ---------- 组合颜色宏 ---------- */
#define LED_Red() do { LED1_On(); LED2_Off(); LED3_Off(); } while(0)
#define LED_Green() do { LED1_Off(); LED2_On(); LED3_Off(); } while(0)
#define LED_Blue() do { LED1_Off(); LED2_Off(); LED3_On(); } while(0)
#define LED_Yellow() do { LED1_On(); LED2_On(); LED3_Off(); } while(0)
#define LED_Purple() do { LED1_On(); LED2_Off(); LED3_On(); } while(0)
#define LED_Cyan() do { LED1_Off(); LED2_On(); LED3_On(); } while(0)
#define LED_White() do { LED1_On(); LED2_On(); LED3_On(); } while(0)
#define LED_RGBOff() do { LED1_Off(); LED2_Off(); LED3_Off(); } while(0)
/* ---------- 函数声明 ---------- */
void LED_Init(void);
#endif /* __LED_H */
在led.c中编辑初始化LED的函数:
#include "led.h"
/**
* @author lamaper (Guo Jun Qi 1120241725)
* @brief 初始化LED对应的GPIO
* @note 配置为推挽输出、无上下拉、低速
*/
void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能GPIOB时钟
LED1_CLK_ENABLE();
LED2_CLK_ENABLE();
LED3_CLK_ENABLE();
// 配置GPIO为推挽输出
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
// 配置LED1引脚
GPIO_InitStruct.Pin = LED1_GPIO_PIN;
HAL_GPIO_Init(LED1_GPIO_PORT, &GPIO_InitStruct);
// 配置LED2引脚
GPIO_InitStruct.Pin = LED2_GPIO_PIN;
HAL_GPIO_Init(LED2_GPIO_PORT, &GPIO_InitStruct);
// 配置LED3引脚
GPIO_InitStruct.Pin = LED3_GPIO_PIN;
HAL_GPIO_Init(LED3_GPIO_PORT, &GPIO_InitStruct);
// 初始状态:所有LED熄灭
LED_RGBOff();
}
之后就可以在main函数中配置基础逻辑了:
void HW1_SpmLight(void)// 用寄存器的方式,分别地发射绿光、蓝光
{
LED_Green(); HAL_Delay(500); // 绿灯亮500ms
LED_Blue(); HAL_Delay(500); // 蓝灯亮500ms
}
void HW1_MixLight(void) // 用寄存器的方式,控制LED发射黄光、紫光、白光
{
LED_Yellow(); HAL_Delay(500); // 黄灯亮500ms
LED_Purple(); HAL_Delay(500); // 紫灯亮500ms
LED_White(); HAL_Delay(500); // 白灯亮500ms
}
void HW1_Submit(void) // 选择黄光、紫光、白光的任意一种提交工程压缩包
{
LED_Yellow(); // 黄灯长亮
}
编译运行后可以看到结果:

作业-按键与亮灯
作业内容:使用相应软件操作STM32开发板,使得按KEY1,控制LED灯在“红光-绿光-蓝光-白光”四种方式之间切换按KEY2,控制LED灯熄灭。
首先查询官方操作手册,获取KEY1和KEY2的针脚信息以操作其发出不同颜色的光,创建文件key.h用于记录宏:
#ifndef __KEY_H
#define __KEY_H
#include "main.h"
/* 按键引脚定义(野火指南者开发板) */
// KEY1 -> PC13
#define KEY1_GPIO_PORT GPIOC
#define KEY1_GPIO_PIN GPIO_PIN_13
#define KEY1_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE()
// KEY2 -> PA0
#define KEY2_GPIO_PORT GPIOA
#define KEY2_GPIO_PIN GPIO_PIN_0
#define KEY2_CLK_ENABLE() __HAL_RCC_GPIOC_CLK_ENABLE()
/* 函数声明 */
void KEY_Init(void);
uint8_t KEY_Scan(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
#endif /* __KEY_H */
单片机中按键检测并没有被封装,因而需要我们自行实现按下的逻辑检测。按键机械触点断开、闭合时,由于触点的弹性作用,按键开关不会马上稳定接通或一下子断开,使用按键时会产生带波纹信号,需要用软件消抖处理滤波,不方便输入检测。

本此实验采用的野火STM32指南者开发板(STM32F103VET6)板所搭载的按键带硬件消抖功能,它利用电容充放电的延时,消除了波纹,从而简化软件的处理,软件只需要直接检测引脚的电平即可。

从按键的原理图可知,这些按键在没有被按下的时候,GPIO引脚的输入状态为低电平;当按键按下时,GPIO引脚的输入状态为高电平。只要我们检测引脚的输入电平,即可判断按键是否被按下。
基于此在后续创建key.c控制按键识别,其中KEY_Init函数用来初始化按键,用KEY_Scan函数检测按钮状态,虽然硬件自带抖动消除,但是这里为了练习,使用HAL库的延时函数:
#include "key.h"
/**
* @brief 初始化按键GPIO
*/
void KEY_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能按键对应GPIO时钟
KEY1_CLK_ENABLE();
KEY2_CLK_ENABLE();
// 配置KEY1(PA0,上拉输入)
GPIO_InitStruct.Pin = KEY1_GPIO_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP; // 上拉输入,按键按下为低电平
HAL_GPIO_Init(KEY1_GPIO_PORT, &GPIO_InitStruct);
// 配置KEY2(PC13,上拉输入)
GPIO_InitStruct.Pin = KEY2_GPIO_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP; // 上拉输入,按键按下为低电平
HAL_GPIO_Init(KEY2_GPIO_PORT, &GPIO_InitStruct);
}
/**
* @brief 按键扫描(带消抖,使用HAL_Delay)
* @param GPIOx: 按键GPIO端口
* @param GPIO_Pin: 按键GPIO引脚
* @retval 1: 按键按下(已消抖),0: 未按下
*/
uint8_t KEY_Scan(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
static uint8_t key_up = 1; // 按键松开标志
if (key_up && (HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_RESET))
{
HAL_Delay(20); // 消抖延时(使用HAL库延迟)
if (HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_RESET)
{
key_up = 0; // 标记按键按下
return 1; // 返回按下状态
}
}
else if (HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_SET)
{
HAL_Delay(20); // 消抖延时
if (HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_SET)
{
key_up = 1; // 标记按键松开
}
}
return 0; // 未按下
}
之后就可以在main函数中配置基础逻辑了。
void HW2(void) {
if (mode == 0) {
LED_RGBOff();
mode += 1;
}
// 作业2:KEY1切换模式(红光→绿光→蓝光→白光循环)
if (KEY_Scan(KEY1_GPIO_PORT, KEY1_GPIO_PIN) == 1)
{
current_mode++;
if (current_mode >= 4) // 超过白光模式(3)则回到红光(0)
current_mode = 0;
// 根据当前模式切换LED(直接使用led.h中的宏)
switch(current_mode)
{
case 0: LED_Red();HAL_Delay(200); break;
case 1: LED_Green();HAL_Delay(200); break;
case 2: LED_Blue();HAL_Delay(200); break;
case 3: LED_White();HAL_Delay(200); break;
default: LED_RGBOff();break;
}
}
// 作业2:KEY2熄灭LED
if (KEY_Scan(KEY2_GPIO_PORT, KEY2_GPIO_PIN) == 1)
{
current_mode = 4; // 标记为熄灭模式
LED_RGBOff();
}
HAL_Delay(10); // 消抖延时
}
作业-蜂鸣器
作业内容:使用相应软件操作STM32开发板,写一个函数,使用SysTick方法,计时0.25s使蜂鸣器产生n次短鸣+1长鸣,n=mod(学号末位)+1短鸣的时间为0.25s,长鸣时间为1s,每次鸣响之间间隔1s蜂鸣响起的同时,红色LED灯同时亮起。
我的学号末尾是5,后面采用5。
首先查询官方操作手册,获取蜂鸣器的针脚信息,创建文件beep.h用于记录宏,通过查询该开发板的技术手册可以得知,这块开发板采用有源蜂鸣器。STM32 驱动蜂鸣器的核心原理,是通过GPIO 引脚输出控制信号,配合蜂鸣器自身的发声结构,最终将电信号转化为声音信号。对于有源蜂鸣器,其内部自带振荡电路且包含芯片,只需GPIO输出高低电平——通电响、断电停——即可使其发声。基于此,定义如下宏以方便开发:
#ifndef __BEEP_H
#define __BEEP_H
#include "main.h"
#include "led.h"
// 用于控制红色LED(LED1)
/* ---------- 蜂鸣器硬件配置 ---------- */
#define BEEP_GPIO_PORT GPIOA /* 蜂鸣器GPIO端口 */
#define BEEP_GPIO_PIN GPIO_PIN_8 /* 蜂鸣器GPIO引脚 */
#define BEEP_GPIO_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE() /* 蜂鸣器时钟使能 */
/* 蜂鸣器控制宏(高电平触发鸣响) */
#define BEEP_ON GPIO_PIN_SET
#define BEEP_OFF GPIO_PIN_RESET
#define BEEP_SetState(state) HAL_GPIO_WritePin(BEEP_GPIO_PORT, BEEP_GPIO_PIN, (state))
#define BEEP_On() BEEP_SetState(BEEP_ON)
#define BEEP_Off() BEEP_SetState(BEEP_OFF)
/* 函数声明 */
void BEEP_Init(void); // 蜂鸣器GPIO初始化
void BEEP_AlarmWithLED(uint8_t n); // 1次短鸣 + 1次长鸣(同步红色LED亮灭)
#endif /* __BEEP_H */
在beep.c中实现头文件中定义的函数。首先是初始化蜂鸣器的GPIO,最后实现蜂鸣器鸣叫函数:
#include "beep.h"
/* 蜂鸣器GPIO初始化:配置为推挽输出 */
void BEEP_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* 使能蜂鸣器GPIO时钟 */
BEEP_GPIO_CLK_ENABLE();
/* 配置蜂鸣器引脚为推挽输出 */
GPIO_InitStruct.Pin = BEEP_GPIO_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL; // 无上下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; // 低速
HAL_GPIO_Init(BEEP_GPIO_PORT, &GPIO_InitStruct);
/* 初始状态:蜂鸣器关闭 */
BEEP_Off();
}
/* 蜂鸣器鸣响+LED同步逻辑:n次短鸣(0.25s) + 1次长鸣(1s) */
void BEEP_AlarmWithLED(uint8_t n)
{
uint8_t i;
/* 计算n:n = mod(学号末位,5) + 1 →
* 我的学号末位为5,即n = 5%5 +1 = 1 */
n = (n % 5) + 1;
/* 执行n次“短鸣+间隔” */
for (i = 0; i < n; i++)
{
BEEP_On(); // 蜂鸣器响
LED1_On(); // 红色LED亮
HAL_Delay(250); // 短鸣持续0.25s
BEEP_Off(); // 蜂鸣器关
LED1_Off(); // 红色LED灭
HAL_Delay(1000); // 短鸣间隔1s
}
/* 执行1次“长鸣” */
BEEP_On();
LED1_On();
HAL_Delay(1000); // 长鸣持续1s
/* 结束后关闭蜂鸣器和LED */
BEEP_Off();
LED1_Off();
}
作业-串口通信
作业内容:使用相应软件操作STM32开发板,用直接配置串口的方式,向PC传输一句话在FLASH中存储一句话,并用DMA配置串口向PC传输。
串口通讯是一种设备间非常常用的串行通讯方式,因为它简单便捷,因此大部分电子设备都支持该通讯方式,电子工程师在调试设备时也经常使用该通讯方式输出调试信息。
USART(Universal Synchronous/Asynchronous Receiver/Transmitter)是 STM32 芯片里的一种串行通信外设。它的主要功能就是把数据(字节)转换成一位一位的电平信号,通过TX引脚发出去,或者从RX引脚接收一位一位的电平信号,再拼成字节给CPU。在本实验中,只用到USART1的异步模式,即串口通信。
基于上述原理,创建文件usart.h用于记录宏:
//
// Created by lamaper on 2025/9/2.
//
#ifndef __USART_H
#define __USART_H
#include "stm32f1xx_hal.h"
#include "stm32f1xx_hal_uart.h"
#include <string.h>
/* 外部句柄 */
extern UART_HandleTypeDef huart1;
extern DMA_HandleTypeDef hdma_tx;
/* 初始化 */
void USART1_UART_Init(void);
/* 普通发送 */
void UART_SendString(char *str);
/* DMA 发送 */
void UART_SendString_DMA(char *str);
#endif /* __USART_H */
之后开始编写初始化usart函数,首先开启USART1、GPIOA、DMA1时钟,之后配置引脚,在STM32F103系列芯片中,统一规定了USART1的引脚是PA9(TX,发送)和PA10(RX,接收)。在此处设置TX为复用推挽,使其能输出波形;RX作为输入,设为上拉状态,记为空闲。然后配置通信格式。
//
// Created by lamaper on 2025/9/2.
//
#include "usart.h"
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_tx;
void USART1_UART_Init(void)
{
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_DMA1_CLK_ENABLE();
/* PA9 = TX, PA10 = RX */
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* USART1 配置: 115200 8N1 */
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
HAL_UART_Init(&huart1);
/* DMA 配置: USART1_TX = DMA1_Channel4 */
hdma_tx.Instance = DMA1_Channel4;
hdma_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_tx.Init.Mode = DMA_NORMAL;
hdma_tx.Init.Priority = DMA_PRIORITY_LOW;
HAL_DMA_Init(&hdma_tx);
__HAL_LINKDMA(&huart1, hdmatx, hdma_tx);
/* DMA NVIC */
HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);
}
void UART_SendString(char *str)
{
HAL_UART_Transmit(&huart1, (uint8_t*)str, strlen(str), HAL_MAX_DELAY);
}
void UART_SendString_DMA(char *str)
{
HAL_UART_Transmit_DMA(&huart1, (uint8_t*)str, strlen(str));
}
STM32F1 的主存储器是片上Flash,既用来存程序,也能存数据。其掉电不丢失,适合存储固定配置;Flash以页为单位擦除,不能只擦除一个字节;以半字,即16bit为最小写入单位,必须2字节对齐。如果要操作,首先要解锁Flash,然后擦除页使其恢复0xFF,之后逐字(半字)写入数据,最后锁上Flash。
在flash.h中声明两个函数负责读写,同时选一个安全带页地址,本文选择STM32F103VET6 最后2K的起始地址(0x0807F800U):
//
// Created by lamaper on 2025/9/2.
//
#ifndef __FLASH_H
#define __FLASH_H
#include "stm32f1xx_hal.h"
#include <string.h>
/* 选一个安全的页地址(比如最后一页) */
#define FLASH_PAGE_ADDR 0x0807F800U // STM32F103VET6 最后 2K 的起始地址
void Flash_Write(uint32_t addr, uint8_t *data, uint16_t len);
void Flash_Read(uint32_t addr, uint8_t *buf, uint16_t len);
#endif /* __FLASH_H */
在flash.c中实现他们:
//
// Created by lamaper on 2025/9/2.
//
#include "flash.h"
void Flash_Write(uint32_t addr, uint8_t *data, uint16_t len)
{
HAL_FLASH_Unlock();
/* 先擦除页(注意:Flash 必须先擦才能重新写) */
FLASH_EraseInitTypeDef erase = {0};
uint32_t pageError = 0;
erase.TypeErase = FLASH_TYPEERASE_PAGES;
erase.PageAddress = FLASH_PAGE_ADDR;
erase.NbPages = 1;
HAL_FLASHEx_Erase(&erase, &pageError);
/* 半字写入 */
for (uint16_t i = 0; i < len; i += 2)
{
uint16_t halfword = data[i];
if (i + 1 < len) halfword |= (data[i+1] << 8);
HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, addr + i, halfword);
}
HAL_FLASH_Lock();
}
void Flash_Read(uint32_t addr, uint8_t *buf, uint16_t len)
{
for (uint16_t i = 0; i < len; i++)
{
buf[i] = *(volatile uint8_t*)(addr + i);
}
}
之后就可以在main函数中配置基础逻辑了:
void HW4(void) {
USART1_UART_Init();
/* 1. 普通发送 */
UART_SendString("Hello, I'm lamaper! This is UART direct send!\r\n");
/* 2. Flash 存储一句话 */
char msg[] = "Hello from Flash + DMA!\r\n";
Flash_Write(FLASH_PAGE_ADDR, (uint8_t*)msg, strlen(msg)+1);
/* 3. 读回并用 DMA 发送 */
char buf[64];
Flash_Read(FLASH_PAGE_ADDR, (uint8_t*)buf, strlen(msg)+1);
UART_SendString_DMA(buf);
}
此外,注意到电脑本身并没有串口监控程序,需要单另下载,因而在Microsoft Store中下载相关工具,启动串口监听,有如下结果:


作业-七彩灯
作业内容:使用相应软件操作STM32开发板,以TIM3输出PWM,控制全彩LED灯变换颜色以另一个TIM作为计数器,每1s产生1次中断以中断控制一个状态机,改变全彩灯的CCR达到效果:赤-橙-黄-绿-青-蓝-紫,七种颜色循环切换,每1s切换一个颜色。
PWM 是 Pulse Width Modulation(脉冲宽度调制)的缩写,是一种通过改变脉冲信号的高电平持续时间与周期的比例,来模拟信号效果的数字控制技术。STM32 的定时器,如TIM3,可硬件生成高精度 PWM 信号,无需 CPU 持续干预。对 LED 来说,在频率足够高的情况下,人眼会因为视觉暂留把高频闪烁看作持续的光。TIMx作为PWM的时基有频率公式:
其中ARR为自动重装载寄存器,决定周期长度,即计数上限;PSC为预分频器,决定计数节拍变慢多少;CRR是捕获/比较寄存器,对应占空比。在STM32F103系列芯片中,TIM3有多个通道能做PWM,通过重映射可以把它们映射到开发板的引脚上:
//
// Created by lamaper on 2025/9/2.
//
#ifndef __TIM_H
#define __TIM_H
#include "stm32f1xx_hal.h" // HAL 基础
#include "stm32f1xx_hal_tim.h" // TIM HAL
/* 全局句柄(只声明) */
extern TIM_HandleTypeDef htim3; // PWM (RGB)
extern TIM_HandleTypeDef htim4; // 1s 周期中断
/* 初始化 */
void TIM3_PWM_Init(uint16_t arr, uint16_t psc);
void TIM4_1s_Init(uint16_t arr, uint16_t psc);
/* 设置 RGB 占空比:0~arr(arr=自动重装值) */
void LED_SetRGB(uint16_t r, uint16_t g, uint16_t b);
/* 作业5:七彩灯(1s 切换) */
void Rainbow_Init(void);
/* 作业6:四彩呼吸灯(周期 1.5s) */
void Breath_Init(void);
void LED_ChannelTest(void);
#endif
接下来实现切换颜色。这里应作业要求,使用另一个计时器,本文采用TIM4.切换颜色并不需要高频变换,最自然的办法是使用计时器做一个1Hz的软节拍,每1s产生一次更新中断。NVIC 开启 TIM4_IRQn,在TIM4_IRQHandler 里调用HAL_TIM_IRQHandler,最终会进入HAL_TIM_PeriodElapsedCallback。
IM_HandleTypeDef htim3;
TIM_HandleTypeDef htim4;
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
typedef enum { MODE_NONE = 0, MODE_RAINBOW, MODE_BREATH } TimerMode_t;
static TimerMode_t g_mode = MODE_NONE;
/
void TIM3_PWM_Init(uint16_t arr, uint16_t psc)
{
__HAL_RCC_TIM3_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_AFIO_CLK_ENABLE();
/* TIM3 部分重映射: CH2->PB5, CH3->PB0, CH4->PB1 */
__HAL_AFIO_REMAP_TIM3_PARTIAL();
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
/* R/G/B 对应引脚 */
GPIO_InitStruct.Pin = GPIO_PIN_5; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); // CH2 → PB5 → R
GPIO_InitStruct.Pin = GPIO_PIN_0; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); // CH3 → PB0 → G
GPIO_InitStruct.Pin = GPIO_PIN_1; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); // CH4 → PB1 → B
/* TIM3 基本参数 */
htim3.Instance = TIM3;
htim3.Init.Prescaler = psc;
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = arr;
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
HAL_TIM_PWM_Init(&htim3);
/* 三路 PWM 通道配置:有效低(低电平点亮,数值越大越亮) */
TIM_OC_InitTypeDef sOC = {0};
sOC.OCMode = TIM_OCMODE_PWM1;
sOC.Pulse = 0;
sOC.OCPolarity = TIM_OCPOLARITY_LOW; // ★ 有效低
sOC.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim3, &sOC, TIM_CHANNEL_2); // R
HAL_TIM_PWM_ConfigChannel(&htim3, &sOC, TIM_CHANNEL_3); // G
HAL_TIM_PWM_ConfigChannel(&htim3, &sOC, TIM_CHANNEL_4); // B
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2);
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3);
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_4);
}
void TIM4_Base_Init(uint16_t arr, uint16_t psc)
{
__HAL_RCC_TIM4_CLK_ENABLE();
htim4.Instance = TIM4;
htim4.Init.Prescaler = psc;
htim4.Init.CounterMode = TIM_COUNTERMODE_UP;
htim4.Init.Period = arr;
htim4.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim4.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
HAL_TIM_Base_Init(&htim4);
HAL_TIM_Base_Start_IT(&htim4);
HAL_NVIC_SetPriority(TIM4_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(TIM4_IRQn);
}
void LED_SetRGB(uint16_t r, uint16_t g, uint16_t b)
{
const uint16_t ARR = __HAL_TIM_GET_AUTORELOAD(&htim3);
if (r > ARR) r = ARR;
if (g > ARR) g = ARR;
if (b > ARR) b = ARR;
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, r); // R -> PB5
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, g); // G -> PB0
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_4, b); // B -> PB1
}
/* ==========================================================================
* 作业5:七彩灯(1s 切换,红→橙→黄→绿→青→蓝→紫)
* ========================================================================== */
static uint8_t rainbow_state = 0;
void Rainbow_Init(void)
{
TIM4_Base_Init(999, 7999); // 1s 节拍
rainbow_state = 0;
g_mode = MODE_RAINBOW;
}
static void Rainbow_Update(void)
{
const uint16_t ARR = __HAL_TIM_GET_AUTORELOAD(&htim3);
switch (rainbow_state)
{
case 0: LED_SetRGB(ARR, 0, 0); break; // 红
case 1: LED_SetRGB(ARR, ARR/2, 0); break; // 橙 = 红 + 半绿
case 2: LED_SetRGB(ARR, ARR, 0); break; // 黄 = 红 + 绿
case 3: LED_SetRGB( 0, ARR, 0); break; // 绿
case 4: LED_SetRGB( 0, ARR, ARR); break; // 青 = 绿 + 蓝
case 5: LED_SetRGB( 0, 0, ARR); break; // 蓝
case 6: LED_SetRGB(ARR, 0, ARR); break; // 紫 = 红 + 蓝
}
rainbow_state = (rainbow_state + 1) % 7;
}
之后进行编译和烧录即可。
作业-呼吸灯
作业内容:使用相应软件操作STM32开发板,产生4彩(红、绿、蓝、白)呼吸灯,呼吸周期为1.x秒(x为学号尾数),CCR更新周期不高于0.2s。
呼吸灯的核心是占空比的周期性变化。首先回顾PWM的内容。PWM 是 Pulse Width Modulation(脉冲宽度调制)的缩写,是一种通过改变脉冲信号的高电平持续时间与周期的比例,来模拟信号效果的数字控制技术。STM32 的定时器,如TIM3,可硬件生成高精度 PWM 信号,无需 CPU 持续干预。对 LED 来说,在频率足够高的情况下,人眼会因为视觉暂留把高频闪烁看作持续的光。TIMx作为PWM的时基有频率公式:
其中ARR为自动重装载寄存器,决定周期长度,即计数上限;PSC为预分频器,决定计数节拍变慢多少;CRR是捕获/比较寄存器,对应占空比。因而只需让CCR随着时间先增再减就能实现呼吸效果。本文采用余弦函数产生更平滑的呼吸效果:
其中duty∈[0,1],T为呼吸周期。
之后设置两个计时器,按照作业5的形式定义TIM3和TIM4的行为,这里不再赘述。
最终在上一节代码之后追加:
#define BREATH_PERIOD 1.5f
#define BREATH_DT 0.1f
static uint16_t breath_step = 0;
static uint8_t breath_color = 0;
void Breath_Init(void)
{
TIM4_Base_Init(99, 7999); // 0.1s 节拍
breath_step = 0;
breath_color = 0;
g_mode = MODE_BREATH;
}
static void Breath_Update(void)
{
const uint16_t ARR = __HAL_TIM_GET_AUTORELOAD(&htim3);
const uint16_t steps_per_cycle = (uint16_t)(BREATH_PERIOD / BREATH_DT); // 例如1.5/0.1=15
float x = (float)breath_step / (float)steps_per_cycle; // 0..1
float duty = (1.0f - cosf(2.0f * (float)M_PI * x)) * 0.5f; // 余弦曲线 0..1
uint16_t v = (uint16_t)(duty * ARR);
switch (breath_color)
{
case 0: LED_SetRGB(v, 0, 0); break; // 红
case 1: LED_SetRGB(0, v, 0); break; // 绿
case 2: LED_SetRGB(0, 0, v); break; // 蓝
case 3: LED_SetRGB(v, v, v); break; // 白
default: break;
}
breath_step++;
if (breath_step >= steps_per_cycle)
{
breath_step = 0;
breath_color = (breath_color + 1) % 4; // 红→绿→蓝→白
}
}
本文来自博客园,作者:lamaper,转载请注明原文链接:https://www.cnblogs.com/lamaper/articles/19079807/bit-ece-stm32

浙公网安备 33010602011771号