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中下载相关工具,启动串口监听,有如下结果:
46541a11663cc8b9da7217d985890d27
图片

作业-七彩灯

作业内容:使用相应软件操作STM32开发板,以TIM3输出PWM,控制全彩LED灯变换颜色以另一个TIM作为计数器,每1s产生1次中断以中断控制一个状态机,改变全彩灯的CCR达到效果:赤-橙-黄-绿-青-蓝-紫,七种颜色循环切换,每1s切换一个颜色。

PWM 是 Pulse Width Modulation(脉冲宽度调制)的缩写,是一种通过改变脉冲信号的高电平持续时间与周期的比例,来模拟信号效果的数字控制技术。STM32 的定时器,如TIM3,可硬件生成高精度 PWM 信号,无需 CPU 持续干预。对 LED 来说,在频率足够高的情况下,人眼会因为视觉暂留把高频闪烁看作持续的光。TIMx作为PWM的时基有频率公式:

\[f_{PWM} = \frac{f_{TIMCLK}}{(PSC+1) \times (ARR+1)} \]

其中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的时基有频率公式:

\[f_{PWM} = \frac{f_{TIMCLK}}{(PSC+1) \times (ARR+1)} \]

其中ARR为自动重装载寄存器,决定周期长度,即计数上限;PSC为预分频器,决定计数节拍变慢多少;CRR是捕获/比较寄存器,对应占空比。因而只需让CCR随着时间先增再减就能实现呼吸效果。本文采用余弦函数产生更平滑的呼吸效果:

\[duty(t) = \frac{1-\cos{(\frac{2\pi t}{T}})}{2} \]

其中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; // 红→绿→蓝→白
    }
}
posted @ 2025-09-08 16:10  lamaper  阅读(214)  评论(0)    收藏  举报

友情链接 jiuler