1. 项目概述
作为一名长期使用CLion开发嵌入式系统的工程师,我发现很多同行在配置STM32项目时都会遇到CMakeLists的配置难题,特别是内存管理这一块。今天我就来详细分享一下我在实际项目中积累的CMakeLists配置经验和内存管理技巧。
CLion作为一款优秀的跨平台C/C++ IDE,其强大的代码分析和重构功能让它成为嵌入式开发的利器。但要让CLion完美支持STM32开发,CMakeLists的配置是关键。不同于普通的桌面应用开发,嵌入式开发需要考虑芯片特性、内存布局、启动文件等特殊因素。
2. 环境准备与工具链配置
2.1 安装必要工具
在开始之前,我们需要准备以下工具:
- CLion 2023.x或更高版本
- ARM GCC工具链(建议使用gcc-arm-none-eabi-10.3-2021.10)
- OpenOCD(用于调试)
- STM32CubeMX(用于生成初始化代码)
提示:建议将工具链路径添加到系统环境变量中,这样CLion可以自动检测到它们。
2.2 配置工具链路径
在CLion中,打开"File > Settings > Build, Execution, Deployment > Toolchains",添加一个新的工具链配置:
- 选择"MinGW"作为基础(虽然我们使用ARM GCC,但CLion需要这个作为起点)
- 设置C编译器路径为arm-none-eabi-gcc.exe
- 设置C++编译器路径为arm-none-eabi-g++.exe
- 设置调试器路径为arm-none-eabi-gdb.exe
3. CMakeLists基础配置
3.1 基本结构
一个典型的STM32项目CMakeLists.txt应该包含以下部分:
cmake复制cmake_minimum_required(VERSION 3.20)
project(STM32_Project C CXX ASM)
# 设置编译选项
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_EXE_LINKER_FLAGS "--specs=nosys.specs")
# 添加源文件
file(GLOB_RECURSE SOURCES "Core/Src/*.c" "Core/Src/*.cpp")
file(GLOB_RECURSE HEADERS "Core/Inc/*.h")
file(GLOB_RECURSE STARTUP "Core/Startup/*.s")
# 添加包含目录
include_directories(Core/Inc Drivers/STM32F4xx_HAL_Driver/Inc Drivers/CMSIS/Include)
# 创建可执行文件
add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS} ${STARTUP})
# 设置链接脚本
target_link_options(${PROJECT_NAME} PRIVATE -T${CMAKE_SOURCE_DIR}/STM32F407VGTx_FLASH.ld)
3.2 关键配置解析
-
编译器标准设置:
- C11标准是STM32 HAL库的最低要求
- C++17标准可以提供更好的现代C++支持
-
链接器标志:
--specs=nosys.specs告诉链接器不使用系统库,因为嵌入式系统没有操作系统
-
源文件收集:
- 使用
GLOB_RECURSE递归收集所有源文件 - 特别注意要包含启动文件(.s)
- 使用
-
链接脚本:
- 必须指定正确的链接脚本路径
- 链接脚本定义了内存布局,对嵌入式系统至关重要
4. 高级CMake配置技巧
4.1 条件编译与芯片选择
在实际项目中,我们经常需要支持多种芯片型号。可以通过CMake选项来实现:
cmake复制option(STM32F4 "Build for STM32F4 series" ON)
option(STM32F7 "Build for STM32F7 series" OFF)
if(STM32F4)
add_definitions(-DSTM32F407xx)
set(MCU_FLAGS "-mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard")
elseif(STM32F7)
add_definitions(-DSTM32F767xx)
set(MCU_FLAGS "-mcpu=cortex-m7 -mthumb -mfpu=fpv5-sp-d16 -mfloat-abi=hard")
endif()
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${MCU_FLAGS}")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${MCU_FLAGS}")
set(CMAKE_ASM_FLAGS "${CMAKE_ASM_FLAGS} ${MCU_FLAGS}")
4.2 优化选项配置
针对不同的开发阶段,我们需要不同的优化级别:
cmake复制option(DEBUG_BUILD "Build with debug symbols" ON)
if(DEBUG_BUILD)
set(OPTIMIZATION_LEVEL "-Og -g3")
add_definitions(-DDEBUG)
else()
set(OPTIMIZATION_LEVEL "-O2")
endif()
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OPTIMIZATION_LEVEL}")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OPTIMIZATION_LEVEL}")
5. 内存管理配置
5.1 链接脚本解析
STM32的内存管理主要通过链接脚本实现。一个典型的链接脚本包含以下关键部分:
code复制MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 192K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 1024K
}
SECTIONS
{
.text :
{
. = ALIGN(4);
*(.text)
*(.text*)
*(.glue_7)
*(.glue_7t)
*(.eh_frame)
KEEP (*(.init))
KEEP (*(.fini))
. = ALIGN(4);
_etext = .;
} > FLASH
/* 其他段定义... */
}
5.2 堆栈大小配置
在链接脚本中,我们可以定义堆栈大小:
code复制_Min_Heap_Size = 0x2000; /* 8KB */
_Min_Stack_Size = 0x1000; /* 4KB */
.heap :
{
. = ALIGN(8);
_sheap = .;
. = . + _Min_Heap_Size;
. = ALIGN(8);
_eheap = .;
} > RAM
.stack :
{
. = ALIGN(8);
_estack = .;
. = . + _Min_Stack_Size;
. = ALIGN(8);
_sstack = .;
} > RAM
5.3 自定义内存区域
对于有外部RAM的型号,可以添加自定义内存区域:
code复制MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 192K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 1024K
SDRAM (xrw) : ORIGIN = 0xC0000000, LENGTH = 8M
}
.custom_section :
{
. = ALIGN(4);
_scustom = .;
*(.custom_section)
. = ALIGN(4);
_ecustom = .;
} > SDRAM
6. 调试配置
6.1 OpenOCD配置
在CLion中配置OpenOCD调试:
- 创建或修改
.idea/runConfigurations/OpenOCD.xml文件 - 添加以下内容:
xml复制<component name="ProjectRunConfigurationManager">
<configuration default="false" name="OpenOCD" type="com.jetbrains.cidr.embedded.openocd.conf.type">
<option name="openOcdConfigFiles">
<list>
<option value="interface/stlink-v2.cfg" />
<option value="target/stm32f4x.cfg" />
</list>
</option>
<option name="targetRemoteLocation" value="localhost:3333" />
<method v="2">
<option name="com.jetbrains.cidr.embedded.openocd.program.method" enabled="true" />
</method>
</configuration>
</component>
6.2 调试技巧
-
半主机模式:
在调试时,可以启用半主机模式输出调试信息:c复制#include "debug.h" void initialise_monitor_handles(void); int main(void) { initialise_monitor_handles(); printf("Debug output via semihosting\n"); // ... }在CMakeLists.txt中添加:
cmake复制target_link_options(${PROJECT_NAME} PRIVATE --specs=rdimon.specs) -
ITM输出:
另一种更高效的调试输出方式是使用ITM:c复制#include "stm32f4xx.h" void ITM_SendChar(uint8_t ch) { while (ITM->PORT[0].u32 == 0); ITM->PORT[0].u8 = ch; } void print(const char* str) { while (*str) { ITM_SendChar(*str++); } }
7. 常见问题与解决方案
7.1 编译问题
问题1:未定义的引用错误
code复制undefined reference to `_sbrk'
解决方案:
添加--specs=nosys.specs到链接器选项:
cmake复制set(CMAKE_EXE_LINKER_FLAGS "--specs=nosys.specs")
问题2:启动文件找不到
code复制cannot find -lstartup_stm32f407xx
解决方案:
确保启动文件(.s)被包含在源文件列表中:
cmake复制file(GLOB_RECURSE STARTUP "Core/Startup/*.s")
add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS} ${STARTUP})
7.2 链接问题
问题1:内存区域溢出
code复制region `FLASH' overflowed by 1234 bytes
解决方案:
- 检查链接脚本中的FLASH大小是否正确
- 优化代码大小:
- 使用
-Os优化选项 - 移除不必要的库函数
- 使用
-ffunction-sections -fdata-sections配合--gc-sections
- 使用
问题2:未定义的中断处理程序
code复制undefined reference to `TIM2_IRQHandler'
解决方案:
确保在代码中实现了所有使用的中断处理程序,即使是空实现:
c复制void TIM2_IRQHandler(void) {
// 空实现
}
7.3 调试问题
问题1:无法连接到目标
code复制Error: open failed
解决方案:
- 检查ST-Link连接是否正常
- 确认OpenOCD配置文件选择正确
- 尝试重置开发板
问题2:断点不工作
code复制Warning: Cannot insert breakpoint
解决方案:
- 确保编译时启用了调试信息(
-g) - 检查优化级别,过高优化可能导致断点不准确
- 尝试在函数入口处设置断点
8. 性能优化技巧
8.1 代码优化
-
内联关键函数:
在关键性能路径上使用__attribute__((always_inline)):c复制static inline __attribute__((always_inline)) void delay_cycles(uint32_t cycles) { volatile uint32_t i = cycles; while (i--); } -
使用CMSIS指令:
对于Cortex-M系列,使用CMSIS提供的内部函数:c复制#include "arm_math.h" void fast_copy(uint32_t* dst, uint32_t* src, size_t len) { arm_copy_q15((q15_t*)src, (q15_t*)dst, len); }
8.2 内存优化
-
使用内存池:
替代动态内存分配,使用静态内存池:c复制#define POOL_SIZE 1024 static uint8_t memory_pool[POOL_SIZE]; static size_t pool_ptr = 0; void* pool_alloc(size_t size) { if (pool_ptr + size > POOL_SIZE) return NULL; void* ptr = &memory_pool[pool_ptr]; pool_ptr += size; return ptr; } -
优化数据对齐:
使用__attribute__((aligned))确保关键数据结构对齐:c复制struct sensor_data { uint32_t timestamp __attribute__((aligned(4))); float values[4] __attribute__((aligned(4))); };
8.3 编译优化
-
链接时优化(LTO):
在CMake中启用LTO:cmake复制set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) -
函数/数据分段:
启用分段以允许链接器移除未使用的代码:cmake复制set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ffunction-sections -fdata-sections") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections")
9. 项目结构最佳实践
9.1 推荐目录结构
code复制project/
├── CMakeLists.txt
├── Core/
│ ├── Inc/ # 头文件
│ ├── Src/ # 源文件
│ └── Startup/ # 启动文件
├── Drivers/
│ ├── CMSIS/ # CMSIS核心
│ └── STM32F4xx_HAL_Driver/ # HAL驱动
├── Middlewares/ # 中间件
├── build/ # 构建目录
└── STM32F407VGTx_FLASH.ld # 链接脚本
9.2 模块化CMake配置
对于大型项目,建议将CMake配置模块化:
- 创建
cmake/目录 - 添加工具链文件
cmake/toolchain-arm-none-eabi.cmake:
cmake复制set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)
set(CMAKE_AR arm-none-eabi-ar)
set(CMAKE_OBJCOPY arm-none-eabi-objcopy)
set(CMAKE_OBJDUMP arm-none-eabi-objdump)
set(CMAKE_SIZE arm-none-eabi-size)
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
- 在主CMakeLists.txt中引用:
cmake复制set(CMAKE_TOOLCHAIN_FILE cmake/toolchain-arm-none-eabi.cmake)
10. 进阶主题:多核STM32配置
对于STM32H7等多核MCU,CMake配置需要特殊处理:
10.1 双核项目结构
code复制project/
├── CM7/
│ ├── Core/
│ ├── Drivers/
│ └── CMakeLists.txt
├── CM4/
│ ├── Core/
│ ├── Drivers/
│ └── CMakeLists.txt
├── Shared/ # 共享代码
└── CMakeLists.txt # 主CMake文件
10.2 主CMakeLists.txt配置
cmake复制cmake_minimum_required(VERSION 3.20)
project(STM32H7_DualCore)
# 共享代码配置
add_library(shared_code STATIC Shared/*.c Shared/*.h)
target_include_directories(shared_code PUBLIC Shared)
# CM7核配置
add_subdirectory(CM7)
# CM4核配置
add_subdirectory(CM4)
10.3 核间通信(IPC)配置
在链接脚本中定义共享内存区域:
code复制MEMORY
{
RAM_D1 (xrw) : ORIGIN = 0x24000000, LENGTH = 512K
RAM_D2 (xrw) : ORIGIN = 0x30000000, LENGTH = 288K
RAM_D3 (xrw) : ORIGIN = 0x38000000, LENGTH = 64K
ITCMRAM (xrw) : ORIGIN = 0x00000000, LENGTH = 64K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 2048K
SHARED_RAM (rw) : ORIGIN = 0x30040000, LENGTH = 32K
}
.shared_data :
{
. = ALIGN(4);
_sshared = .;
*(.shared_data)
. = ALIGN(4);
_eshared = .;
} > SHARED_RAM
在代码中使用共享区域:
c复制#define SHARED_DATA_SECTION __attribute__((section(".shared_data")))
typedef struct {
uint32_t counter;
uint8_t buffer[256];
} shared_data_t;
SHARED_DATA_SECTION volatile shared_data_t ipc_data;
11. 单元测试集成
11.1 添加Unity测试框架
- 下载Unity测试框架
- 在项目中创建
Tests/目录 - 添加测试CMakeLists.txt:
cmake复制add_executable(test_runner
Tests/test_runner.c
Tests/test_hal_gpio.c
External/Unity/src/unity.c
)
target_include_directories(test_runner PRIVATE
Core/Inc
Drivers/STM32F4xx_HAL_Driver/Inc
External/Unity/src
)
target_link_libraries(test_runner PRIVATE
${PROJECT_NAME}
)
11.2 模拟硬件接口
创建硬件抽象层(HAL)的模拟实现:
c复制// Tests/hal_mock.h
typedef struct {
uint32_t GPIO_Pin;
GPIO_PinState PinState;
} gpio_mock_t;
extern gpio_mock_t gpio_mock;
void HAL_GPIO_WritePin_Mock(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState) {
gpio_mock.GPIO_Pin = GPIO_Pin;
gpio_mock.PinState = PinState;
}
11.3 在CLion中运行测试
- 创建新的运行配置
- 选择测试可执行文件
- 添加必要的环境变量
12. 持续集成配置
12.1 GitHub Actions配置
创建.github/workflows/build.yml:
yaml复制name: STM32 Build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install ARM GCC
run: |
sudo apt-get update
sudo apt-get install gcc-arm-none-eabi
- name: Configure CMake
run: cmake -B build -DCMAKE_TOOLCHAIN_FILE=cmake/toolchain-arm-none-eabi.cmake
- name: Build
run: cmake --build build --target all
12.2 静态代码分析
在CMake中集成Clang-Tidy:
cmake复制set(CMAKE_C_CLANG_TIDY clang-tidy;-checks=*;-warnings-as-errors=*)
set(CMAKE_CXX_CLANG_TIDY clang-tidy;-checks=*;-warnings-as-errors=*)
13. 实用脚本与工具
13.1 内存使用分析脚本
创建scripts/memory_report.py:
python复制import re
import sys
def parse_size(size_str):
units = {"B": 1, "KB": 1024, "MB": 1024*1024}
num, unit = re.match(r"(\d+)(\w+)", size_str).groups()
return int(num) * units[unit.upper()]
# 解析arm-none-eabi-size输出
# ...
13.2 自动生成CMake文件列表
创建scripts/generate_cmake.py:
python复制import os
def find_sources(directory, extensions):
sources = []
for root, _, files in os.walk(directory):
for file in files:
if any(file.endswith(ext) for ext in extensions):
sources.append(os.path.join(root, file))
return sources
# 生成CMake文件列表
# ...
14. 性能监控与分析
14.1 实时性能计数器
使用STM32的DWT(Data Watchpoint and Trace)单元:
c复制#include "core_cm4.h"
void enable_dwt() {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
uint32_t get_cycle_count() {
return DWT->CYCCNT;
}
void profile_function() {
enable_dwt();
uint32_t start = get_cycle_count();
// 要测量的代码
uint32_t end = get_cycle_count();
printf("Cycles: %lu\n", end - start);
}
14.2 内存使用监控
添加堆使用监控:
c复制extern uint32_t _sheap;
extern uint32_t _eheap;
size_t get_heap_usage() {
uint8_t* p = (uint8_t*)&_sheap;
while (p < (uint8_t*)&_eheap && *p == 0xCD) p++;
return (uint8_t*)&_eheap - p;
}
void check_heap() {
size_t used = get_heap_usage();
size_t total = (uint8_t*)&_eheap - (uint8_t*)&_sheap;
printf("Heap usage: %zu/%zu bytes (%.1f%%)\n",
used, total, (float)used/total*100);
}
15. 电源管理集成
15.1 低功耗模式配置
在CMake中定义低功耗选项:
cmake复制option(LOW_POWER "Enable low power features" OFF)
if(LOW_POWER)
add_definitions(-DUSE_LOW_POWER)
target_sources(${PROJECT_NAME} PRIVATE power_management.c)
endif()
15.2 唤醒源管理
c复制void enter_stop_mode() {
// 配置唤醒源
HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1);
// 进入STOP模式
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
// 唤醒后重新配置时钟
SystemClock_Config();
}
16. 固件升级支持
16.1 添加Bootloader支持
创建独立的bootloader项目:
cmake复制project(STM32_Bootloader C ASM)
# Bootloader需要自己的链接脚本
set(LINKER_SCRIPT ${CMAKE_SOURCE_DIR}/STM32F407VGTx_BOOTLOADER.ld)
add_executable(${PROJECT_NAME}
Core/Src/main.c
Core/Startup/startup_stm32f407xx.s
)
target_link_options(${PROJECT_NAME} PRIVATE -T${LINKER_SCRIPT})
16.2 应用跳转机制
c复制typedef void (*pFunction)(void);
void jump_to_application(uint32_t app_address) {
pFunction app_entry;
uint32_t app_stack;
// 检查应用地址是否有效
app_stack = *(uint32_t*)app_address;
if((app_stack & 0x2FFE0000) == 0x20000000) {
// 设置主堆栈指针
__set_MSP(*(uint32_t*)app_address);
// 获取复位处理函数地址
app_entry = (pFunction)*(uint32_t*)(app_address + 4);
// 跳转到应用
app_entry();
}
}
17. 多配置管理
17.1 构建类型配置
在CMake中定义不同的构建类型:
cmake复制set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Choose build type: Debug Release MinSizeRel RelWithDebInfo")
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
add_definitions(-DDEBUG -DUSE_FULL_ASSERT)
set(OPTIMIZATION_LEVEL "-Og -g3")
elseif(CMAKE_BUILD_TYPE STREQUAL "Release")
set(OPTIMIZATION_LEVEL "-O3")
endif()
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OPTIMIZATION_LEVEL}")
17.2 功能开关配置
使用CMake选项控制功能模块:
cmake复制option(USE_FREERTOS "Enable FreeRTOS support" OFF)
option(USE_LWIP "Enable LWIP network stack" OFF)
option(USE_USB_DEVICE "Enable USB Device mode" ON)
if(USE_FREERTOS)
add_subdirectory(Middlewares/FreeRTOS)
add_definitions(-DUSE_FREERTOS)
endif()
if(USE_LWIP)
add_subdirectory(Middlewares/LwIP)
add_definitions(-DUSE_LWIP)
endif()
18. 第三方库集成
18.1 添加FatFS文件系统
- 下载FatFS源代码
- 在CMakeLists.txt中添加:
cmake复制add_library(fatfs STATIC
Middlewares/FatFs/src/ff.c
Middlewares/FatFs/src/ffunicode.c
Middlewares/FatFs/src/diskio.c
)
target_include_directories(fatfs PUBLIC
Middlewares/FatFs/src
)
target_link_libraries(${PROJECT_NAME} PRIVATE fatfs)
18.2 自定义diskio实现
c复制#include "ff.h"
#include "diskio.h"
DSTATUS disk_initialize(BYTE pdrv) {
if(pdrv != 0) return STA_NOINIT;
// 初始化SD卡接口
if(BSP_SD_Init() != MSD_OK) {
return STA_NOINIT;
}
return RES_OK;
}
19. 代码生成工具集成
19.1 自动生成硬件初始化代码
使用STM32CubeMX生成代码后,可以创建自定义的CMake目标来处理:
cmake复制add_custom_target(generate_code
COMMAND java -jar STM32CubeMX -q -s project.ioc -o Generated
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Generating code from STM32CubeMX"
)
add_custom_command(
OUTPUT ${GENERATED_SOURCES}
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/Generated ${CMAKE_BINARY_DIR}/Generated
DEPENDS generate_code
)
19.2 自定义代码生成脚本
创建Python脚本生成特定代码:
python复制# scripts/generate_hal_config.py
import json
def generate_hal_config(config):
with open('hal_config.h', 'w') as f:
f.write('#ifndef HAL_CONFIG_H\n')
f.write('#define HAL_CONFIG_H\n\n')
for peripheral, settings in config.items():
for setting, value in settings.items():
f.write(f'#define {peripheral}_{setting} {value}\n')
f.write('\n#endif // HAL_CONFIG_H\n')
20. 跨平台开发技巧
20.1 模拟器支持
添加模拟器构建目标:
cmake复制option(BUILD_SIMULATOR "Build for simulator" OFF)
if(BUILD_SIMULATOR)
add_executable(simulator
simulator/main.c
${SOURCES}
)
target_compile_definitions(simulator PRIVATE SIMULATOR)
target_include_directories(simulator PRIVATE simulator)
endif()
20.2 硬件抽象层设计
设计可移植的硬件抽象层:
c复制// hal_gpio.h
typedef enum {
GPIO_LOW = 0,
GPIO_HIGH = 1
} gpio_state_t;
void hal_gpio_init(void);
void hal_gpio_set(uint8_t pin, gpio_state_t state);
// stm32/hal_gpio.c
#include "stm32f4xx_hal.h"
void hal_gpio_set(uint8_t pin, gpio_state_t state) {
HAL_GPIO_WritePin(GPIOA, 1 << pin,
state == GPIO_HIGH ? GPIO_PIN_SET : GPIO_PIN_RESET);
}
// simulator/hal_gpio.c
void hal_gpio_set(uint8_t pin, gpio_state_t state) {
printf("GPIO %d set to %d\n", pin, state);
}