配置驱动开发:初探零代码构建嵌入式软件配置工具

前言

在嵌入式软件开发中,硬件初始化与寄存器配置长期依赖人工编写重复代码。以STM32外设初始化为例,开发者需手动完成时钟使能、引脚模式设置、参数配置等步骤,不仅耗时易错(如位掩码写反、模式枚举值混淆),更因代码冗余导致维护成本高——修改一个外设参数可能需同步调整多处代码,稍有不慎便引发硬件异常。

当看到同事通过GUI配置,自动生成底层代码后,其效率提升让我意识到:传统"手写代码+文档"的模式已难以适应复杂嵌入式系统的开发需求。为此,我简要探索了一下​​配置即代码(Configuration-as-Code)​​方案,目标是通过结构化配置文件与自动化工具链,实现"零手写代码"生成高质量嵌入式初始化代码,同时兼顾可维护性与灵活性。

技术选型与核心架构

针对嵌入式开发的特殊性,配置工具需满足三大核心需求:

  1. ​严格的Schema验证机制​​:确保配置文件符合硬件约束(如引脚复用冲突、参数取值范围),从源头避免无效配置;
  2. ​树形结构清晰表达层级​​:映射硬件物理层级的嵌套关系(如"外设→端口→引脚→参数"),提升配置文件的可读性与可维护性;
  3. ​注释能力便于维护​​:支持多语言注释(如中文硬件说明、英文API提示),降低团队协作与后续迭代的沟通成本。

架构分层设计

工具采用"配置模型-数据绑定-代码生成"三层架构(如图1所示):

  • ​配置模型层​​:基于XML Schema(XSD)定义标准化配置结构,约束各字段的取值范围与层级关系;
  • ​数据绑定层​​:通过双向数据绑定机制,实现UI操作与XML配置的实时同步;
  • ​代码生成层​​:根据目标平台(如STM32系列)的HAL库规范,将XML配置转换为平台相关的初始化代码。

关键技术实现

UI与XML的实时绑定

UI状态与XML数据模型通过​​双向数据绑定​​实现同步:

  • 当用户在UI修改"GPIO0模式"为"输出"时,XML中<gpio id="0"><mode>节点的值立即更新为"输出";
  • 当通过脚本修改XML中<gpio id="1"><pull>节点为"下拉"时,UI中对应引脚的下拉框会自动选中"下拉"选项。

示例XML(STM32_GPIO配置片段)​​:

<stm32_gpio_config version="1.0"><gpio id="0"><pin>PA0</pin><mode>输入</mode><output_type>N/A</output_type><speed>N/A</speed><pull>上拉</pull></gpio><gpio id="1"><pin>PA1</pin><mode>输出</mode><output_type>推挽</output_type><speed>中速</speed><pull>无</pull></gpio><gpio id="2"><pin>PA2</pin><mode>复用</mode><output_type>开漏</output_type><speed>中速</speed><pull>无</pull></gpio>
</stm32_gpio_config>

自动转换逻辑

在代码生成阶段,根据目标平台选择对应的映射规则,将通用配置转换为平台特定的HAL库参数。例如,当目标平台为STM32F1时,配置中的“输出”、“推挽”会自动替换为GPIO_Mode_Out_PP,避免手动修改错误。

双向同步机制

为支持遗留项目改造,工具提供​​代码→XML的双向同步​​能力:

  • ​GUI→代码​​:用户在UI的修改实时生成符合HAL库规范的初始化代码(如MX_GPIO_Init函数),直接替换手写代码;
  • ​代码→GUI​​:通过静态代码分析工具解析现有工程的HAL库初始化代码(如提取GPIO_InitStruct.GPIO_Mode的值),逆向生成XML配置,快速实现遗留项目的可视化改造。

端口聚合优化:减少冗余寄存器操作

传统手写代码中,每个外设引脚的初始化需单独调用GPIO_Init函数,导致大量重复的寄存器操作(如多次使能同一端口的时钟)。我们的工具通过​​端口聚合优化​​,将同端口的配置合并,显著减少代码冗余并提升执行效率。

优化前(手写代码)
// 传统手写代码示例(STM32F103 GPIO初始化)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);  // 时钟使能
GPIO_InitTypeDef GPIO_InitStruct;// PA0配置(输入浮空)
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStruct);// PA1配置(输出推挽)
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOA, &GPIO_InitStruct);// PA2配置(复用开漏)
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &GPIO_InitStruct);
优化后(工具生成代码)
/* 自动生成的STM32F103 GPIO初始化代码 */
#include "stm32f10x.h"// 合并时钟使能(仅调用1次)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);// 按端口聚合配置(仅调用3次GPIO_Init,覆盖所有引脚)
GPIO_InitTypeDef GPIO_InitStruct_0;
GPIO_InitStruct_0.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct_0.GPIO_Mode = GPIO_Mode_IN_FLOATING;  // PA0输入浮空
GPIO_Init(GPIOA, &GPIO_InitStruct_0);GPIO_InitTypeDef GPIO_InitStruct_1;
GPIO_InitStruct_1.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStruct_1.GPIO_Mode = GPIO_Mode_Out_PP;       // PA1输出推挽
GPIO_Init(GPIOA, &GPIO_InitStruct_1);GPIO_InitTypeDef GPIO_InitStruct_2;
GPIO_InitStruct_2.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStruct_2.GPIO_Mode = GPIO_Mode_AF_PP;        // PA2复用开漏
GPIO_Init(GPIOA, &GPIO_InitStruct_2);

