Skip to content

Commit c897b6c

Browse files
committed
stm32/make-af-csv: Add tool to generate af csv files.
Uses official online source: github/STM32_open_pin_data. Signed-off-by: Andrew Leech <[email protected]>
1 parent 7e14680 commit c897b6c

File tree

1 file changed

+275
-0
lines changed

1 file changed

+275
-0
lines changed

ports/stm32/boards/make-af-csv.py

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
#!/usr/bin/env python
2+
"""
3+
Generates MCU alternate-function (AF) definition files for stm32 parts
4+
using the official sources from:
5+
https://github.com/STMicroelectronics/STM32_open_pin_data
6+
7+
Usage:
8+
$ python3 make-af-csv.py <mcu_name>
9+
10+
Example:
11+
$ python3 make-af-csv.py stm32wb55
12+
13+
"""
14+
import json
15+
import re
16+
import sys
17+
import urllib.request
18+
import xml.etree.ElementTree as ET
19+
from collections import defaultdict
20+
from io import BytesIO
21+
from pathlib import Path
22+
23+
24+
# https://docs.github.com/en/rest/git/trees?apiVersion=2022-11-28#get-a-tree
25+
# Note this API has a limit of 60 (unauthenticated) requests per hour.
26+
repo_url = "https://api.github.com/repos/STMicroelectronics/STM32_open_pin_data/git/trees/"
27+
28+
# Raw xml files can be downloaded here
29+
xml_url = "https://raw.githubusercontent.com/STMicroelectronics/STM32_open_pin_data/master/mcu/"
30+
31+
32+
mcu_list_all = None
33+
34+
35+
def generate_af_csv(target: str) -> None:
36+
"""
37+
Generates an AF CSV file based on the given target.
38+
39+
Parameters:
40+
- target (str): The mcu to generate the AF CSV for.
41+
42+
"""
43+
global mcu_list_all
44+
45+
print("Generating AF file for:", target)
46+
cpu = target.rstrip("x").upper()
47+
48+
if mcu_list_all is None:
49+
with urllib.request.urlopen(repo_url + "master") as response:
50+
repo_list_all = json.load(response)["tree"]
51+
mcu_list_url = [entry for entry in repo_list_all if entry["path"] == "mcu"][0]["url"]
52+
with urllib.request.urlopen(mcu_list_url + "?recursive=1") as response:
53+
mcu_list_all = json.load(response)["tree"]
54+
55+
# Get list of all mcu files matching the target
56+
mcu_list = [mcu for mcu in mcu_list_all if mcu.get("path").upper().startswith(cpu)]
57+
if not mcu_list:
58+
raise SystemExit('ERROR: Could not find mcu "{}" on: \n{}'.format(cpu, mcu_list_url))
59+
60+
# Check the mcu definition files for one of the matching mcus,
61+
# pick largest to include all possible pins.
62+
gpio_file_url = None
63+
mcu_xml_len = 0
64+
mcu_url = None
65+
for mcu in mcu_list:
66+
mcu_xml_url = xml_url + mcu["path"]
67+
with urllib.request.urlopen(mcu_xml_url) as response:
68+
length = int(response.headers["content-length"])
69+
if length > mcu_xml_len:
70+
mcu_xml_len = length
71+
mcu_url = mcu_xml_url
72+
73+
with urllib.request.urlopen(mcu_url) as response:
74+
mcuxmlstr = response.read()
75+
print("Downloaded:", mcu_url)
76+
77+
# The STM xml files have a dummy namespace declared which confuses ElementTree.
78+
# This wrapper iter removes them.
79+
it = ET.iterparse(BytesIO(mcuxmlstr))
80+
for _, el in it:
81+
_, _, el.tag = el.tag.rpartition("}") # strip ns
82+
root = it.root
83+
84+
# This mcu definition file declares which pins have ADC pins. Gather them now.
85+
adc_pins = dict()
86+
for pin in root.findall("./Pin"):
87+
for sig in pin.findall("./Signal"):
88+
sig_name = sig.get("Name")
89+
if sig_name.startswith("ADC") and "_IN" in sig_name:
90+
# On some parts they omit the 1 on the first ADC peripheral
91+
sig_name = sig_name.replace("ADC_", "ADC1_")
92+
index, channel = sig_name.split("_")
93+
pname = pin_name(pin.get("Name"))
94+
dual_pin = pin.get("Name").endswith("_C")
95+
ptype = "C_ADC" if dual_pin else "ADC"
96+
tc = (ptype, channel)
97+
# Keep just the ADC index and IN channel.
98+
n_index = re.search("\d+", index)[0]
99+
if pname in adc_pins:
100+
if tc not in adc_pins[pname]:
101+
adc_pins[pname][tc] = n_index
102+
else:
103+
# Merge ADC unit together, eg ADC1 and ADC3 becomes ADC13
104+
adc_pins[pname][tc] = "".join(sorted(adc_pins[pname][tc] + n_index))
105+
else:
106+
adc_pins[pname] = {tc: n_index}
107+
108+
# The mcu definition xml file includes a reference to the GPIO definition file
109+
# matching the chip. This is the file with AF data.
110+
for detail in root.findall("./IP[@Name='GPIO']"):
111+
gpio_file = [
112+
gpio
113+
for gpio in mcu_list_all
114+
if gpio["path"].startswith("IP/GPIO") and detail.get("Version") in gpio["path"]
115+
][0]
116+
gpio_file_url = xml_url + gpio_file["path"]
117+
break
118+
119+
if gpio_file_url is None:
120+
raise SystemError("ERROR: Could not find GPIO details in {}".format(mcu_xml_url))
121+
122+
with urllib.request.urlopen(gpio_file_url) as response:
123+
xml_data = response.read()
124+
125+
print("Downloaded:", gpio_file_url)
126+
127+
# Parse all the alternate function mappings from the xml file.
128+
it = ET.iterparse(BytesIO(xml_data))
129+
for _, el in it:
130+
_, _, el.tag = el.tag.rpartition("}")
131+
root = it.root
132+
133+
af_domains = set()
134+
135+
mapping = defaultdict(list)
136+
for pin in root.findall("./GPIO_Pin"):
137+
pname = pin_name(pin.get("Name"))
138+
if pname is None:
139+
continue
140+
_ = mapping[pname] # Ensure pin gets captured / listed
141+
for pin_signal in pin.findall("./PinSignal"):
142+
for specific_param in pin_signal.findall("SpecificParameter"):
143+
if specific_param.get("Name") != "GPIO_AF":
144+
continue
145+
signal_name = pin_signal.get("Name")
146+
af_fn = specific_param.find("./PossibleValue").text
147+
mapping[pname].append((signal_name, af_fn))
148+
af_domains.add(af_fn.split("_")[1])
149+
150+
# Note: "AF0" though "AF15" are somewhat standard across STM range,
151+
# however not all chips use all domains. List them all for consistency however.
152+
af_domains = ["AF%s" % i for i in range(16)] + ["ADC"]
153+
154+
# Format the AF data into appropriate CSV file.
155+
# First row in CSV is the heading, going through all the AF domains
156+
heading = ["Port", "Pin"] + af_domains
157+
col_width = [4] * len(heading)
158+
# Second csv row lists the peripherals handled by that column
159+
category_parts = [defaultdict(set) for _ in range(len(heading))]
160+
# Third row onwards are the individual pin function mappings
161+
rows = []
162+
for pin in sorted(mapping, key=pin_int):
163+
functions = mapping[pin]
164+
row = [""] * len(heading)
165+
row[0] = "Port%s" % pin[1]
166+
row[1] = pin
167+
for signal, af in functions:
168+
# Some signals are loose about whether 1 is declared or not.
169+
signal = signal.replace("CAN_", "CAN1_")
170+
signal = signal.replace("I2S_", "I2S1_")
171+
# / is used later to delineate multiple functions
172+
signal = signal.replace("/", "_")
173+
174+
if signal.startswith("ETH"):
175+
signal = eth_remap(signal)
176+
177+
column = heading.index(re.search("(AF\d+)", af).group(1))
178+
peripheral = re.search("([A-Z1-9]*[A-Z]+)(\d*)(_|$)", signal)
179+
category_parts[column][peripheral[1]].add(peripheral[2])
180+
signal = signal.replace("SYS_", "")
181+
signal = signal.replace("-", "/")
182+
if row[column]:
183+
if signal not in row[column].split("/"):
184+
# multiple af signals per pin
185+
row[column] = "/".join((row[column], signal))
186+
else:
187+
row[column] = signal
188+
189+
if pin in adc_pins:
190+
row[-1] = "/".join(
191+
("{}{}_{}".format(pt, i, ch) for (pt, ch), i in adc_pins[pin].items())
192+
)
193+
194+
for i, val in enumerate(row):
195+
col_width[i] = max(col_width[i], len(val))
196+
197+
rows.append(row)
198+
199+
# Simplify the category sets, eg TIM1/TIM2 -> TIM1/2
200+
category_head = ["" for _ in range(len(heading))]
201+
for i, periph in enumerate(category_parts):
202+
periphs = []
203+
for key, vals in periph.items():
204+
if len(vals) > 1:
205+
if "" in vals:
206+
vals.remove("")
207+
vals.add("1")
208+
periphs.append(key + "/".join(sorted(vals, key=int)))
209+
else:
210+
periphs.append(key + vals.pop())
211+
212+
category_head[i] = "/".join(sorted(periphs))
213+
col_width[i] = max(col_width[i], len(category_head[i]))
214+
category_head[-1] = "ADC"
215+
216+
# Sort the rows by the first two columns; port and pin number
217+
rows.sort(key=lambda row: (row[0], pin_int(row[1])))
218+
219+
output_file = Path(__file__).parent / ("%s_af.csv" % target.lower())
220+
with output_file.open("w") as out:
221+
print("# This was auto-generated by make-af.csv.py", file=out)
222+
padded_heading = [val.ljust(col_width[i]) for i, val in enumerate(heading)]
223+
print(",".join(padded_heading).strip(), file=out)
224+
categories = [val.ljust(col_width[i]) for i, val in enumerate(category_head)]
225+
print(",".join(categories).rstrip(), file=out)
226+
for row in rows:
227+
padded_row = [val.ljust(col_width[i]) for i, val in enumerate(row)]
228+
print(",".join(padded_row).strip(), file=out)
229+
230+
print("Written:", output_file.resolve())
231+
232+
233+
def pin_int(pname: str):
234+
# Takes a Pin (or AF) name like PA5, PD15 or AF4 and returns the integer component
235+
return int(re.search("[PA][A-Z](\d+)", pname).group(1))
236+
237+
238+
def pin_name(pin: str):
239+
# Filters out just the pin name, eg PB12 from provided pin string
240+
return (re.search("P[A-Z]\d+", pin) or [None])[0]
241+
242+
243+
def eth_remap(signal):
244+
# The AF signal names in stm xml's generally match the names used in datasheets,
245+
# except for ethernet RMII / MII ones...
246+
# This mapping was generated from comparing xml data to datasheet on STM32H573
247+
eth_signals = {
248+
"ETH_MDC": "ETH_MDC",
249+
"ETH_MDIO": "ETH_MDIO",
250+
"ETH_PPS_OUT": "ETH_PPS_OUT",
251+
"ETH_CRS": "ETH_MII_CRS",
252+
"ETH_COL": "ETH_MII_COL",
253+
"ETH_TX_ER": "ETH_MII_TX_ER",
254+
"ETH_RXD2": "ETH_MII_RXD2",
255+
"ETH_RXD3": "ETH_MII_RXD3",
256+
"ETH_TXD3": "ETH_MII_TXD3",
257+
"ETH_RX_ER": "ETH_MII_RX_ER",
258+
"ETH_TXD2": "ETH_MII_TXD2",
259+
"ETH_TX_CLK": "ETH_MII_TX_CLK",
260+
"ETH_RX_CLK": "ETH_MII_RX_CLK",
261+
"ETH_RX_DV": "ETH_MII_RX_DV",
262+
"ETH_REF_CLK": "ETH_RMII_REF_CLK",
263+
"ETH_CRS_DV": "ETH_RMII_CRS_DV",
264+
"ETH_TX_EN": "ETH_MII_TX_EN/ETH_RMII_TX_EN",
265+
"ETH_TXD0": "ETH_MII_TXD0/ETH_RMII_TXD0",
266+
"ETH_TXD1": "ETH_MII_TXD1/ETH_RMII_TXD1",
267+
"ETH_RXD0": "ETH_MII_RXD0/ETH_RMII_RXD0",
268+
"ETH_RXD1": "ETH_MII_RXD1/ETH_RMII_RXD1",
269+
}
270+
return eth_signals.get(signal, signal)
271+
272+
273+
if __name__ == "__main__":
274+
for target in sys.argv[1:]:
275+
generate_af_csv(target)

0 commit comments

Comments
 (0)