4、YAML配置文件:YAML语法入门、数据结构(列表/字典)、使用PyYAML解析与生成、寄存器描述文件设计。
做嵌入式这么多年,我接触过的配置文件格式少说也有七八种。INI、JSON、XML……各有各的毛病。直到我遇见了YAML,说实话,有点相见恨晚的感觉。
YAML,全称是“YAML Ain‘t Markup Language”。它不搞那些花里胡哨的标签,也不需要用大括号把数据包得严严实实。它的核心思想就一个——用缩进来表达层级关系。你想想看,这不就是我们平时写代码、写文档时最自然的习惯吗?
我个人习惯把YAML叫做“人类友好的数据格式”。为什么?因为它读起来就像是在看一份结构清晰的笔记。对于咱们搞硬件寄存器配置的,YAML简直就是量身定做的。
YAML语法入门:别被缩进吓到
YAML的语法规则其实很少,但有几个关键点你必须刻在脑子里。我在项目中见过太多因为缩进问题导致的bug了。
- 缩进只能用空格,不能用Tab。这是铁律。我曾经有一次排查了整整一个下午,最后发现是编辑器自动把空格转成了Tab。嗯,从那以后我写YAML之前都会先检查编辑器设置。
- 键值对用冒号加空格。比如
name: timer0。冒号后面必须跟一个空格,不然解析器会报错。 - 注释用井号。和Python一样,
# 这是注释。我建议你在每个寄存器字段旁边都加上注释,一个月后你自己回来看,会感谢现在的你。 - 字符串可以不加引号。除非字符串里包含特殊字符,比如冒号或者井号。这时候用单引号或双引号包起来就行。
数据结构:列表和字典
YAML里最常用的两种数据结构,说白了就是列表和字典。这两个东西组合起来,能表达任何复杂的配置信息。
字典(Mapping):就是键值对的集合。用冒号表示。
# 一个寄存器描述
register:
name: CTRL_REG
address: 0x40001000
size: 32
access: read-write
列表(Sequence):就是一组有序的元素。用短横线加空格表示。
# 多个寄存器
registers:
- name: CTRL_REG
address: 0x40001000
- name: STATUS_REG
address: 0x40001004
- name: DATA_REG
address: 0x40001008
你发现没有?列表里的每一项本身又是一个字典。这就是YAML最强大的地方——嵌套组合。你可以把列表嵌套在字典里,也可以把字典嵌套在列表里,想怎么玩都行。
使用PyYAML解析与生成
在Python里操作YAML,最常用的库就是PyYAML。安装很简单:pip install pyyaml。然后就可以开始玩了。
读取YAML文件:
import yaml
with open('registers.yaml', 'r') as f:
data = yaml.safe_load(f)
print(data['registers'][0]['name']) # 输出: CTRL_REG
这里我强烈建议你用 yaml.safe_load() 而不是 yaml.load()。为什么?因为 safe_load 更安全,不会执行YAML文件里可能包含的恶意代码。虽然咱们自己写的配置文件不会有问题,但养成好习惯总没错。
生成YAML文件:
import yaml
reg_config = {
'module': 'UART',
'registers': [
{'name': 'TX_REG', 'address': 0x40002000, 'fields': 8},
{'name': 'RX_REG', 'address': 0x40002004, 'fields': 8},
{'name': 'BAUD_REG', 'address': 0x40002008, 'fields': 16}
]
}
with open('uart_regs.yaml', 'w') as f:
yaml.dump(reg_config, f, default_flow_style=False)
default_flow_style=False 这个参数很重要。它会让生成的YAML保持块状格式,也就是我们习惯的那种缩进风格。如果设为True,它会输出类似JSON的流式格式,可读性差很多。
yaml.dump() 里加上 allow_unicode=True 参数。我踩过这个坑,写出来给大家提个醒。
寄存器描述文件设计
好了,前面都是热身。现在咱们来聊聊正事——怎么用YAML设计一个寄存器描述文件。这是我在实际项目中反复打磨出来的方案,你可以直接拿去用。
一个完整的寄存器描述文件,应该包含以下信息:
| 层级 | 内容 | 说明 |
|---|---|---|
| 模块级 | 模块名称、基地址、描述 | 比如UART、SPI、GPIO |
| 寄存器级 | 寄存器名称、偏移地址、宽度、访问权限 | 每个模块下有多个寄存器 |
| 字段级 | 字段名称、位范围、默认值、描述 | 每个寄存器下有多个位域 |
下面是一个完整的示例:
# GPIO模块寄存器描述文件
module: GPIO
base_address: 0x40010000
description: 通用输入输出控制器
registers:
- name: GPIO_OUT
offset: 0x00
width: 32
access: write-only
description: 输出数据寄存器
fields:
- name: OUT_PIN
bits: [0, 31]
default: 0x00000000
description: 输出引脚状态
- name: GPIO_IN
offset: 0x04
width: 32
access: read-only
description: 输入数据寄存器
fields:
- name: IN_PIN
bits: [0, 31]
default: 0x00000000
description: 输入引脚状态
- name: GPIO_DIR
offset: 0x08
width: 32
access: read-write
description: 方向控制寄存器
fields:
- name: DIR_PIN
bits: [0, 31]
default: 0x00000000
description: 0=输入, 1=输出
这个结构的好处是什么?你想想看,有了这个YAML文件,你可以:
- 自动生成C语言的头文件(宏定义、结构体)
- 自动生成Python的寄存器操作类
- 自动生成文档(Markdown、HTML、PDF)
- 自动生成测试用例
说白了,一份YAML配置文件,就是整个硬件寄存器操作的“数据中台”。所有代码生成、文档生成、测试生成,都围绕它来转。
我在一个项目里用这套方案管理了200多个寄存器,覆盖了5个外设模块。每次硬件改版,我只需要花10分钟更新YAML文件,然后一键生成所有代码。以前手动改头文件的日子,一去不复返了。
嗯,YAML这块就聊这么多。下一章咱们会基于这个YAML描述文件,写一个Python脚本来自动生成C语言头文件。到时候你就知道,前期花在YAML设计上的功夫,会带来多大的回报。