“ CubeMX+cmake+ninja+arm-gcc ” STM32开发环境搭建 和 “printf重定向”。基于b站up主linkyourbin。

一、视频链接
https://www.bilibili.com/video/BV1cBwuedEXe?spm_id_from=333.788.videopod.sections&vd_source=14a161ea00c9a50477dfd2bcbda91e08

二、工具链下载报错
建议使用mysys2做工具包管理,也就是和博主一样。这样便于管理开发环境。

问题1:工具链已安装,但是终端找不到工具链
image

这个问题,是你的环境变量没有添加。mysys2的环境变量除了你使用的包管理文件的bin文件夹,还有usr文件夹需要添加。检查下这四个文件夹是不是都有。
这里环境变量中的地址要一直到bin为止,不然找不到你脚本中调用的.exe程序的。
(1)arm-gcc的存放文件夹arm_genu_toolchain/bin要有,编译工具的基础。
(2)注意因为我是用MSYS2 UCRT64下载的,所以我在环境变量中配置的urct64/bin。你可以根据你的下载包管理工具,比如MYSYS2 MINGW64...
(3)usr/bin文件夹是一定要有的,你下载的工具在这个里面。
(4)mingw64/bin文件夹是存放的gcc编译器,由于交叉编译他是armgccgcc混编,所以这个文件夹也要有。
1772452825724

三、编译报错
问题1:找不到c编译器
image
这一部分是CubeMX生成的,在cmake文件夹下的gcc-arm-none-eabi.cmake配置文件并没有被cmake命令正确识别。
1772453948988
将cmake命令改为:

#编译器编译成build_ninja,也可以改为生成Makefile,CubeMX设置的cmake默认编译器为ninja。
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/gcc-arm-none-eabi.cmake -DCMAKE_BUILD_TYPE=Debug -G "Ninja"

补充全流程命令行脚本,大家可以自己参考:

#可选,删除build
rm -rf build

#建立build文件夹
mkdir build && cd build
#生成build_ninja,作用于Makefile类似
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/gcc-arm-none-eabi.cmake -DCMAKE_BUILD_TYPE=Debug -G "Ninja"
#编译成elf
ninja

#烧录
openocd -f interface/stlink.cfg -f target/stm32f1x.cfg -c "program F103_Cmake_Test.elf verify reset exit"

问题2:无法生成Makefile文件,无法使用make命令
这一个问题,建议切换为Ninja。因为如下图所示,CubeMX生成的配置文件CMakePresets.json中编译器默认为Ninja。
而我们为了工程的复用,尽量不修改cmake配置文件。
1772454416592

四、一键下载编译脚本run.sh

这个脚本由AI生成,编译和下载每一步都会询问你。
默认下载的文件格式为hex,也可以指定为bin。

移植到别的项目只需要修改前面三行。

点击查看run.sh脚本
#!/bin/bash

#===============================================================================
# STM32 一键编译下载脚本 (Ninja + OpenOCD)
# 支持生成 .elf、.hex、.bin 文件,默认烧录 .hex
#===============================================================================

set -e  # 任何命令失败立即退出

#-------------------------------------------------------------------------------
# 配置区域(根据实际情况修改)
#-------------------------------------------------------------------------------
PROJECT_NAME="F103_Cmake_Test"
BUILD_DIR="build/Debug"

# 输出文件路径
ELF_FILE="${BUILD_DIR}/${PROJECT_NAME}.elf"
HEX_FILE="${BUILD_DIR}/${PROJECT_NAME}.hex"
BIN_FILE="${BUILD_DIR}/${PROJECT_NAME}.bin"

# OpenOCD 配置
OPENOCD_CFG="-f interface/stlink.cfg -f target/stm32f1x.cfg"

# 烧录文件类型: "hex" 或 "bin"(默认 hex)
# bin文件体积更小,但是需要手动指定地址。是一种纯二进制文件,没有调试信息。
FLASH_TYPE="hex"

#-------------------------------------------------------------------------------
# 颜色输出函数
#-------------------------------------------------------------------------------
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

info() {
    echo -e "${BLUE}[INFO]${NC} $1"
}

success() {
    echo -e "${GREEN}[SUCCESS]${NC} $1"
}

warning() {
    echo -e "${YELLOW}[WARNING]${NC} $1"
}

error() {
    echo -e "${RED}[ERROR]${NC} $1"
}

#-------------------------------------------------------------------------------
# 步骤 1:检查依赖工具
#-------------------------------------------------------------------------------
check_dependencies() {
    info "检查依赖工具..."

    local missing_tools=()

    if ! command -v cmake &> /dev/null; then
        missing_tools+=("cmake")
    fi

    if ! command -v ninja &> /dev/null; then
        missing_tools+=("ninja")
    fi

    if ! command -v openocd &> /dev/null; then
        missing_tools+=("openocd")
    fi

    if ! command -v arm-none-eabi-objcopy &> /dev/null; then
        missing_tools+=("arm-none-eabi-objcopy (arm-none-eabi-gcc 工具链)")
    fi

    if [ ${#missing_tools[@]} -ne 0 ]; then
        error "缺少以下工具: ${missing_tools[*]}"
        error "请先安装: pacman -S cmake ninja openocd arm-none-eabi-gcc"
        exit 1
    fi

    success "所有依赖工具已就绪"
}

#-------------------------------------------------------------------------------
# 步骤 2:检查文件是否存在
#-------------------------------------------------------------------------------
check_files() {
    local need_rebuild=0

    if [ ! -f "$ELF_FILE" ]; then
        need_rebuild=1
    fi

    if [ $need_rebuild -eq 0 ]; then
        return 0
    else
        return 1
    fi
}

#-------------------------------------------------------------------------------
# 步骤 3:从 ELF 生成 HEX 和 BIN 文件
#-------------------------------------------------------------------------------
generate_output_files() {
    info "从 ELF 生成 HEX 和 BIN 文件..."

    # 生成 HEX 文件
    if arm-none-eabi-objcopy -O ihex "$ELF_FILE" "$HEX_FILE"; then
        local hex_size=$(stat -c%s "$HEX_FILE")
        success "HEX 文件生成成功: $HEX_FILE (${hex_size} bytes)"
    else
        error "HEX 文件生成失败!"
        exit 1
    fi

    # 生成 BIN 文件
    if arm-none-eabi-objcopy -O binary "$ELF_FILE" "$BIN_FILE"; then
        local bin_size=$(stat -c%s "$BIN_FILE")
        success "BIN 文件生成成功: $BIN_FILE (${bin_size} bytes)"
    else
        error "BIN 文件生成失败!"
        exit 1
    fi
}

#-------------------------------------------------------------------------------
# 步骤 4:清理并重新构建
#-------------------------------------------------------------------------------
build_project() {
    info "开始构建项目..."

    # 删除旧构建目录
    if [ -d "build" ]; then
        info "清理旧构建目录..."
        rm -rf build
    fi

    # 使用 CMake Preset 配置
    info "运行 cmake --preset=Debug..."
    if ! cmake --preset=Debug; then
        error "CMake 配置失败!"
        exit 1
    fi

    # 使用 Ninja 编译
    info "运行 ninja 编译..."
    cd "$BUILD_DIR"
    if ! ninja; then
        error "编译失败!"
        cd ../..
        exit 1
    fi
    cd ../..

    # 检查 ELF 是否生成
    if [ ! -f "$ELF_FILE" ]; then
        error "ELF 文件未生成: $ELF_FILE"
        exit 1
    fi

    local elf_size=$(stat -c%s "$ELF_FILE")
    success "构建完成!ELF 大小: ${elf_size} bytes"

    # 生成 HEX 和 BIN
    generate_output_files
}

#-------------------------------------------------------------------------------
# 步骤 5:烧录到 STM32
#-------------------------------------------------------------------------------
flash_device() {
    # 确定要烧录的文件
    local flash_file=""
    if [ "$FLASH_TYPE" = "hex" ]; then
        flash_file="$HEX_FILE"
    elif [ "$FLASH_TYPE" = "bin" ]; then
        flash_file="$BIN_FILE"
    else
        error "未知的 FLASH_TYPE: $FLASH_TYPE"
        exit 1
    fi

    # 检查烧录文件是否存在
    if [ ! -f "$flash_file" ]; then
        error "烧录文件不存在: $flash_file"
        exit 1
    fi

    info "开始烧录到 STM32..."
    info "烧录文件类型: $FLASH_TYPE"
    info "烧录文件路径: $flash_file"

    # 检查 ST-Link 连接
    info "检查 ST-Link 连接..."
    if ! openocd $OPENOCD_CFG -c "init" -c "exit" 2>/dev/null; then
        warning "ST-Link 连接可能有问题,但仍尝试烧录..."
    fi

    # 烧录文件
    info "烧录 $flash_file ..."
    
    # 如果是 BIN 文件,需要指定加载地址(STM32F103 通常是 0x08000000)
    if [ "$FLASH_TYPE" = "bin" ]; then
        if openocd $OPENOCD_CFG -c "program $flash_file 0x08000000 verify reset exit"; then
            success "烧录成功!"
        else
            error "烧录失败!"
            error "可能原因:"
            error "  1. ST-Link 未连接或驱动未安装"
            error "  2. 接线错误(SWCLK/SWDIO/GND)"
            error "  3. 目标芯片被锁死"
            exit 1
        fi
    else
        # HEX 文件自带加载地址信息
        if openocd $OPENOCD_CFG -c "program $flash_file verify reset exit"; then
            success "烧录成功!"
        else
            error "烧录失败!"
            error "可能原因:"
            error "  1. ST-Link 未连接或驱动未安装"
            error "  2. 接线错误(SWCLK/SWDIO/GND)"
            error "  3. 目标芯片被锁死"
            exit 1
        fi
    fi
}

#-------------------------------------------------------------------------------
# 步骤 6:显示固件信息
#-------------------------------------------------------------------------------
show_firmware_info() {
    info "固件信息:"
    echo "----------------------------------------"
    echo "ELF 文件:"
    arm-none-eabi-size "$ELF_FILE"
    echo ""
    echo "HEX 文件大小: $(stat -c%s "$HEX_FILE") bytes"
    echo "BIN 文件大小: $(stat -c%s "$BIN_FILE") bytes"
    echo "----------------------------------------"
    echo "ELF 段信息:"
    arm-none-eabi-objdump -h "$ELF_FILE" | head -20
    echo "----------------------------------------"
}

#-------------------------------------------------------------------------------
# 步骤 7:显示帮助信息
#-------------------------------------------------------------------------------
show_help() {
    echo "用法: $0 [选项]"
    echo ""
    echo "选项:"
    echo "  -h, --help     显示此帮助信息"
    echo "  -b, --bin      使用 BIN 文件进行烧录(默认使用 HEX)"
    echo "  -r, --rebuild  强制重新编译"
    echo "  -f, --flash    只烧录,不编译"
    echo "  -s, --size     只显示固件大小,不编译不烧录"
    echo ""
    echo "示例:"
    echo "  $0             编译并烧录 HEX 文件"
    echo "  $0 -b          编译并烧录 BIN 文件"
    echo "  $0 -r          强制重新编译并烧录"
    echo "  $0 -f          只烧录现有 HEX 文件"
    echo "  $0 -s          只显示固件大小"
}

#-------------------------------------------------------------------------------
# 解析命令行参数
#-------------------------------------------------------------------------------
parse_args() {
    while [[ $# -gt 0 ]]; do
        case $1 in
            -h|--help)
                show_help
                exit 0
                ;;
            -b|--bin)
                FLASH_TYPE="bin"
                shift
                ;;
            -r|--rebuild)
                REBUILD_FORCE=1
                shift
                ;;
            -f|--flash)
                FLASH_ONLY=1
                shift
                ;;
            -s|--size)
                SIZE_ONLY=1
                shift
                ;;
            *)
                error "未知选项: $1"
                show_help
                exit 1
                ;;
        esac
    done
}