展望

这次探索自动代码生成的经历,本质上是一次对"开发流程智能化"的小范围实践。通过引入XML/JSON这样的中间配置文件作为"意图载体",我把原本固化的代码生成逻辑变成了可维护、可追溯的结构化数据,不仅解决了重复编码的效率问题,还摸索出一套"配置驱动开发"的轻量级方法。过程中几个关键收获,值得好好总结:

首先是​​配置文件的标准化设计​​。XML/JSON的可扩展性和语义明确性,让它们既能描述复杂业务规则(比如条件分支、动态组件加载),又能通过版本控制工具(如Git)追踪配置变更,方便团队协作和回滚。未来如果能结合领域特定语言(DSL)进一步抽象配置语法,配置的可读性和表达能力应该还能提升。

其次是​​AI辅助工具链的价值​​。传统工具链里,开发者得同时懂配置语法、生成逻辑和调试技巧;但在AI的帮助下,我只需要说清楚业务需求,AI就能自动补全配置细节、生成符合规范的代码,还能根据运行反馈定位问题。这种"需求到代码"的智能映射,正在模糊开发者和工具的边界,推动开发角色向"需求架构师"转变。

最后是​​自动化生成的边界​​。自动代码生成最适合"模式化、低风险、高重复"的任务,而不是替代开发者的创造性工作。未来的工具链应该更注重"人机协同"——AI处理标准化部分,开发者聚焦创新和灵活需求,形成分层协作的生态。

以上是AI生成的文章,以下才是我想表达的

之前在用ST单片机时,我一直是用标准库来写代码,对每一行代码的细节都要了解清楚,对未知的代码就特别想了解清楚。最开始接触到自动代码生成,是大学时了解到CubeMX,只需要在GUI界面,配置IO端口,配置时钟树。当时对这个功能其实也不算很震惊,甚至感觉不过如此,可能是因为了解得太少吧。不过我还是查了很多文章,差不多是:为什么ST官方要推出CubeMX、HAL库有什么优势。

最近工作时,我发现同事在用自动代码生成工具,这才是真正震惊到我的地方,因为它就实实在在发生在我的身边。我也明白了,为什么经常有人调侃自己是码农,因为有些代码的编写,真的跟搬砖没区别,只不过门槛高一些罢了。有了AI后,其实很多重复性的代码都不需要人来开发,而我们的核心价值,也就在创造性上面(文学艺术创作除外,哭笑),在于人的主观能动性,至少在我所从事的汽车电子行业是这样。

最近也大概了解了自动代码生成,了解相关技术及工具,最开始无从下手,也是边问AI边了解,慢慢深入。后面在QtCreater和PyQt、JSON和XML之间,均选择了后者。

今天趁着周末,外面还在下雨,能安心下来,初探嵌入式软件配置工具,自动代码生成。我让AI帮我生成代码,时间从早上10点一直到晚上10点。离谱的是,后面的代码我基本上是看不懂了,但我还是通过表达我的想法,让AI生成代码,我在PyCharm上调试,将问题反馈给AI,一直在迭代,整个过程有点像在训练AI模型。到后面代码超过500行时,整个AI思考和推理生成代码的时间特别久,最长需要5分钟,不过最终也实现了基础功能。

嵌入式软件配置自动生成代码,核心思想,就是下面这张图。以我现有水平和精力,将代码逆向解析成数据结构有点难度,所以还没实现。不过整个流程基本上是跑通了,也完成了核心的工作。

最后AI生成的效果属实是有点意料之外,没想到能做得这么好。

/* 自动生成的STM32F103 GPIO初始化代码 */
#include "stm32f10x.h"RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOPA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);/* GPIO初始化配置 */
GPIO_InitTypeDef GPIO_InitStruct_0;
GPIO_InitStruct_0.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct_0.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStruct_0.GPIO_PuPd = GPIO_PuPd_NOPULL;GPIO_InitTypeDef GPIO_InitStruct_1;
GPIO_InitStruct_1.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStruct_1.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct_1.GPIO_Speed = GPIO_Speed_10MHz;GPIO_InitTypeDef GPIO_InitStruct_2;
GPIO_InitStruct_2.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStruct_2.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct_2.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_InitStruct_2.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_InitTypeDef GPIO_InitStruct_3;
GPIO_InitStruct_3.GPIO_Pin = GPIO_Pin_3;
GPIO_InitStruct_3.GPIO_Mode = GPIO_Mode_AIN;
GPIO_InitStruct_3.GPIO_PuPd = GPIO_PuPd_NOPULL;/* 初始化所有GPIO */
GPIO_Init(GPIOPA, &GPIO_InitStruct_0);
GPIO_Init(GPIOPA, &GPIO_InitStruct_1);
GPIO_Init(GPIOPA, &GPIO_InitStruct_2);
GPIO_Init(GPIOPA, &GPIO_InitStruct_3);/* 初始化完成 */

