Skip to content

Commit a067b65

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 a067b65

File tree

1 file changed

+286
-0
lines changed

1 file changed

+286
-0
lines changed

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

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
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_pad_pin = pin.get("Name").endswith("_C")
95+
if dual_pad_pin:
96+
# https://github.com/micropython/micropython/pull/13764
97+
pname += "_C"
98+
# Keep just the ADC index and IN channel.
99+
n_index = re.search("\d+", index)[0]
100+
if pname in adc_pins:
101+
if channel not in adc_pins[pname]:
102+
adc_pins[pname][channel] = n_index
103+
else:
104+
# Merge ADC unit together, eg ADC1 and ADC3 becomes ADC13
105+
adc_pins[pname][channel] = "".join(
106+
sorted(adc_pins[pname][channel] + n_index)
107+
)
108+
else:
109+
adc_pins[pname] = {channel: n_index}
110+
111+
# The mcu definition xml file includes a reference to the GPIO definition file
112+
# matching the chip. This is the file with AF data.
113+
for detail in root.findall("./IP[@Name='GPIO']"):
114+
gpio_file = [
115+
gpio
116+
for gpio in mcu_list_all
117+
if gpio["path"].startswith("IP/GPIO") and detail.get("Version") in gpio["path"]
118+
][0]
119+
gpio_file_url = xml_url + gpio_file["path"]
120+
break
121+
122+
if gpio_file_url is None:
123+
raise SystemError("ERROR: Could not find GPIO details in {}".format(mcu_xml_url))
124+
125+
with urllib.request.urlopen(gpio_file_url) as response:
126+
xml_data = response.read()
127+
128+
print("Downloaded:", gpio_file_url)
129+
130+
# Parse all the alternate function mappings from the xml file.
131+
it = ET.iterparse(BytesIO(xml_data))
132+
for _, el in it:
133+
_, _, el.tag = el.tag.rpartition("}")
134+
root = it.root
135+
136+
af_domains = set()
137+
138+
mapping = defaultdict(list)
139+
for pin in root.findall("./GPIO_Pin"):
140+
pname = pin_name(pin.get("Name"))
141+
if pname is None:
142+
continue
143+
_ = mapping[pname] # Ensure pin gets captured / listed
144+
for pin_signal in pin.findall("./PinSignal"):
145+
for specific_param in pin_signal.findall("SpecificParameter"):
146+
if specific_param.get("Name") != "GPIO_AF":
147+
continue
148+
signal_name = pin_signal.get("Name")
149+
af_fn = specific_param.find("./PossibleValue").text
150+
mapping[pname].append((signal_name, af_fn))
151+
af_domains.add(af_fn.split("_")[1])
152+
153+
# Note: "AF0" though "AF15" are somewhat standard across STM range,
154+
# however not all chips use all domains. List them all for consistency however.
155+
af_domains = ["AF%s" % i for i in range(16)] + ["ADC"]
156+
157+
# Format the AF data into appropriate CSV file.
158+
# First row in CSV is the heading, going through all the AF domains
159+
heading = ["Port", "Pin"] + af_domains
160+
col_width = [4] * len(heading)
161+
# Second csv row lists the peripherals handled by that column
162+
category_parts = [defaultdict(set) for _ in range(len(heading))]
163+
# Third row onwards are the individual pin function mappings
164+
rows = []
165+
for pin in sorted(mapping, key=pin_int):
166+
functions = mapping[pin]
167+
row = [""] * len(heading)
168+
row[0] = "Port%s" % pin[1]
169+
row[1] = pin
170+
for signal, af in functions:
171+
# Some signals are loose about whether 1 is declared or not.
172+
signal = signal.replace("CAN_", "CAN1_")
173+
signal = signal.replace("I2S_", "I2S1_")
174+
# / is used later to delineate multiple functions
175+
signal = signal.replace("/", "_")
176+
177+
if signal.startswith("ETH"):
178+
signal = eth_remap(signal)
179+
180+
column = heading.index(re.search("(AF\d+)", af).group(1))
181+
peripheral = re.search("([A-Z1-9]*[A-Z]+)(\d*)(_|$)", signal)
182+
category_parts[column][peripheral[1]].add(peripheral[2])
183+
signal = signal.replace("SYS_", "")
184+
signal = signal.replace("-", "/")
185+
if row[column]:
186+
if signal not in row[column].split("/"):
187+
# multiple af signals per pin
188+
row[column] = "/".join((row[column], signal))
189+
else:
190+
row[column] = signal
191+
192+
if pin in adc_pins:
193+
row[-1] = "/".join(("ADC{}_{}".format(i, ch) for ch, i in adc_pins[pin].items()))
194+
195+
for i, val in enumerate(row):
196+
col_width[i] = max(col_width[i], len(val))
197+
198+
rows.append(row)
199+
200+
if (dual_pad := pin + "_C") in adc_pins:
201+
# Add extra row for dual-pad ADC entry.
202+
row_c = [""] * len(row)
203+
row_c[0] = row[0]
204+
row_c[1] = dual_pad
205+
row_c[-1] = "/".join(
206+
("ADC{}_{}".format(i, ch) for ch, i in adc_pins[dual_pad].items())
207+
)
208+
rows.append(row_c)
209+
210+
# Simplify the category sets, eg TIM1/TIM2 -> TIM1/2
211+
category_head = ["" for _ in range(len(heading))]
212+
for i, periph in enumerate(category_parts):
213+
periphs = []
214+
for key, vals in periph.items():
215+
if len(vals) > 1:
216+
if "" in vals:
217+
vals.remove("")
218+
vals.add("1")
219+
periphs.append(key + "/".join(sorted(vals, key=int)))
220+
else:
221+
periphs.append(key + vals.pop())
222+
223+
category_head[i] = "/".join(sorted(periphs))
224+
col_width[i] = max(col_width[i], len(category_head[i]))
225+
category_head[-1] = "ADC"
226+
227+
# Sort the rows by the first two columns; port and pin number
228+
rows.sort(key=lambda row: (row[0], pin_int(row[1])))
229+
230+
output_file = Path(__file__).parent / ("%s_af.csv" % target.lower())
231+
with output_file.open("w") as out:
232+
print("# This was auto-generated by make-af.csv.py", file=out)
233+
padded_heading = [val.ljust(col_width[i]) for i, val in enumerate(heading)]
234+
print(",".join(padded_heading).strip(), file=out)
235+
categories = [val.ljust(col_width[i]) for i, val in enumerate(category_head)]
236+
print(",".join(categories).rstrip(), file=out)
237+
for row in rows:
238+
padded_row = [val.ljust(col_width[i]) for i, val in enumerate(row)]
239+
print(",".join(padded_row).strip(), file=out)
240+
241+
print("Written:", output_file.resolve())
242+
243+
244+
def pin_int(pname: str):
245+
# Takes a Pin (or AF) name like PA5, PD15 or AF4 and returns the integer component
246+
return int(re.search("[PA][A-Z](\d+)", pname).group(1))
247+
248+
249+
def pin_name(pin: str):
250+
# Filters out just the pin name, eg PB12 from provided pin string
251+
return (re.search("P[A-Z]\d+", pin) or [None])[0]
252+
253+
254+
def eth_remap(signal):
255+
# The AF signal names in stm xml's generally match the names used in datasheets,
256+
# except for ethernet RMII / MII ones...
257+
# This mapping was generated from comparing xml data to datasheet on STM32H573
258+
eth_signals = {
259+
"ETH_MDC": "ETH_MDC",
260+
"ETH_MDIO": "ETH_MDIO",
261+
"ETH_PPS_OUT": "ETH_PPS_OUT",
262+
"ETH_CRS": "ETH_MII_CRS",
263+
"ETH_COL": "ETH_MII_COL",
264+
"ETH_TX_ER": "ETH_MII_TX_ER",
265+
"ETH_RXD2": "ETH_MII_RXD2",
266+
"ETH_RXD3": "ETH_MII_RXD3",
267+
"ETH_TXD3": "ETH_MII_TXD3",
268+
"ETH_RX_ER": "ETH_MII_RX_ER",
269+
"ETH_TXD2": "ETH_MII_TXD2",
270+
"ETH_TX_CLK": "ETH_MII_TX_CLK",
271+
"ETH_RX_CLK": "ETH_MII_RX_CLK",
272+
"ETH_RX_DV": "ETH_MII_RX_DV",
273+
"ETH_REF_CLK": "ETH_RMII_REF_CLK",
274+
"ETH_CRS_DV": "ETH_RMII_CRS_DV",
275+
"ETH_TX_EN": "ETH_MII_TX_EN/ETH_RMII_TX_EN",
276+
"ETH_TXD0": "ETH_MII_TXD0/ETH_RMII_TXD0",
277+
"ETH_TXD1": "ETH_MII_TXD1/ETH_RMII_TXD1",
278+
"ETH_RXD0": "ETH_MII_RXD0/ETH_RMII_RXD0",
279+
"ETH_RXD1": "ETH_MII_RXD1/ETH_RMII_RXD1",
280+
}
281+
return eth_signals.get(signal, signal)
282+
283+
284+
if __name__ == "__main__":
285+
for target in sys.argv[1:]:
286+
generate_af_csv(target)

0 commit comments

Comments
 (0)