#-------------------------------------------------------------------------------
# 主流程
#-------------------------------------------------------------------------------
main() {
    parse_args "$@"

    echo ""
    echo "=============================================="
    echo "  STM32 一键编译下载工具 (增强版)"
    echo "  Project: $PROJECT_NAME"
    echo "  Date: $(date)"
    echo "  烧录类型: $FLASH_TYPE"
    echo "=============================================="
    echo ""

    # 检查依赖
    check_dependencies

    # 只显示大小
    if [ "$SIZE_ONLY" = "1" ]; then
        if check_files; then
            generate_output_files
            show_firmware_info
            success "✅ 完成"
        else
            error "ELF 文件不存在,请先编译"
            exit 1
        fi
        exit 0
    fi

    # 只烧录
    if [ "$FLASH_ONLY" = "1" ]; then
        if check_files; then
            generate_output_files
            flash_device
            success "🎉 烧录完成!"
        else
            error "ELF 文件不存在,无法生成烧录文件"
            exit 1
        fi
        exit 0
    fi

    # 检查是否需要重建
    if check_files && [ "$REBUILD_FORCE" != "1" ]; then
        warning "发现已有 ELF 文件"
        read -p "是否重新编译?(y/N): " rebuild_choice
        if [[ $rebuild_choice =~ ^[Yy]$ ]]; then
            build_project
        else
            info "跳过编译,使用现有 ELF 文件"
            generate_output_files
        fi
    else
        build_project
    fi

    # 显示固件信息
    show_firmware_info

    # 确认烧录
    echo ""
    read -p "是否烧录到 STM32?(y/N): " flash_choice
    if [[ $flash_choice =~ ^[Yy]$ ]]; then
        flash_device
        success "🎉 一键编译下载完成!"
    else
        info "跳过烧录"
        success "✅ 编译完成"
        echo "   ELF: $ELF_FILE"
        echo "   HEX: $HEX_FILE"
        echo "   BIN: $BIN_FILE"
    fi
}