完整代码放在下面

import sys
import re
import os
import xml.etree.ElementTree as ET
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,QLabel, QComboBox, QPushButton, QFileDialog, QMessageBox,QScrollArea)
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFontclass STM32GPIOConfigTool(QMainWindow):def __init__(self):super().__init__()self.setWindowTitle("STM32F103 GPIO配置工具 (增强版)")self.setGeometry(100, 100, 900, 650)# 设置应用样式self.setStyleSheet("""QMainWindow {background-color: #F5F5F5;}QLabel {font-weight: bold;}QPushButton {background-color: #4CAF50;color: white;border: none;padding: 8px 16px;border-radius: 4px;font-weight: bold;}QPushButton:hover {background-color: #45a049;}QPushButton:pressed {background-color: #3d8b40;}QComboBox {padding: 4px;border: 1px solid #ccc;border-radius: 4px;}QScrollArea {background-color: white;border: none;}QWidget#config_row {background-color: white;border-radius: 6px;padding: 8px;margin: 4px;border: 1px solid #E0E0E0;}""")# 初始化数据存储self.gpio_configs = []  # 存储有效配置(含引脚、模式等)self.used_pins = set()  # 记录已使用的引脚(防止重复)# 主布局main_widget = QWidget()self.setCentralWidget(main_widget)main_layout = QVBoxLayout(main_widget)main_layout.setSpacing(15)main_layout.setContentsMargins(15, 15, 15, 15)# 标题title_label = QLabel("STM32F103 GPIO配置工具 (增强版)")title_label.setFont(QFont("Arial", 16, QFont.Weight.Bold))title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)main_layout.addWidget(title_label)# 操作按钮区域btn_layout = QHBoxLayout()btn_layout.setSpacing(10)self.btn_add = QPushButton("添加引脚配置")self.btn_add.clicked.connect(self.add_gpio_row)self.btn_import = QPushButton("导入XML配置")self.btn_import.clicked.connect(self.import_xml)self.btn_save_xml = QPushButton("保存XML配置")self.btn_save_xml.clicked.connect(self.save_xml)self.btn_generate = QPushButton("生成STM32 HAL代码")self.btn_generate.clicked.connect(self.generate_code)btn_layout.addWidget(self.btn_add)btn_layout.addWidget(self.btn_import)btn_layout.addWidget(self.btn_save_xml)btn_layout.addWidget(self.btn_generate)main_layout.addLayout(btn_layout)# 配置区域标题config_title = QLabel("GPIO引脚配置")config_title.setFont(QFont("Arial", 12, QFont.Weight.Bold))main_layout.addWidget(config_title)# 滚动区域(支持多行滚动)self.scroll_area = QScrollArea()self.scroll_area.setWidgetResizable(True)self.scroll_content = QWidget()self.gpio_layout = QVBoxLayout(self.scroll_content)self.gpio_layout.setSpacing(10)self.gpio_layout.setContentsMargins(10, 10, 10, 10)self.scroll_area.setWidget(self.scroll_content)main_layout.addWidget(self.scroll_area)# 添加初始行self.add_gpio_row()def add_gpio_row(self):"""添加新行并自动分配未使用的GPIO(优化PA0优先)"""try:# 创建行部件row_widget = QWidget()row_widget.setObjectName("config_row")row_layout = QHBoxLayout(row_widget)row_layout.setContentsMargins(10, 10, 10, 10)row_layout.setSpacing(15)# 引脚选择(PA0-PB15)pin_label = QLabel("引脚:")pin_combo = QComboBox()pin_combo.addItems([f"PA{i}" for i in range(16)] + [f"PB{i}" for i in range(16)])row_layout.addWidget(pin_label, 0, Qt.AlignmentFlag.AlignRight)row_layout.addWidget(pin_combo, 1)# 模式选择(输入/输出/复用/模拟)mode_label = QLabel("模式:")mode_combo = QComboBox()mode_combo.addItems(["输入", "输出", "复用", "模拟"])row_layout.addWidget(mode_label, 0, Qt.AlignmentFlag.AlignRight)row_layout.addWidget(mode_combo, 1)# 输出类型(仅输出/复用模式有效)type_label = QLabel("输出类型:")type_combo = QComboBox()type_combo.addItems(["推挽", "开漏"])type_combo.setEnabled(False)row_layout.addWidget(type_label, 0, Qt.AlignmentFlag.AlignRight)row_layout.addWidget(type_combo, 1)# 速度等级(仅输出/复用模式有效)speed_label = QLabel("速度:")speed_combo = QComboBox()speed_combo.addItems(["低速", "中速", "高速", "最高速"])speed_combo.setEnabled(False)row_layout.addWidget(speed_label, 0, Qt.AlignmentFlag.AlignRight)row_layout.addWidget(speed_combo, 1)# 上下拉电阻(仅输入/模拟模式有效)pull_label = QLabel("上下拉:")pull_combo = QComboBox()pull_combo.addItems(["无", "上拉", "下拉"])pull_combo.setCurrentText("无")row_layout.addWidget(pull_label, 0, Qt.AlignmentFlag.AlignRight)row_layout.addWidget(pull_combo, 1)# 行内删除按钮del_btn = QPushButton("删除")del_btn.setFixedSize(80, 30)del_btn.setStyleSheet("""QPushButton {background-color: #f44336;color: white;border-radius: 4px;font-weight: bold;}QPushButton:hover {background-color: #d32f2f;}QPushButton:pressed {background-color: #b71c1c;}""")del_btn.clicked.connect(lambda: self.remove_gpio_row(row_widget))row_layout.addWidget(del_btn, 0, Qt.AlignmentFlag.AlignRight)# 绑定事件mode_combo.currentTextChanged.connect(lambda mode, tc=type_combo, sc=speed_combo, pc=pull_combo:self.toggle_gpio_options(mode, tc, sc, pc))pin_combo.currentTextChanged.connect(lambda text, rw=row_widget: self.on_pin_selected(text, rw))# 自动分配未使用的引脚(优化:优先PA0)available_pins = self._get_unused_pins()if available_pins:selected_pin = available_pins[0]  # 优先PA0pin_combo.setCurrentText(selected_pin)self.used_pins.add(selected_pin)  # 标记为已使用else:QMessageBox.warning(self, "警告", "所有GPIO引脚已被使用!")return# 初始化配置对象config = {"widget": row_widget,"pin_combo": pin_combo,"mode_combo": mode_combo,"type_combo": type_combo,"speed_combo": speed_combo,"pull_combo": pull_combo,"pin": pin_combo.currentText(),"index": len(self.gpio_configs)}self.gpio_configs.append(config)# 添加行到布局self.gpio_layout.addWidget(row_widget)# 设置初始模式为输入mode_combo.setCurrentText("输入")except Exception as e:QMessageBox.critical(self, "错误", f"添加行失败:{str(e)}")if 'row_widget' in locals():row_widget.deleteLater()def _get_unused_pins(self):"""获取所有未使用的GPIO引脚(PA0优先)"""all_pins = [f"PA{i}" for i in range(16)] + [f"PB{i}" for i in range(16)]unused = [p for p in all_pins if p not in self.used_pins]# 优先PA0(如果有)if "PA0" in unused:return ["PA0"] + [p for p in unused if p != "PA0"]return unuseddef remove_gpio_row(self, row_widget):"""删除行并释放对应引脚"""try:# 查找对应的配置for i, cfg in enumerate(self.gpio_configs):if cfg["widget"] == row_widget:# 释放引脚if cfg["pin"] in self.used_pins:self.used_pins.remove(cfg["pin"])# 移除配置self.gpio_configs.pop(i)break# 从布局移除部件self.gpio_layout.removeWidget(row_widget)row_widget.deleteLater()except Exception as e:QMessageBox.critical(self, "错误", f"删除行失败:{str(e)}")def on_pin_selected(self, selected_pin, row_widget):"""引脚选择实时检测(重复/格式校验)"""try:# 查找对应的配置config = Nonefor cfg in self.gpio_configs:if cfg["widget"] == row_widget:config = cfgbreakif not config:return# 清除之前的错误状态row_widget.setStyleSheet("")# 校验1:引脚格式是否合法if not re.fullmatch(r"^PA\d{1,2}$|^PB\d{1,2}$", selected_pin):row_widget.setStyleSheet("background-color: #FFCCCC;")QMessageBox.warning(self, "警告", "无效引脚格式!请使用PA0-PA15或PB0-PB15")# 重置为之前的有效选择config["pin_combo"].setCurrentText(config["pin"])return# 校验2:引脚是否已被使用(排除自身)other_used = [cfg for cfg in self.gpio_configsif cfg["pin"] == selected_pin and cfg != config]if other_used:row_widget.setStyleSheet("background-color: #FFCCCC;")QMessageBox.warning(self, "警告", f"引脚 {selected_pin} 已被使用,请选择其他引脚!")# 重置为之前的有效选择config["pin_combo"].setCurrentText(config["pin"])return# 更新配置old_pin = config["pin"]config["pin"] = selected_pin# 更新使用记录if old_pin in self.used_pins:self.used_pins.remove(old_pin)self.used_pins.add(selected_pin)# 触发模式相关控件状态更新self.toggle_gpio_options(config["mode_combo"].currentText(),config["type_combo"],config["speed_combo"],config["pull_combo"])except Exception as e:QMessageBox.critical(self, "错误", f"引脚检查失败:{str(e)}")def toggle_gpio_options(self, mode, type_combo, speed_combo, pull_combo):"""根据模式控制控件状态"""is_input = mode == "输入"is_analog = mode == "模拟"is_output_remap = mode in ["输出", "复用"]# 控制上下拉pull_combo.setEnabled(is_input or is_analog)if is_analog:pull_combo.setCurrentText("无")pull_combo.setEnabled(False)# 控制输出类型和速度(仅输出/复用模式)type_combo.setEnabled(is_output_remap)speed_combo.setEnabled(is_output_remap)# 输出/复用模式下禁用上下拉if is_output_remap:pull_combo.setEnabled(False)pull_combo.setCurrentText("无")def save_xml(self):"""保存XML配置"""try:# 检查重复引脚if not self._check_duplicates():return# 构建XMLroot = ET.Element("stm32_gpio_config", version="1.0")for idx, cfg in enumerate(self.gpio_configs):gpio_elem = ET.SubElement(root, "gpio", id=str(idx))ET.SubElement(gpio_elem, "pin").text = cfg["pin"]ET.SubElement(gpio_elem, "mode").text = cfg["mode_combo"].currentText()if cfg["mode_combo"].currentText() in ["输出", "复用"]:ET.SubElement(gpio_elem, "output_type").text = cfg["type_combo"].currentText()ET.SubElement(gpio_elem, "speed").text = cfg["speed_combo"].currentText()ET.SubElement(gpio_elem, "pull").text = "无"else:ET.SubElement(gpio_elem, "output_type").text = "N/A"ET.SubElement(gpio_elem, "speed").text = "N/A"ET.SubElement(gpio_elem, "pull").text = cfg["pull_combo"].currentText()# 保存文件file_path, _ = QFileDialog.getSaveFileName(self, "保存XML", "gpio_config.xml", "XML文件 (*.xml)")if not file_path:returnos.makedirs(os.path.dirname(file_path), exist_ok=True)with open(file_path, "w", encoding="utf-8") as f:f.write('<?xml version="1.0" encoding="utf-8"?>\n')f.write(ET.tostring(root, encoding="utf-8", method="xml").decode("utf-8"))QMessageBox.information(self, "成功", f"XML已保存至:{file_path}")except Exception as e:QMessageBox.critical(self, "错误", f"保存XML失败:{str(e)}")def _check_duplicates(self):"""检查重复引脚"""pins = [cfg["pin"] for cfg in self.gpio_configs]duplicates = set([pin for pin in pins if pins.count(pin) > 1])if duplicates:error_msg = "发现重复引脚:\n"for pin in duplicates:indices = [str(i + 1) for i, cfg in enumerate(self.gpio_configs) if cfg["pin"] == pin]error_msg += f"{pin}(行 {', '.join(indices)})\n"QMessageBox.warning(self, "错误", error_msg)return False# 清除所有错误背景for cfg in self.gpio_configs:cfg["widget"].setStyleSheet("")return Truedef import_xml(self):"""导入XML配置"""try:file_path, _ = QFileDialog.getOpenFileName(self, "导入XML", "", "XML文件 (*.xml)")if not file_path:return# 清空当前配置self._clear_configs()# 解析XMLtree = ET.parse(file_path)root = tree.getroot()if root.tag != "stm32_gpio_config":QMessageBox.warning(self, "警告", "无效的STM32 GPIO配置文件!")return# 遍历所有gpio节点for gpio_node in root.findall("gpio"):# 解析节点pin = gpio_node.find("pin").text.strip() if gpio_node.find("pin") is not None else ""mode = gpio_node.find("mode").text.strip() if gpio_node.find("mode") is not None else ""output_type = gpio_node.find("output_type").text.strip() if gpio_node.find("output_type") is not None else "N/A"speed = gpio_node.find("speed").text.strip() if gpio_node.find("speed") is not None else "N/A"pull = gpio_node.find("pull").text.strip() if gpio_node.find("pull") is not None else "无"# 添加新配置config = {"widget": None,"pin_combo": None,"mode_combo": None,"type_combo": None,"speed_combo": None,"pull_combo": None,"pin": pin,"mode": mode,"output_type": output_type,"speed": speed,"pull": pull,"index": len(self.gpio_configs)}self.gpio_configs.append(config)self.used_pins.add(pin)# 创建行控件for cfg in self.gpio_configs:self._add_gpio_row_from_config(cfg)QMessageBox.information(self, "导入成功", f"成功导入 {len(self.gpio_configs)} 个配置!")except Exception as e:QMessageBox.critical(self, "导入失败", f"导入XML失败:{str(e)}")def _clear_configs(self):"""清空当前配置"""# 删除所有行部件for i in reversed(range(self.gpio_layout.count())):widget = self.gpio_layout.itemAt(i).widget()if widget:widget.deleteLater()# 清空配置列表和使用记录self.gpio_configs.clear()self.used_pins.clear()def _add_gpio_row_from_config(self, config):"""从配置创建行控件"""try:row_widget = QWidget()row_widget.setObjectName("config_row")row_layout = QHBoxLayout(row_widget)row_layout.setContentsMargins(10, 10, 10, 10)row_layout.setSpacing(15)# 引脚选择pin_label = QLabel("引脚:")pin_combo = QComboBox()pin_combo.addItems([f"PA{i}" for i in range(16)] + [f"PB{i}" for i in range(16)])pin_combo.setCurrentText(config["pin"])row_layout.addWidget(pin_label, 0, Qt.AlignmentFlag.AlignRight)row_layout.addWidget(pin_combo, 1)# 模式选择mode_label = QLabel("模式:")mode_combo = QComboBox()mode_combo.addItems(["输入", "输出", "复用", "模拟"])mode_combo.setCurrentText(config.get("mode", "输入"))row_layout.addWidget(mode_label, 0, Qt.AlignmentFlag.AlignRight)row_layout.addWidget(mode_combo, 1)# 输出类型type_label = QLabel("输出类型:")type_combo = QComboBox()type_combo.addItems(["推挽", "开漏"])type_combo.setCurrentText(config.get("output_type", "推挽"))type_combo.setEnabled(False)row_layout.addWidget(type_label, 0, Qt.AlignmentFlag.AlignRight)row_layout.addWidget(type_combo, 1)# 速度speed_label = QLabel("速度:")speed_combo = QComboBox()speed_combo.addItems(["低速", "中速", "高速", "最高速"])speed_combo.setCurrentText(config.get("speed", "低速"))speed_combo.setEnabled(False)row_layout.addWidget(speed_label, 0, Qt.AlignmentFlag.AlignRight)row_layout.addWidget(speed_combo, 1)# 上下拉pull_label = QLabel("上下拉:")pull_combo = QComboBox()pull_combo.addItems(["无", "上拉", "下拉"])pull_combo.setCurrentText(config.get("pull", "无"))row_layout.addWidget(pull_label, 0, Qt.AlignmentFlag.AlignRight)row_layout.addWidget(pull_combo, 1)# 删除按钮del_btn = QPushButton("删除")del_btn.setFixedSize(80, 30)del_btn.setStyleSheet("""QPushButton {background-color: #f44336;color: white;border-radius: 4px;font-weight: bold;}QPushButton:hover {background-color: #d32f2f;}QPushButton:pressed {background-color: #b71c1c;}""")del_btn.clicked.connect(lambda: self.remove_gpio_row(row_widget))row_layout.addWidget(del_btn, 0, Qt.AlignmentFlag.AlignRight)# 绑定事件mode_combo.currentTextChanged.connect(lambda mode, tc=type_combo, sc=speed_combo, pc=pull_combo:self.toggle_gpio_options(mode, tc, sc, pc))pin_combo.currentTextChanged.connect(lambda text, rw=row_widget: self.on_pin_selected(text, rw))# 保存配置config["widget"] = row_widgetconfig["pin_combo"] = pin_comboconfig["mode_combo"] = mode_comboconfig["type_combo"] = type_comboconfig["speed_combo"] = speed_comboconfig["pull_combo"] = pull_combo# 添加行到布局self.gpio_layout.addWidget(row_widget)# 设置模式以更新控件状态mode_combo.currentTextChanged.emit(mode_combo.currentText())except Exception as e:QMessageBox.critical(self, "错误", f"创建行失败:{str(e)}")def generate_code(self):"""生成STM32 HAL代码"""try:# 检查重复引脚if not self._check_duplicates():return# 准备代码模板code = ["/* 自动生成的STM32F103 GPIO初始化代码 */","#include \"stm32f10x.h\"\n"]# 时钟使能rcc_groups = set()for cfg in self.gpio_configs:pin = cfg["pin"]pin_group = pin[:2]  # PA/PBrcc = f"RCC_APB2Periph_GPIO{pin_group}"rcc_groups.add(rcc)if rcc_groups:for rcc in rcc_groups:code.append(f"RCC_APB2PeriphClockCmd({rcc}, ENABLE);")if any(cfg["mode_combo"].currentText() == "复用" for cfg in self.gpio_configs):code.append("RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);")code.append("\n/* GPIO初始化配置 */")PULL_MAP = {"无": "GPIO_PuPd_NOPULL", "上拉": "GPIO_PuPd_UP", "下拉": "GPIO_PuPd_DOWN"}SPEED_MAP = {"低速": "GPIO_Speed_2MHz", "中速": "GPIO_Speed_10MHz","高速": "GPIO_Speed_50MHz", "最高速": "GPIO_Speed_100MHz"}MODE_MAP = {"输入": "GPIO_Mode_IN_FLOATING","输出": "GPIO_Mode_Out_PP","复用": "GPIO_Mode_AF_PP","模拟": "GPIO_Mode_AIN"}for i, cfg in enumerate(self.gpio_configs):pin = cfg["pin"]pin_num = int(''.join(filter(str.isdigit, pin)))code.append(f"GPIO_InitTypeDef GPIO_InitStruct_{i};")code.append(f"GPIO_InitStruct_{i}.GPIO_Pin = GPIO_Pin_{pin_num};")code.append(f"GPIO_InitStruct_{i}.GPIO_Mode = {MODE_MAP[cfg['mode_combo'].currentText()]};")if cfg["mode_combo"].currentText() in ["输出", "复用"]:code.append(f"GPIO_InitStruct_{i}.GPIO_Speed = {SPEED_MAP[cfg['speed_combo'].currentText()]};")if cfg["mode_combo"].currentText() == "复用":code.append(f"GPIO_InitStruct_{i}.GPIO_Mode = GPIO_Mode_AF_PP;")  # 复用推挽else:code.append(f"GPIO_InitStruct_{i}.GPIO_PuPd = {PULL_MAP[cfg['pull_combo'].currentText()]};")code.append("")code.append("\n/* 初始化所有GPIO */")for i, cfg in enumerate(self.gpio_configs):pin_group = cfg["pin"][:2]code.append(f"GPIO_Init(GPIO{pin_group}, &GPIO_InitStruct_{i});")code.append("\n/* 初始化完成 */")# 保存代码file_path, _ = QFileDialog.getSaveFileName(self, "保存代码", "STM32F103_GPIO.c", "C文件 (*.c)")if not file_path:returnos.makedirs(os.path.dirname(file_path), exist_ok=True)with open(file_path, "w", encoding="utf-8") as f:f.write('\n'.join(code))QMessageBox.information(self, "成功", f"代码已生成至:{file_path}")except Exception as e:QMessageBox.critical(self, "错误", f"生成代码失败:{str(e)}")if __name__ == "__main__":app = QApplication(sys.argv)tool = STM32GPIOConfigTool()tool.show()sys.exit(app.exec())

 以下是迭代过程:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/914139.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/914139.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Elasticsearch混合搜索深度解析(下):执行机制与完整流程

