配置驱动开发:初探零代码构建嵌入式软件配置工具
前言
在嵌入式软件开发中,硬件初始化与寄存器配置长期依赖人工编写重复代码。以STM32外设初始化为例,开发者需手动完成时钟使能、引脚模式设置、参数配置等步骤,不仅耗时易错(如位掩码写反、模式枚举值混淆),更因代码冗余导致维护成本高——修改一个外设参数可能需同步调整多处代码,稍有不慎便引发硬件异常。
当看到同事通过GUI配置,自动生成底层代码后,其效率提升让我意识到:传统\"手写代码+文档\"的模式已难以适应复杂嵌入式系统的开发需求。为此,我简要探索了一下配置即代码(Configuration-as-Code)方案,目标是通过结构化配置文件与自动化工具链,实现\"零手写代码\"生成高质量嵌入式初始化代码,同时兼顾可维护性与灵活性。
技术选型与核心架构
针对嵌入式开发的特殊性,配置工具需满足三大核心需求:
- 严格的Schema验证机制:确保配置文件符合硬件约束(如引脚复用冲突、参数取值范围),从源头避免无效配置;
- 树形结构清晰表达层级:映射硬件物理层级的嵌套关系(如\"外设→端口→引脚→参数\"),提升配置文件的可读性与可维护性;
- 注释能力便于维护:支持多语言注释(如中文硬件说明、英文API提示),降低团队协作与后续迭代的沟通成本。
架构分层设计
工具采用\"配置模型-数据绑定-代码生成\"三层架构(如图1所示):
- 配置模型层:基于XML Schema(XSD)定义标准化配置结构,约束各字段的取值范围与层级关系;
- 数据绑定层:通过双向数据绑定机制,实现UI操作与XML配置的实时同步;
- 代码生成层:根据目标平台(如STM32系列)的HAL库规范,将XML配置转换为平台相关的初始化代码。
关键技术实现
UI与XML的实时绑定
UI状态与XML数据模型通过双向数据绑定实现同步:
- 当用户在UI修改\"GPIO0模式\"为\"输出\"时,XML中
节点的值立即更新为\"输出\";
- 当通过脚本修改XML中
节点为\"下拉\"时,UI中对应引脚的下拉框会自动选中\"下拉\"选项。
示例XML(STM32_GPIO配置片段):
PA0 输入 N/A N/A 上拉 PA1 输出 推挽 中速 无 PA2 复用 开漏 中速 无
自动转换逻辑
在代码生成阶段,根据目标平台选择对应的映射规则,将通用配置转换为平台特定的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 sysimport reimport osimport xml.etree.ElementTree as ETfrom PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QPushButton, QFileDialog, QMessageBox, QScrollArea)from PyQt6.QtCore import Qtfrom 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] # 优先PA0 pin_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 unused def 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 = None for cfg in self.gpio_configs: if cfg[\"widget\"] == row_widget: config = cfg break if 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_configs if 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 # 构建XML root = 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: return os.makedirs(os.path.dirname(file_path), exist_ok=True) with open(file_path, \"w\", encoding=\"utf-8\") as f: f.write(\'\\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 True def import_xml(self): \"\"\"导入XML配置\"\"\" try: file_path, _ = QFileDialog.getOpenFileName( self, \"导入XML\", \"\", \"XML文件 (*.xml)\" ) if not file_path: return # 清空当前配置 self._clear_configs() # 解析XML tree = 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_widget config[\"pin_combo\"] = pin_combo config[\"mode_combo\"] = mode_combo config[\"type_combo\"] = type_combo config[\"speed_combo\"] = speed_combo config[\"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/PB rcc = 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: return os.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())
以下是迭代过程: