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时,你可以在VS Code里装一个YAML插件。它会实时检查缩进和语法错误,省去很多调试时间。

数据结构:列表和字典

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的流式格式,可读性差很多。

注意:PyYAML在dump中文时可能会出乱码。解决办法是在 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配置文件,就是整个硬件寄存器操作的“数据中台”。所有代码生成、文档生成、测试生成,都围绕它来转。

核心思想:把寄存器描述信息从代码中剥离出来,放到YAML文件里。这样当硬件版本更新时,你只需要修改YAML文件,然后重新运行脚本,所有相关代码都会自动更新。这就是自动化操作的魅力所在。

我在一个项目里用这套方案管理了200多个寄存器,覆盖了5个外设模块。每次硬件改版,我只需要花10分钟更新YAML文件,然后一键生成所有代码。以前手动改头文件的日子,一去不复返了。

嗯,YAML这块就聊这么多。下一章咱们会基于这个YAML描述文件,写一个Python脚本来自动生成C语言头文件。到时候你就知道,前期花在YAML设计上的功夫,会带来多大的回报。