引言 在上篇中&#xff0c;我们发现了KNN结果通过SubSearch机制被保留的关键事实。本篇将继续深入分析混合搜索的执行机制&#xff0c;揭示完整的处理流程&#xff0c;并解答之前的所有疑惑。 深入源码分析 1. SubSearch的执行机制 1.1 KnnScoreDocQueryBuilder的实现 KNN结果被…

Apache HTTP Server 从安装到配置

一、Apache 是什么&#xff1f;Apache&#xff08;全称 Apache HTTP Server&#xff09;是当前最流行的开源Web服务器软件之一&#xff0c;由Apache软件基金会维护。它以稳定性高、模块化设计和灵活的配置著称&#xff0c;支持Linux、Windows等多平台&#xff0c;是搭建个人博客…

php中调用对象的方法可以使用array($object, ‘methodName‘)?

是的&#xff0c;在PHP中&#xff0c;array($object, methodName) 是一种标准的回调语法&#xff0c;用于表示“调用某个对象的特定方法”。这种语法可以被许多函数&#xff08;如 call_user_func()、call_user_func_array()、usort() 等&#xff09;识别并执行。 语法原理 在P…

【设计模式】单例模式 饿汉式单例与懒汉式单例

单例模式&#xff08;Singleton Pattern&#xff09;详解一、单例模式简介 单例模式&#xff08;Singleton Pattern&#xff09; 是一种 创建型设计模式&#xff0c;它确保一个类只有一个实例&#xff0c;并提供一个全局访问点来获取这个实例。&#xff08;对象创建型模式&…