#-------------------------------------------------------------------------------
# 运行主函数
#-------------------------------------------------------------------------------
main "$@"
使用方法:将run.sh拷贝到你的工程根目录。
命令行执行    ./run.sh

五、Printf重定向报错
重定向程序:

int _write(int fd, char *pBuffer, int size)
{
    // 避免串口发送过程中的死循环,加入超时机制
    const uint32_t timeout = 100000; // 超时周期,假设最多等待 100,000 次
    uint32_t timeout_counter = 0;

    for (int i = 0; i < size; i++)
    {
        // 等待直到串口的数据寄存器空
        while ((USART1->SR & 0x40) == 0)
        {
            timeout_counter++;
            if (timeout_counter >= timeout)
            {
                // 如果超过超时限制,可以跳出并返回错误,或者做其他处理
                return -1; // 返回错误
            }
        }
        USART1->DR = (uint8_t) pBuffer[i]; // 写入数据寄存器,发送字符
    }

    return size; // 返回成功发送的字符数量
}
/* USER CODE END 1 */

在Up主的基础上,改动了添加的Cmake配置文件。内容依旧加到最后。

## 有中文注释的部分,是cubemx不会自动生成的,需要手动添加
set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -u _printf_float")   # 支持printf函数打印浮点数
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -lm")        # 链接数学库libm。这一部分与博主有些不同,不是使用的C_Link。
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections,--no-warn-rwx-segments")  # 取消 rwx 段的警告
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-unused-parameter")        # 忽略C代码中未使用参数的警告
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-parameter")    # 忽略C++代码中未使用参数的警告

补充:
以下由元宝AI生成
image
image
image

六、番外:在keil中实现printf重定向
依旧在usart.c增加代码:

#ifdef _GNUC_
  #define PUTCHAR_PROTOTYPE int _io_putchar(int ch)
#else
  #define PUTCHAR_PROTOTYPE int fputc(int ch,FILE *f)
#endif

PUTCHAR_PROTOTYPE{
  HAL_UART_Transmit(&huart1,(uint8_t *)&ch,sizeof(ch),0xFFF);
  return ch;
}

勾选MicroLib,不然程序会卡死。
1772458828616

posted @ 2026-03-02 21:57  alanala  阅读(11)  评论(0)    收藏  举报