vue3 el-table 行数据沾满格自动换行

在使用 Vue 3 结合 Element Plus 的 <el-table> 组件时&#xff0c;如果你希望当表格中的行数据文本过长时能够自动换行&#xff0c;而不是溢出到其他单元格或简单地截断&#xff0c;你可以通过以下几种方式来实现&#xff1a;方法 1&#xff1a;使用 CSS最简单的方法是通…

windows电脑远程win系统服务器上的wsl2

情况 我自己使用win11笔记本电脑&#xff0c;想要远程win11服务器上的wsl2 我这里只有服务器安装了wsl2&#xff0c;win11笔记本没有安装 因此下面提到的Ubuntu终端指的是win服务器上的wsl2终端 一定要区分是在哪里输入命令&#xff01;&#xff01; 安装SSH 在服务器上&#x…

神经辐射场 (NeRF):重构三维世界的AI新视角

神经辐射场 (NeRF)&#xff1a;重构三维世界的AI新视角 旧金山蜿蜒起伏的街道上&#xff0c;一辆装备12个摄像头的Waymo自动驾驶测试车缓缓驶过。它记录的280万张街景图像并未被简单地拼接成平面地图&#xff0c;而是被输入一个名为Block-NeRF的神经网络。数周后&#xff0c;一…

Kubernetes自动扩缩容方案对比与实践指南

Kubernetes自动扩缩容方案对比与实践指南 随着微服务架构和容器化的广泛采用&#xff0c;Kubernetes 自动扩缩容&#xff08;Autoscaling&#xff09;成为保障生产环境性能稳定与资源高效利用的关键技术。面对水平 Pod 扩缩容、垂直资源调整、集群节点扩缩容以及事件驱动扩缩容…

【CVPR2025】计算机视觉|SIREN: 元学习赋能!突破INR高分辨率图像分类难题

论文地址&#xff1a;https://arxiv.org/pdf/2503.18123v1 代码地址&#xff1a;https://github.com/SanderGielisse/MWT 关注UP CV缝合怪&#xff0c;分享最计算机视觉新即插即用模块&#xff0c;并提供配套的论文资料与代码。 https://space.bilibili.com/473764881 摘要 …

牛客周赛 Round 99

赛时成绩如下&#xff1a;A. Round 99题目描述 对于给定的五位整数&#xff0c;检查其中是否含有数字 99&#xff1b;换句话说&#xff0c;检查是否存在相邻的两个数位&#xff0c;其值均为 。解题思路&#xff1a; 检查相邻的两个数字是否均为9#include <bits/stdc.h> u…

从0到1搭建个人技术博客:用GitHub Pages+Hexo实现

一、为什么要搭建个人技术博客&#xff1f; 在技术圈&#xff0c;拥有个人博客的好处不言而喻&#xff1a; 简历加分项&#xff1a;面试官更青睐有技术沉淀的候选人知识系统化&#xff1a;输出倒逼输入&#xff0c;加深技术理解人脉拓展&#xff1a;吸引同行关注&#xff0c;…

Ubuntu22.04 设置显示存在双屏却无法双屏显示

文章目录一、背景描述二、解决方法一、背景描述 回到工位后&#xff0c;发现昨天离开时还可正常显示的双屏&#xff0c;今早ubuntu22.04 的设置界面显示有双屏&#xff0c;但外接的显示屏无法正常显示。 首先&#xff0c;查看当前图像处理显卡是否为N卡&#xff0c;没错&#…

高亚科技签约奕源金属,助力打造高效智能化采购管理体系

深圳市奕源金属制品有限公司近日&#xff0c;国内企业管理软件服务商高亚科技与深圳市奕源金属制品有限公司&#xff08;以下简称“奕源金属”&#xff09;正式签约&#xff0c;双方将基于高亚科技自主研发的8Manage SRM采购管理系统&#xff0c;共同推动奕源金属采购管理的数字…

数据结构之map

map的基本介绍我们常常把map称之为映射&#xff0c;就是将一个元素&#xff08;通常称之为key键&#xff09;与一个相对应的值&#xff08;通常称之为value&#xff09;关联起来&#xff0c;比如说一个学生的名字&#xff08;key&#xff09;有与之对应的成绩&#xff08;value…

vue3 canvas 选择器 Canvas 增加页面性能

文章目录Vue3 选择器 Canvas 增加页面性能基于Vue3 Composition API和Canvas实现的交互式选择器&#xff0c;支持PC端和移动端的拖动选择、多选取消选择功能vue3组件封装html代码Vue3 选择器 Canvas 增加页面性能 基于Vue3 Composition API和Canvas实现的交互式选择器&#xf…

Python 实战:打造多文件批量重命名工具

引言在实际运维、测试、数据分析、开发流程中&#xff0c;我们经常会处理成百上千条命令操作&#xff0c;例如&#xff1a;各种脚本任务&#xff08;启动、备份、重启、日志查看&#xff09;数据处理流程&#xff08;爬取 → 清洗 → 统计 → 可视化&#xff09;配置自动化&…

设计模式笔记_结构型_代理模式

1. 代理模式介绍代理模式是一种结构型设计模式&#xff0c;它允许你提供一个代理对象来控制对另一个对象的访问。代理对象通常在客户端和目标对象之间起到中介作用&#xff0c;能够在不改变目标对象的前提下增加额外的功能操作&#xff0c;比如延迟初始化、访问控制、日志记录等…

C语言<数据结构-单链表>(收尾)

上篇博客我将基础的尾插、尾删、头插、头删逐一讲解了&#xff0c;这篇博客将对上篇博客进行收尾&#xff0c;讲一下指定位置操作增删以及查找这几个函数&#xff0c;其实大同小异&#xff1a;一.查找函数&#xff1a;查找函数其实就是一个简单的循环遍历&#xff0c;所以不加以…

十年架构心路:从单机到云原生的分布式系统演进史

十年架构心路&#xff1a;从单机到云原生的分布式系统演进史 这里写目录标题十年架构心路&#xff1a;从单机到云原生的分布式系统演进史一、技术生涯的起点&#xff1a;单体架构的黄金时代1.1 典型技术栈1.2 记忆深刻的故障二、分布式架构转型期2.1 服务化拆分实践2.2 分布式事…

使用docker搭建nginx

安装docker 和 docker compose验证docker版本配置docker目录配置代理&#xff0c;使docker能访问外网能否ping通最后直接拉入镜像即可docker pull nginx