├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── demo.png ├── memparse.py ├── pinparse.py ├── src ├── db │ ├── af_f0.rs │ ├── features.rs │ ├── mem_f0.rs │ └── mod.rs ├── generate.rs ├── gpio.rs ├── i2c.rs ├── lib.rs ├── main.rs ├── rcc.rs ├── spi.rs ├── usart.rs └── utils.rs └── tests ├── stm32f042.ioc └── test.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cube2rust" 3 | description = "A tool for generating a rust project from a STM32CubeMX ioc file" 4 | version = "0.0.1" 5 | edition = "2018" 6 | authors = ["Dimitri Polonski"] 7 | repository = "https://github.com/dimpolo/cube2rust/" 8 | license = "MIT OR Apache-2.0" 9 | readme = "README.md" 10 | categories = [ 11 | "command-line-utilities", 12 | "embedded", 13 | "hardware-support", 14 | "config" 15 | ] 16 | keywords = [ 17 | "embedded", 18 | "generator", 19 | "cube", 20 | "stm32", 21 | "hal", 22 | ] 23 | 24 | 25 | [dependencies] 26 | regex = "1.3.7" 27 | fstrings = "0.2.3" 28 | anyhow = "1.0.31" 29 | human-sort = "0.2.2" 30 | phf = { version = "0.8.0", features = ["macros"] } 31 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Dimitri Polonski 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dimitri Polonski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cube2rust 2 | A tool for generating a rust project from a STM32CubeMX ioc file. 3 | 4 | ![demo](demo.png) 5 | 6 | The tool will run `cargo init` in the same directory as the ioc file. 7 | 8 | It will then add dependencies to `Cargo.toml` and generate a `src/main.rs`, `.cargo/config` and `memory.x`. 9 | 10 | Currently, running this tool will overwrite everything, so use with caution. 11 | 12 | ## Installation 13 | ```bash 14 | $ cargo install cube2rust 15 | ``` 16 | ## Usage 17 | From inside a directory containing an ioc file 18 | ```bash 19 | $ cube2rust 20 | ``` 21 | 22 | From anywhere 23 | ```bash 24 | $ cube2rust path/to/project_directory 25 | ``` 26 | 27 | ## Currently supported 28 | * Only STM32F0 29 | * GPIO, RCC, SPI, USART, I2C 30 | 31 | ## License 32 | 33 | Licensed under either of 34 | 35 | * Apache License, Version 2.0 36 | ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 37 | * MIT license 38 | ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 39 | 40 | at your option. 41 | 42 | ### Contribution 43 | 44 | Unless you explicitly state otherwise, any contribution intentionally submitted 45 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 46 | dual licensed as above, without any additional terms or conditions. 47 | -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dimpolo/cube2rust/4d73964b49753e8603e89ff7b462308026ca6e36/demo.png -------------------------------------------------------------------------------- /memparse.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ElementTree 2 | 3 | from natsort import natsorted 4 | 5 | file_path = r'C:\Program Files (x86)\STMicroelectronics\STM32Cube\STM32CubeMX\db\mcu\families.xml' 6 | FEATURE = 'F0' 7 | 8 | 9 | def get_memory_sizes(): 10 | mem_info = {} 11 | 12 | tree = ElementTree.parse(file_path) 13 | root = tree.getroot() 14 | 15 | for family in root: 16 | for subfamily in family: 17 | for mcu in subfamily: 18 | mcu_name = mcu.attrib['RefName'] 19 | if not mcu_name.startswith('STM32' + FEATURE): 20 | continue 21 | 22 | rams = mcu.findall('Ram') 23 | flashs = mcu.findall('Flash') 24 | 25 | assert len(rams) == 1 26 | assert len(flashs) == 1 27 | 28 | flash = flashs[0].text 29 | ram = rams[0].text 30 | 31 | assert mcu_name not in mem_info 32 | mem_info[mcu_name] = {'ram': ram, 'flash': flash} 33 | 34 | return mem_info 35 | 36 | 37 | def get_mem_sizes_string(mem_info: dict): 38 | outstring = 'pub static MEMORY_SIZES: Map<&str, MemSize> = phf_map! {\n' 39 | 40 | for mcu, mem_size in natsorted(mem_info.items()): 41 | flash = mem_size['flash'] 42 | ram = mem_size['ram'] 43 | outstring += f' "{mcu}" => MemSize{{flash: {flash}, ram: {ram}}},\n' 44 | 45 | return outstring + '};\n' 46 | 47 | 48 | if __name__ == '__main__': 49 | mem_info = get_memory_sizes() 50 | mem_sizes_string = get_mem_sizes_string(mem_info) 51 | 52 | imports = 'use phf::{phf_map, Map};\n'\ 53 | 'use super::MemSize;\n\n' 54 | 55 | print(imports + mem_sizes_string) 56 | -------------------------------------------------------------------------------- /pinparse.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ElementTree 2 | import re 3 | from pathlib import Path 4 | 5 | from natsort import natsorted 6 | 7 | dir_path = Path(r'C:\Program Files (x86)\STMicroelectronics\STM32Cube\STM32CubeMX\db\mcu\IP') 8 | FEATURE = 'F0' 9 | 10 | p = '{http://mcd.rou.st.com/modules.php?name=mcu}GPIO_Pin' 11 | s = '{http://mcd.rou.st.com/modules.php?name=mcu}PinSignal' 12 | sp = '{http://mcd.rou.st.com/modules.php?name=mcu}SpecificParameter' 13 | pv = '{http://mcd.rou.st.com/modules.php?name=mcu}PossibleValue' 14 | 15 | re_chip_name = re.compile(r'^GPIO-STM32(\w*)_gpio\w*.xml$') 16 | re_af_name = re.compile(r'^GPIO_(AF\d*)_\w*$') 17 | re_pin_name = re.compile(r'^P[A-K]\d{1,2}') 18 | 19 | 20 | def print_red(*args): 21 | print('\033[31m', end='') 22 | print(*args, end='') 23 | print('\033[0m') 24 | 25 | 26 | def transpose(din): 27 | dout = {} 28 | for key1, inner in din.items(): 29 | for key2, value in inner.items(): 30 | dout.setdefault(key2, {})[key1] = value 31 | return dout 32 | 33 | 34 | def get_pins(): 35 | pins = {} 36 | 37 | for path in dir_path.iterdir(): 38 | if not path.name.startswith('GPIO-STM32' + FEATURE): 39 | continue 40 | chip_type = re_chip_name.match(path.name).group(1) 41 | 42 | tree = ElementTree.parse(str(path)) 43 | root = tree.getroot() 44 | 45 | for pin in root.findall(p): 46 | for signal in pin.findall(s): 47 | for parameter in signal.findall(sp): 48 | for possible_value in parameter.findall(pv): 49 | pin_name = re_pin_name.findall(pin.attrib['Name'])[0] 50 | signal_name = signal.attrib['Name'] 51 | try: 52 | af = re_af_name.match(possible_value.text).group(1) 53 | pins \ 54 | .setdefault(pin_name, {}) \ 55 | .setdefault(signal_name, {}) \ 56 | .setdefault(af, []).append(chip_type) 57 | except AttributeError: 58 | print_red(path, possible_value.text) 59 | 60 | return pins 61 | 62 | 63 | def get_afs(pattern: re.Pattern): 64 | all_pins = get_pins() 65 | all_signals = transpose(all_pins) 66 | 67 | outstring = '' 68 | 69 | for signal, pins in natsorted(all_signals.items()): 70 | if not pattern.match(signal): 71 | continue 72 | 73 | outstring += f'static {signal}: Map<&str, u8> = phf_map! {{\n' 74 | for pin, afs in natsorted(pins.items()): 75 | assert len(afs) == 1 76 | af = list(afs.keys())[0] 77 | outstring += f' "{pin.lower()}" => {af[-1]},\n' 78 | outstring += '};\n\n' 79 | 80 | mapped_signals.append(signal) 81 | 82 | return outstring 83 | 84 | 85 | if __name__ == '__main__': 86 | mapped_signals = [] 87 | 88 | re_spis = re.compile(r'^SPI\d_(SCK|MISO|MOSI)') 89 | re_i2cs = re.compile(r'^I2C\d_(SCL|SDA)') 90 | re_uarts = re.compile(r'^USART\d_(TX|RX)') 91 | 92 | outstring = '' 93 | outstring += get_afs(re_i2cs) 94 | outstring += get_afs(re_spis) 95 | outstring += get_afs(re_uarts) 96 | 97 | af_map_string = 'pub static AF_MAP: Map<&str, &Map<&str, u8>> = phf_map! {\n' 98 | for signal in mapped_signals: 99 | af_map_string += f' "{signal}" => &{signal},\n' 100 | af_map_string += '};\n\n' 101 | 102 | imports = 'use phf::{phf_map, Map};\n\n' 103 | 104 | print(imports + af_map_string + outstring) 105 | -------------------------------------------------------------------------------- /src/db/af_f0.rs: -------------------------------------------------------------------------------- 1 | use phf::{phf_map, Map}; 2 | 3 | pub static AF_MAP: Map<&str, &Map<&str, u8>> = phf_map! { 4 | "I2C1_SCL" => &I2C1_SCL, 5 | "I2C1_SDA" => &I2C1_SDA, 6 | "I2C2_SCL" => &I2C2_SCL, 7 | "I2C2_SDA" => &I2C2_SDA, 8 | "SPI1_MISO" => &SPI1_MISO, 9 | "SPI1_MOSI" => &SPI1_MOSI, 10 | "SPI1_SCK" => &SPI1_SCK, 11 | "SPI2_MISO" => &SPI2_MISO, 12 | "SPI2_MOSI" => &SPI2_MOSI, 13 | "SPI2_SCK" => &SPI2_SCK, 14 | "USART1_RX" => &USART1_RX, 15 | "USART1_TX" => &USART1_TX, 16 | "USART2_RX" => &USART2_RX, 17 | "USART2_TX" => &USART2_TX, 18 | "USART3_RX" => &USART3_RX, 19 | "USART3_TX" => &USART3_TX, 20 | "USART4_RX" => &USART4_RX, 21 | "USART4_TX" => &USART4_TX, 22 | "USART5_RX" => &USART5_RX, 23 | "USART5_TX" => &USART5_TX, 24 | "USART6_RX" => &USART6_RX, 25 | "USART6_TX" => &USART6_TX, 26 | "USART7_RX" => &USART7_RX, 27 | "USART7_TX" => &USART7_TX, 28 | "USART8_RX" => &USART8_RX, 29 | "USART8_TX" => &USART8_TX, 30 | }; 31 | 32 | static I2C1_SCL: Map<&str, u8> = phf_map! { 33 | "pa9" => 4, 34 | "pa11" => 5, 35 | "pb6" => 1, 36 | "pb8" => 1, 37 | "pb10" => 1, 38 | "pb13" => 5, 39 | "pf1" => 1, 40 | }; 41 | 42 | static I2C1_SDA: Map<&str, u8> = phf_map! { 43 | "pa10" => 4, 44 | "pa12" => 5, 45 | "pb7" => 1, 46 | "pb9" => 1, 47 | "pb11" => 1, 48 | "pb14" => 5, 49 | "pf0" => 1, 50 | }; 51 | 52 | static I2C2_SCL: Map<&str, u8> = phf_map! { 53 | "pa11" => 5, 54 | "pb10" => 1, 55 | "pb13" => 5, 56 | }; 57 | 58 | static I2C2_SDA: Map<&str, u8> = phf_map! { 59 | "pa12" => 5, 60 | "pb11" => 1, 61 | "pb14" => 5, 62 | }; 63 | 64 | static SPI1_MISO: Map<&str, u8> = phf_map! { 65 | "pa6" => 0, 66 | "pb4" => 0, 67 | "pb14" => 0, 68 | "pe14" => 1, 69 | }; 70 | 71 | static SPI1_MOSI: Map<&str, u8> = phf_map! { 72 | "pa7" => 0, 73 | "pb5" => 0, 74 | "pb15" => 0, 75 | "pe15" => 1, 76 | }; 77 | 78 | static SPI1_SCK: Map<&str, u8> = phf_map! { 79 | "pa5" => 0, 80 | "pb3" => 0, 81 | "pb13" => 0, 82 | "pe13" => 1, 83 | }; 84 | 85 | static SPI2_MISO: Map<&str, u8> = phf_map! { 86 | "pb14" => 0, 87 | "pc2" => 1, 88 | "pd3" => 1, 89 | }; 90 | 91 | static SPI2_MOSI: Map<&str, u8> = phf_map! { 92 | "pb15" => 0, 93 | "pc3" => 1, 94 | "pd4" => 1, 95 | }; 96 | 97 | static SPI2_SCK: Map<&str, u8> = phf_map! { 98 | "pb10" => 5, 99 | "pb13" => 0, 100 | "pd1" => 1, 101 | }; 102 | 103 | static USART1_RX: Map<&str, u8> = phf_map! { 104 | "pa3" => 1, 105 | "pa10" => 1, 106 | "pa15" => 1, 107 | "pb7" => 0, 108 | }; 109 | 110 | static USART1_TX: Map<&str, u8> = phf_map! { 111 | "pa2" => 1, 112 | "pa9" => 1, 113 | "pa14" => 1, 114 | "pb6" => 0, 115 | }; 116 | 117 | static USART2_RX: Map<&str, u8> = phf_map! { 118 | "pa3" => 1, 119 | "pa15" => 1, 120 | "pd6" => 0, 121 | }; 122 | 123 | static USART2_TX: Map<&str, u8> = phf_map! { 124 | "pa2" => 1, 125 | "pa14" => 1, 126 | "pd5" => 0, 127 | }; 128 | 129 | static USART3_RX: Map<&str, u8> = phf_map! { 130 | "pb11" => 4, 131 | "pc5" => 1, 132 | "pc11" => 1, 133 | "pd9" => 0, 134 | }; 135 | 136 | static USART3_TX: Map<&str, u8> = phf_map! { 137 | "pb10" => 4, 138 | "pc4" => 1, 139 | "pc10" => 1, 140 | "pd8" => 0, 141 | }; 142 | 143 | static USART4_RX: Map<&str, u8> = phf_map! { 144 | "pa1" => 4, 145 | "pc11" => 0, 146 | "pe9" => 1, 147 | }; 148 | 149 | static USART4_TX: Map<&str, u8> = phf_map! { 150 | "pa0" => 4, 151 | "pc10" => 0, 152 | "pe8" => 1, 153 | }; 154 | 155 | static USART5_RX: Map<&str, u8> = phf_map! { 156 | "pb4" => 4, 157 | "pd2" => 2, 158 | "pe11" => 1, 159 | }; 160 | 161 | static USART5_TX: Map<&str, u8> = phf_map! { 162 | "pb3" => 4, 163 | "pc12" => 2, 164 | "pe10" => 1, 165 | }; 166 | 167 | static USART6_RX: Map<&str, u8> = phf_map! { 168 | "pa5" => 5, 169 | "pc1" => 2, 170 | "pf10" => 1, 171 | }; 172 | 173 | static USART6_TX: Map<&str, u8> = phf_map! { 174 | "pa4" => 5, 175 | "pc0" => 2, 176 | "pf9" => 1, 177 | }; 178 | 179 | static USART7_RX: Map<&str, u8> = phf_map! { 180 | "pc1" => 1, 181 | "pc7" => 1, 182 | "pf3" => 1, 183 | }; 184 | 185 | static USART7_TX: Map<&str, u8> = phf_map! { 186 | "pc0" => 1, 187 | "pc6" => 1, 188 | "pf2" => 1, 189 | }; 190 | 191 | static USART8_RX: Map<&str, u8> = phf_map! { 192 | "pc3" => 2, 193 | "pc9" => 1, 194 | "pd14" => 0, 195 | }; 196 | 197 | static USART8_TX: Map<&str, u8> = phf_map! { 198 | "pc2" => 2, 199 | "pc8" => 1, 200 | "pd13" => 0, 201 | }; 202 | -------------------------------------------------------------------------------- /src/db/features.rs: -------------------------------------------------------------------------------- 1 | pub static F0_FEATURES: &[&str] = &[ 2 | "stm32f030x4", 3 | "stm32f030x6", 4 | "stm32f030x8", 5 | "stm32f030xc", 6 | "stm32f031", 7 | "stm32f038", 8 | "stm32f042", 9 | "stm32f048", 10 | "stm32f051", 11 | "stm32f058", 12 | "stm32f070x6", 13 | "stm32f070xb", 14 | "stm32f071", 15 | "stm32f072", 16 | "stm32f078", 17 | "stm32f091", 18 | "stm32f098", 19 | ]; 20 | -------------------------------------------------------------------------------- /src/db/mem_f0.rs: -------------------------------------------------------------------------------- 1 | use super::MemSize; 2 | use phf::{phf_map, Map}; 3 | 4 | pub static MEMORY_SIZES: Map<&str, MemSize> = phf_map! { 5 | "STM32F030C6Tx" => MemSize{flash: 32, ram: 4}, 6 | "STM32F030C8Tx" => MemSize{flash: 64, ram: 8}, 7 | "STM32F030CCTx" => MemSize{flash: 256, ram: 32}, 8 | "STM32F030F4Px" => MemSize{flash: 16, ram: 4}, 9 | "STM32F030K6Tx" => MemSize{flash: 32, ram: 4}, 10 | "STM32F030R8Tx" => MemSize{flash: 64, ram: 8}, 11 | "STM32F030RCTx" => MemSize{flash: 256, ram: 32}, 12 | "STM32F031C4Tx" => MemSize{flash: 16, ram: 4}, 13 | "STM32F031C6Tx" => MemSize{flash: 32, ram: 4}, 14 | "STM32F031E6Yx" => MemSize{flash: 32, ram: 4}, 15 | "STM32F031F4Px" => MemSize{flash: 16, ram: 4}, 16 | "STM32F031F6Px" => MemSize{flash: 32, ram: 4}, 17 | "STM32F031G4Ux" => MemSize{flash: 16, ram: 4}, 18 | "STM32F031G6Ux" => MemSize{flash: 32, ram: 4}, 19 | "STM32F031K4Ux" => MemSize{flash: 16, ram: 4}, 20 | "STM32F031K6Tx" => MemSize{flash: 32, ram: 4}, 21 | "STM32F031K6Ux" => MemSize{flash: 32, ram: 4}, 22 | "STM32F038C6Tx" => MemSize{flash: 32, ram: 4}, 23 | "STM32F038E6Yx" => MemSize{flash: 32, ram: 4}, 24 | "STM32F038F6Px" => MemSize{flash: 32, ram: 4}, 25 | "STM32F038G6Ux" => MemSize{flash: 32, ram: 4}, 26 | "STM32F038K6Ux" => MemSize{flash: 32, ram: 4}, 27 | "STM32F042C4Tx" => MemSize{flash: 16, ram: 6}, 28 | "STM32F042C4Ux" => MemSize{flash: 16, ram: 6}, 29 | "STM32F042C6Tx" => MemSize{flash: 32, ram: 6}, 30 | "STM32F042C6Ux" => MemSize{flash: 32, ram: 6}, 31 | "STM32F042F4Px" => MemSize{flash: 16, ram: 6}, 32 | "STM32F042F6Px" => MemSize{flash: 32, ram: 6}, 33 | "STM32F042G4Ux" => MemSize{flash: 16, ram: 6}, 34 | "STM32F042G6Ux" => MemSize{flash: 32, ram: 6}, 35 | "STM32F042K4Tx" => MemSize{flash: 16, ram: 6}, 36 | "STM32F042K4Ux" => MemSize{flash: 16, ram: 6}, 37 | "STM32F042K6Tx" => MemSize{flash: 32, ram: 6}, 38 | "STM32F042K6Ux" => MemSize{flash: 32, ram: 6}, 39 | "STM32F042T6Yx" => MemSize{flash: 32, ram: 6}, 40 | "STM32F048C6Ux" => MemSize{flash: 32, ram: 6}, 41 | "STM32F048G6Ux" => MemSize{flash: 32, ram: 6}, 42 | "STM32F048T6Yx" => MemSize{flash: 32, ram: 6}, 43 | "STM32F051C4Tx" => MemSize{flash: 16, ram: 8}, 44 | "STM32F051C4Ux" => MemSize{flash: 16, ram: 8}, 45 | "STM32F051C6Tx" => MemSize{flash: 32, ram: 8}, 46 | "STM32F051C6Ux" => MemSize{flash: 32, ram: 8}, 47 | "STM32F051C8Tx" => MemSize{flash: 64, ram: 8}, 48 | "STM32F051C8Ux" => MemSize{flash: 64, ram: 8}, 49 | "STM32F051K4Tx" => MemSize{flash: 16, ram: 8}, 50 | "STM32F051K4Ux" => MemSize{flash: 16, ram: 8}, 51 | "STM32F051K6Tx" => MemSize{flash: 32, ram: 8}, 52 | "STM32F051K6Ux" => MemSize{flash: 32, ram: 8}, 53 | "STM32F051K8Tx" => MemSize{flash: 64, ram: 8}, 54 | "STM32F051K8Ux" => MemSize{flash: 64, ram: 8}, 55 | "STM32F051R4Tx" => MemSize{flash: 16, ram: 8}, 56 | "STM32F051R6Tx" => MemSize{flash: 32, ram: 8}, 57 | "STM32F051R8Hx" => MemSize{flash: 64, ram: 8}, 58 | "STM32F051R8Tx" => MemSize{flash: 64, ram: 8}, 59 | "STM32F051T8Yx" => MemSize{flash: 64, ram: 8}, 60 | "STM32F058C8Ux" => MemSize{flash: 64, ram: 8}, 61 | "STM32F058R8Hx" => MemSize{flash: 64, ram: 8}, 62 | "STM32F058R8Tx" => MemSize{flash: 64, ram: 8}, 63 | "STM32F058T8Yx" => MemSize{flash: 64, ram: 8}, 64 | "STM32F070C6Tx" => MemSize{flash: 32, ram: 6}, 65 | "STM32F070CBTx" => MemSize{flash: 128, ram: 16}, 66 | "STM32F070F6Px" => MemSize{flash: 32, ram: 6}, 67 | "STM32F070RBTx" => MemSize{flash: 128, ram: 16}, 68 | "STM32F071C8Tx" => MemSize{flash: 64, ram: 16}, 69 | "STM32F071C8Ux" => MemSize{flash: 64, ram: 16}, 70 | "STM32F071CBTx" => MemSize{flash: 128, ram: 16}, 71 | "STM32F071CBUx" => MemSize{flash: 128, ram: 16}, 72 | "STM32F071CBYx" => MemSize{flash: 128, ram: 16}, 73 | "STM32F071RBTx" => MemSize{flash: 128, ram: 16}, 74 | "STM32F071V8Hx" => MemSize{flash: 64, ram: 16}, 75 | "STM32F071V8Tx" => MemSize{flash: 64, ram: 16}, 76 | "STM32F071VBHx" => MemSize{flash: 128, ram: 16}, 77 | "STM32F071VBTx" => MemSize{flash: 128, ram: 16}, 78 | "STM32F072C8Tx" => MemSize{flash: 64, ram: 16}, 79 | "STM32F072C8Ux" => MemSize{flash: 64, ram: 16}, 80 | "STM32F072CBTx" => MemSize{flash: 128, ram: 16}, 81 | "STM32F072CBUx" => MemSize{flash: 128, ram: 16}, 82 | "STM32F072CBYx" => MemSize{flash: 128, ram: 16}, 83 | "STM32F072R8Tx" => MemSize{flash: 64, ram: 16}, 84 | "STM32F072RBHx" => MemSize{flash: 128, ram: 16}, 85 | "STM32F072RBIx" => MemSize{flash: 128, ram: 16}, 86 | "STM32F072RBTx" => MemSize{flash: 128, ram: 16}, 87 | "STM32F072V8Hx" => MemSize{flash: 64, ram: 16}, 88 | "STM32F072V8Tx" => MemSize{flash: 64, ram: 16}, 89 | "STM32F072VBHx" => MemSize{flash: 128, ram: 16}, 90 | "STM32F072VBTx" => MemSize{flash: 128, ram: 16}, 91 | "STM32F078CBTx" => MemSize{flash: 128, ram: 16}, 92 | "STM32F078CBUx" => MemSize{flash: 128, ram: 16}, 93 | "STM32F078CBYx" => MemSize{flash: 128, ram: 16}, 94 | "STM32F078RBHx" => MemSize{flash: 128, ram: 16}, 95 | "STM32F078RBTx" => MemSize{flash: 128, ram: 16}, 96 | "STM32F078VBHx" => MemSize{flash: 128, ram: 16}, 97 | "STM32F078VBTx" => MemSize{flash: 128, ram: 16}, 98 | "STM32F091CBTx" => MemSize{flash: 128, ram: 32}, 99 | "STM32F091CBUx" => MemSize{flash: 128, ram: 32}, 100 | "STM32F091CCTx" => MemSize{flash: 256, ram: 32}, 101 | "STM32F091CCUx" => MemSize{flash: 256, ram: 32}, 102 | "STM32F091RBTx" => MemSize{flash: 128, ram: 32}, 103 | "STM32F091RCHx" => MemSize{flash: 256, ram: 32}, 104 | "STM32F091RCTx" => MemSize{flash: 256, ram: 32}, 105 | "STM32F091RCYx" => MemSize{flash: 256, ram: 32}, 106 | "STM32F091VBTx" => MemSize{flash: 128, ram: 32}, 107 | "STM32F091VCHx" => MemSize{flash: 256, ram: 32}, 108 | "STM32F091VCTx" => MemSize{flash: 256, ram: 32}, 109 | "STM32F098CCTx" => MemSize{flash: 256, ram: 32}, 110 | "STM32F098CCUx" => MemSize{flash: 256, ram: 32}, 111 | "STM32F098RCHx" => MemSize{flash: 256, ram: 32}, 112 | "STM32F098RCTx" => MemSize{flash: 256, ram: 32}, 113 | "STM32F098RCYx" => MemSize{flash: 256, ram: 32}, 114 | "STM32F098VCHx" => MemSize{flash: 256, ram: 32}, 115 | "STM32F098VCTx" => MemSize{flash: 256, ram: 32}, 116 | }; 117 | -------------------------------------------------------------------------------- /src/db/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use regex::Regex; 3 | 4 | pub fn get_alternate_function( 5 | mcu_family: MCUFamily, 6 | gpio: &GpioPin, 7 | peripheral_function: &str, 8 | ) -> u8 { 9 | let map = match mcu_family { 10 | MCUFamily::STM32F0 => &af_f0::AF_MAP, 11 | _ => todo!("other AF_MAPs"), 12 | }; 13 | 14 | *map.get(peripheral_function) 15 | .unwrap_or_else(|| todo!("Alternate functions for {}", peripheral_function)) 16 | .get(gpio.register.as_str()) 17 | .unwrap_or_else(|| { 18 | todo!( 19 | "Alternate function for {}:{}", 20 | gpio.register, 21 | peripheral_function 22 | ) 23 | }) 24 | } 25 | 26 | pub struct MemSize { 27 | pub flash: usize, 28 | pub ram: usize, 29 | } 30 | 31 | pub fn get_mem_size(config: &Config) -> &MemSize { 32 | let map = match config.mcu_family { 33 | MCUFamily::STM32F0 => &mem_f0::MEMORY_SIZES, 34 | _ => todo!("other MEMORY_SIZES"), 35 | }; 36 | 37 | map.get(config.mcu_name.as_str()) 38 | .unwrap_or_else(|| todo!("Unknown MCU {}", config.mcu_name)) 39 | } 40 | 41 | pub fn get_feature(config: &Config) -> anyhow::Result<&'static str> { 42 | let mcu_name = config.mcu_name.to_ascii_lowercase(); 43 | 44 | let features = match config.mcu_family { 45 | MCUFamily::STM32F0 => features::F0_FEATURES, 46 | _ => todo!("More features"), 47 | }; 48 | 49 | for feature in features { 50 | // x can be any word character 51 | // "stm32f030x4" -> Regex::new(r"^stm32f030\w4") 52 | let regex = Regex::new(&("^".to_string() + &feature.replace("x", r"\w"))).unwrap(); 53 | 54 | if regex.is_match(&mcu_name) { 55 | return Ok(feature); 56 | } 57 | } 58 | 59 | bail!("no feature for {}", mcu_name) 60 | } 61 | 62 | mod af_f0; 63 | mod features; 64 | mod mem_f0; 65 | -------------------------------------------------------------------------------- /src/generate.rs: -------------------------------------------------------------------------------- 1 | use crate::db::*; 2 | use crate::gpio::*; 3 | use crate::i2c::*; 4 | use crate::rcc::*; 5 | use crate::spi::*; 6 | use crate::usart::*; 7 | use crate::utils::*; 8 | use crate::{Config, MCUFamily}; 9 | 10 | pub fn generate_main(config: &Config) -> anyhow::Result { 11 | let hal = match config.mcu_family { 12 | MCUFamily::STM32F0 => "stm32f0xx_hal", 13 | _ => todo!("Only STM32F0 supported for now"), 14 | }; 15 | 16 | let mut imports = GeneratedString::new(); 17 | 18 | imports.line("#![no_std]"); 19 | imports.line("#![no_main]"); 20 | imports.empty_line(); 21 | imports.line("use crate::hal::{prelude::*, stm32};"); 22 | imports.line("use cortex_m::interrupt;"); 23 | imports.line("use cortex_m_rt::entry;"); 24 | imports.line("use panic_halt as _;"); 25 | imports.line(f!("use {hal} as hal;")); 26 | imports.empty_line(); 27 | 28 | let mut main_func = GeneratedString::new(); 29 | 30 | main_func.line("#[entry]"); 31 | main_func.line("fn main() -> ! {"); 32 | main_func.indent_right(); 33 | 34 | main_func.line("let mut p = stm32::Peripherals::take().unwrap();"); 35 | 36 | add_rcc(&mut main_func, &config); 37 | 38 | add_ports(&mut main_func, &config); 39 | 40 | add_gpios(&mut main_func, &config); 41 | 42 | for spi in config.spis.iter() { 43 | add_spi(&mut main_func, &mut imports, spi); 44 | } 45 | 46 | for usart in config.usarts.iter() { 47 | add_usart(&mut main_func, &mut imports, usart); 48 | } 49 | 50 | for i2c in config.i2cs.iter() { 51 | add_i2c(&mut main_func, &mut imports, i2c); 52 | } 53 | 54 | main_func.line("loop {}"); 55 | 56 | main_func.indent_left(); 57 | main_func.line("}"); 58 | 59 | Ok(imports.string + "\n" + &main_func.string) 60 | } 61 | 62 | fn add_rcc(string: &mut GeneratedString, config: &Config) { 63 | string.line("let mut rcc = p"); 64 | string.indent_right(); 65 | string.line(".RCC"); 66 | string.line(".configure()"); 67 | match config.rcc.clock_source { 68 | ClockSource::HSI => {} 69 | ClockSource::HSI48 => string.line(".hsi48()"), 70 | ClockSource::HSE(HSEMode::NotBypassed(freq)) => string.line(f!( 71 | ".hse({freq}.hz(), hal::rcc::HSEBypassMode::NotBypassed)" 72 | )), 73 | ClockSource::HSE(HSEMode::Bypassed(freq)) => { 74 | string.line(f!(".hse({freq}.hz(), hal::rcc::HSEBypassMode::Bypassed)")) 75 | } 76 | } 77 | 78 | if let Some(sysclk_freq) = config.rcc.sysclk_freq { 79 | string.line(f!(".sysclk({sysclk_freq}.hz())")); 80 | } 81 | if let Some(hclk_freq) = config.rcc.hclk_freq { 82 | string.line(f!(".hclk({hclk_freq}.hz())")); 83 | } 84 | if let Some(apb1_freq) = config.rcc.apb1_freq { 85 | string.line(f!(".pclk({apb1_freq}.hz())")); 86 | } 87 | string.line(".freeze(&mut p.FLASH);"); 88 | string.indent_left(); 89 | string.empty_line(); 90 | } 91 | 92 | fn add_ports(string: &mut GeneratedString, config: &Config) { 93 | for port in config.ports.iter() { 94 | let port_lower = port.to_ascii_lowercase(); 95 | 96 | let gpio_names: Vec<_> = config 97 | .gpios 98 | .iter() 99 | .filter(|gpio| gpio.port.ends_with(*port)) 100 | .map(|gpio| gpio.register.clone()) 101 | .collect(); 102 | 103 | let registers: Vec<_> = gpio_names 104 | .iter() 105 | .map(|name| f!("_{port_lower}.{name}")) 106 | .collect(); 107 | 108 | let gpio_names = gpio_names.join(", "); 109 | let registers = registers.join(", "); 110 | 111 | string.line(f!("let _{port_lower} = p.GPIO{port}.split(&mut rcc);")); 112 | string.line(f!("let ({gpio_names}) = ({registers});")); 113 | } 114 | string.empty_line(); 115 | } 116 | 117 | fn add_gpios(string: &mut GeneratedString, config: &Config) { 118 | for gpio in config.gpios.iter() { 119 | let pin_name = gpio.get_name(); 120 | let pin_configuration = configure_gpio(gpio, config.mcu_family); 121 | 122 | let mutable = if !matches!(&gpio.signal, SignalType::Peripheral(_)) { 123 | "mut " 124 | } else { 125 | "" 126 | }; 127 | 128 | string.line(f!("let {mutable}{pin_name} = {pin_configuration};")); 129 | } 130 | 131 | string.empty_line(); 132 | } 133 | 134 | fn configure_gpio(gpio: &GpioPin, mcu_family: MCUFamily) -> String { 135 | let name = gpio.get_name(); 136 | 137 | let func = match gpio.signal { 138 | SignalType::AdcInput => f!("into_analog"), 139 | SignalType::GpioInput => match gpio.pu_pd.unwrap_or_default() { 140 | PullType::GPIO_NOPULL => f!("into_floating_input"), 141 | PullType::GPIO_PULLUP => f!("into_pull_up_input"), 142 | PullType::GPIO_PULLDOWN => f!("into_pull_down_input"), 143 | }, 144 | SignalType::GpioOutput => match gpio.mode_default_output_pp.unwrap_or_default() { 145 | ModeOutputType::GPIO_MODE_OUTPUT_OD => match gpio.speed.unwrap_or_default() { 146 | SpeedType::GPIO_SPEED_FREQ_LOW => f!("into_open_drain_output"), 147 | _ => todo!("{} higher speeds for", name), 148 | }, 149 | ModeOutputType::GPIO_MODE_OUTPUT_PP => match gpio.speed.unwrap_or_default() { 150 | SpeedType::GPIO_SPEED_FREQ_LOW => f!("into_push_pull_output"), 151 | SpeedType::GPIO_SPEED_FREQ_MEDIUM => f!("into_push_pull_output_hs"), 152 | 153 | _ => todo!("{} higher speeds for", name), 154 | }, 155 | }, 156 | SignalType::Peripheral(ref name) => { 157 | let af = get_alternate_function(mcu_family, gpio, name); 158 | f!("into_alternate_af{af}") 159 | } 160 | }; 161 | f!("interrupt::free(|cs| {gpio.register}.{func}(cs))") 162 | } 163 | 164 | fn add_spi(main_func: &mut GeneratedString, imports: &mut GeneratedString, spi: &SPI) { 165 | let polarity = match spi.polarity.unwrap_or_default() { 166 | CLKPolarity::SPI_POLARITY_LOW => "IdleLow", 167 | CLKPolarity::SPI_POLARITY_HIGH => "IdleHigh", 168 | }; 169 | 170 | let phase = match spi.phase.unwrap_or_default() { 171 | CLKPhase::SPI_PHASE_1EDGE => "CaptureOnFirstTransition", 172 | CLKPhase::SPI_PHASE_2EDGE => "CaptureOnSecondTransition", 173 | }; 174 | 175 | imports.line("use hal::spi::{Spi, Mode, Phase, Polarity};"); 176 | 177 | main_func.line(f!("let mut {spi.name_lower} = Spi::{spi.name_lower}(")); 178 | main_func.indent_right(); 179 | main_func.line(f!("p.{spi.name_upper},")); 180 | main_func.line(f!( 181 | "({spi.name_lower}_sck, {spi.name_lower}_miso, {spi.name_lower}_mosi)," 182 | )); 183 | main_func.line("Mode {"); 184 | main_func.indent_right(); 185 | main_func.line(f!("polarity: Polarity::{polarity},")); 186 | main_func.line(f!("phase: Phase::{phase},")); 187 | main_func.indent_left(); 188 | main_func.line("},"); 189 | main_func.line(f!("{spi.baudrate.0}.hz(),")); 190 | main_func.line("&mut rcc"); 191 | main_func.indent_left(); 192 | main_func.line(");"); 193 | main_func.empty_line(); 194 | } 195 | 196 | fn add_usart(main_func: &mut GeneratedString, imports: &mut GeneratedString, usart: &USART) { 197 | let baudrate = usart.baudrate.unwrap_or(38400); 198 | 199 | imports.line("use hal::serial::Serial;"); 200 | 201 | main_func.line(f!( 202 | "let mut {usart.name_lower} = Serial::{usart.name_lower}(" 203 | )); 204 | main_func.indent_right(); 205 | main_func.line(f!("p.{usart.name_upper},")); 206 | main_func.line(f!("({usart.name_lower}_tx, {usart.name_lower}_rx),")); 207 | main_func.line(f!("{baudrate}.bps(),")); 208 | main_func.line("&mut rcc"); 209 | main_func.indent_left(); 210 | main_func.line(");"); 211 | main_func.empty_line(); 212 | } 213 | 214 | fn add_i2c(main_func: &mut GeneratedString, imports: &mut GeneratedString, i2c: &I2C) { 215 | imports.line("use hal::i2c::I2c;"); 216 | 217 | let speed: u32 = match i2c.mode.unwrap_or_default() { 218 | Mode::I2C_Standard => 100, 219 | Mode::I2C_Fast => 400, 220 | Mode::I2C_Fast_Plus => 1000, 221 | }; 222 | 223 | main_func.line(f!("let mut {i2c.name_lower} = I2c::{i2c.name_lower}(")); 224 | main_func.indent_right(); 225 | main_func.line(f!("p.{i2c.name_upper},")); 226 | main_func.line(f!("({i2c.name_lower}_scl, {i2c.name_lower}_sda),")); 227 | main_func.line(f!("{speed}.khz(),")); 228 | main_func.line("&mut rcc"); 229 | main_func.indent_left(); 230 | main_func.line(");"); 231 | main_func.empty_line(); 232 | } 233 | 234 | pub fn generate_cargo_config(config: &Config) -> String { 235 | let mut file_content = String::from( 236 | r#"[target.'cfg(all(target_arch = "arm", target_os = "none"))'] 237 | # uncomment ONE of these three option to make `cargo run` start a GDB session 238 | # which option to pick depends on your system 239 | # runner = "arm-none-eabi-gdb -q -x openocd.gdb" 240 | # runner = "gdb-multiarch -q -x openocd.gdb" 241 | # runner = "gdb -q -x openocd.gdb" 242 | 243 | rustflags = [ 244 | # LLD (shipped with the Rust toolchain) is used as the default linker 245 | "-C", "link-arg=-Tlink.x", 246 | 247 | # if you run into problems with LLD switch to the GNU linker by commenting out 248 | # this line 249 | # "-C", "linker=arm-none-eabi-ld", 250 | 251 | # if you need to link to pre-compiled C libraries provided by a C toolchain 252 | # use GCC as the linker by commenting out both lines above and then 253 | # uncommenting the three lines below 254 | # "-C", "linker=arm-none-eabi-gcc", 255 | # "-C", "link-arg=-Wl,-Tlink.x", 256 | # "-C", "link-arg=-nostartfiles", 257 | ] 258 | 259 | [build] 260 | "#, 261 | ); 262 | 263 | let target = match config.mcu_family { 264 | MCUFamily::STM32F0 | MCUFamily::STM32L0 | MCUFamily::STM32G0 => "thumbv6m-none-eabi", 265 | MCUFamily::STM32F1 | MCUFamily::STM32F2 | MCUFamily::STM32L1 => "thumbv7m-none-eabi", 266 | MCUFamily::STM32F3 | MCUFamily::STM32F4 => "thumbv7em-none-eabihf", 267 | _ => todo!("find out if it has FPU"), 268 | }; 269 | 270 | file_content.push_str(&f!("target = \"{target}\"\n")); 271 | file_content 272 | } 273 | 274 | pub fn generate_dependencies(config: &Config) -> anyhow::Result { 275 | let hal_crate = match config.mcu_family { 276 | MCUFamily::STM32F0 => "stm32f0xx-hal", 277 | _ => todo!("other hal crates"), 278 | }; 279 | 280 | let feature = get_feature(config)?; 281 | 282 | let mut filecontent = 283 | f!("{hal_crate} = {{version = \"*\", features = [\"{feature}\", \"rt\"]}}"); 284 | 285 | filecontent.push_str( 286 | r#" 287 | cortex-m = "*" 288 | cortex-m-rt = "*" 289 | panic-halt = "*" 290 | 291 | [profile.dev.package."*"] 292 | # opt-level = "z" 293 | 294 | [profile.release] 295 | # lto = true 296 | "#, 297 | ); 298 | 299 | Ok(filecontent) 300 | } 301 | 302 | pub fn generate_memory_x(config: &Config) -> anyhow::Result { 303 | let mem_size = get_mem_size(config); 304 | 305 | Ok(f!("\ 306 | MEMORY 307 | {{ 308 | FLASH : ORIGIN = 0x00000000, LENGTH = {mem_size.flash}K 309 | RAM : ORIGIN = 0x20000000, LENGTH = {mem_size.ram}K 310 | }} 311 | ")) 312 | } 313 | -------------------------------------------------------------------------------- /src/gpio.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | 3 | use regex::Regex; 4 | 5 | use crate::*; 6 | 7 | #[derive(Debug)] 8 | pub struct GpioPin { 9 | pub port: String, 10 | pub register: String, 11 | pub signal: SignalType, 12 | pub label: Option, 13 | pub pin_state: Option, 14 | pub speed: Option, 15 | pub pu_pd: Option, 16 | pub mode_default_output_pp: Option, 17 | } 18 | 19 | pub fn get_gpios(config: &ConfigParams<'_>) -> anyhow::Result<(Vec, Vec)> { 20 | // regex matches PA11, PB4, etc 21 | let re = Regex::new(r"^P[A-K]\d{1,2}").unwrap(); 22 | 23 | // collect regex matches into a params Vec<(match, params)> 24 | let mut gpio_params = Vec::new(); 25 | for (name, params) in config { 26 | if let Some(name_match) = re.find(name) { 27 | gpio_params.push((name_match.as_str(), params)); 28 | } 29 | } 30 | 31 | // sort vec alphanumerically, e.g. PA1, PA2, PA11, PB1 32 | gpio_params.sort_by(|(a, _), (b, _)| human_sort::compare(a, b)); 33 | 34 | // map params to GpioPins 35 | let mut gpios = Vec::new(); 36 | let mut ports = Vec::new(); 37 | for (name, parameters) in gpio_params { 38 | let gpio: GpioPin = GpioPin::new(name, parameters).context(format!("Pin: {}", name))?; 39 | 40 | // Don't count external clock sources as GPIOs 41 | if let SignalType::Peripheral(ref signal) = gpio.signal { 42 | if signal == "RCC_OSC_OUT" || signal == "RCC_OSC_IN" { 43 | continue; 44 | } 45 | }; 46 | 47 | ports.push(gpio.port.chars().last().unwrap()); 48 | gpios.push(gpio); 49 | } 50 | 51 | ports.dedup(); 52 | 53 | Ok((ports, gpios)) 54 | } 55 | 56 | impl GpioPin { 57 | pub fn new(name: &str, parameters: &HashMap<&str, &str>) -> anyhow::Result { 58 | let (port, register) = parse_name(name)?; 59 | 60 | let signal = parse_mandatory_param(parameters, "Signal")?; 61 | let label = parameters.get("GPIO_Label").map(|&s| String::from(s)); 62 | 63 | let pin_state = parse_optional_param(parameters, "PinState")?; 64 | let pu_pd = parse_optional_param(parameters, "GPIO_PuPd")?; 65 | 66 | let speed = parse_optional_param(parameters, "GPIO_Speed")?; 67 | let mode_default_output_pp = parse_optional_param(parameters, "GPIO_ModeDefaultOutputPP")?; 68 | 69 | Ok(GpioPin { 70 | port, 71 | register, 72 | signal, 73 | label, 74 | pin_state, 75 | speed, 76 | pu_pd, 77 | mode_default_output_pp, 78 | }) 79 | } 80 | 81 | pub fn get_name(&self) -> String { 82 | let register = &self.register; 83 | 84 | match &self.label { 85 | Some(label) => label.clone(), 86 | None => match self.signal { 87 | SignalType::Peripheral(ref name) => name.to_lowercase(), 88 | SignalType::AdcInput => f!("adc_{register}"), 89 | _ => f!("gpio_{register}"), 90 | }, 91 | } 92 | } 93 | } 94 | 95 | fn parse_name(name: &str) -> anyhow::Result<(String, String)> { 96 | let port_char_upper = name 97 | .chars() 98 | .nth(1) 99 | .ok_or_else(|| anyhow!("could not parse port"))?; 100 | let pin: u8 = name[2..] 101 | .parse() 102 | .map_err(|_| anyhow!("could not parse pin number"))?; 103 | 104 | let port_char_lower = port_char_upper.to_ascii_lowercase(); 105 | let port = f!("GPIO{port_char_upper}"); 106 | let register = f!("p{port_char_lower}{pin}"); 107 | Ok((port, register)) 108 | } 109 | 110 | #[derive(Debug, PartialEq)] 111 | pub enum SignalType { 112 | GpioInput, 113 | GpioOutput, 114 | AdcInput, 115 | Peripheral(String), 116 | } 117 | 118 | impl TryFrom<&str> for SignalType { 119 | type Error = anyhow::Error; 120 | 121 | fn try_from(text: &str) -> anyhow::Result { 122 | match text { 123 | "GPIO_Input" => Ok(SignalType::GpioInput), 124 | "GPIO_Output" => Ok(SignalType::GpioOutput), 125 | "GPIO_Analog" => Ok(SignalType::AdcInput), 126 | _ => Ok(SignalType::Peripheral(String::from(text))), 127 | } 128 | } 129 | } 130 | 131 | parameter!(PinStateType, [GPIO_PIN_SET, GPIO_PIN_RESET]); 132 | parameter!( 133 | PullType, 134 | [GPIO_PULLUP, GPIO_PULLDOWN, GPIO_NOPULL], 135 | default = GPIO_NOPULL 136 | ); 137 | parameter!( 138 | ModeOutputType, 139 | [GPIO_MODE_OUTPUT_OD, GPIO_MODE_OUTPUT_PP], 140 | default = GPIO_MODE_OUTPUT_PP 141 | ); 142 | parameter!( 143 | SpeedType, 144 | [ 145 | GPIO_SPEED_FREQ_LOW, 146 | GPIO_SPEED_FREQ_MEDIUM, 147 | GPIO_SPEED_FREQ_HIGH, 148 | GPIO_SPEED_FREQ_VERY_HIGH 149 | ], 150 | default = GPIO_SPEED_FREQ_LOW 151 | ); 152 | -------------------------------------------------------------------------------- /src/i2c.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use regex::Regex; 3 | 4 | #[derive(Debug)] 5 | pub struct I2C { 6 | pub name_lower: String, 7 | pub name_upper: String, 8 | pub mode: Option, 9 | } 10 | 11 | pub fn get_i2cs(config: &ConfigParams<'_>) -> anyhow::Result> { 12 | let mut i2cs = Vec::new(); 13 | 14 | // regex matches I2C1_SDA, I2C2_SDA, etc 15 | let re = Regex::new(r"^(I2C\d)_SDA").unwrap(); 16 | 17 | // if the I2C is left at default values CubeMX will not generate an entry for it in the ioc file 18 | // so we search for a I2C_SDA signal set on a GPIO 19 | for v in config.values() { 20 | if let Some(signal) = v.get("Signal") { 21 | if let Some(captures) = re.captures(signal) { 22 | let name_upper = String::from(captures.get(1).unwrap().as_str()); 23 | let name_lower = name_upper.to_ascii_lowercase(); 24 | let mut mode = None; 25 | 26 | if let Some(i2c_params) = config.get::(&name_upper) { 27 | mode = parse_optional_param(i2c_params, "I2C_Speed_Mode")?; 28 | } 29 | 30 | i2cs.push(I2C { 31 | name_lower, 32 | name_upper, 33 | mode, 34 | }); 35 | } 36 | }; 37 | } 38 | 39 | Ok(i2cs) 40 | } 41 | 42 | parameter!( 43 | Mode, 44 | [I2C_Standard, I2C_Fast, I2C_Fast_Plus], 45 | default = I2C_Standard 46 | ); 47 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A tool for generating a rust project from a STM32CubeMX ioc file. 2 | //! 3 | //! The tool will run `cargo init` in the same directory as the ioc file. 4 | //! 5 | //! It will then add dependencies to `Cargo.toml` and generate a `src/main.rs`, `.cargo/config` and `memory.x`. 6 | //! 7 | //! Currently, running this tool will overwrite everything, so use with caution. 8 | //! 9 | //! # Installation 10 | //! ```bash 11 | //! $ cargo install cube2rust 12 | //! ``` 13 | //! # Usage 14 | //! From inside a directory containing an ioc file 15 | //! ```bash 16 | //! $ cube2rust 17 | //! ``` 18 | //! 19 | //! From anywhere 20 | //! ```bash 21 | //! $ cube2rust path/to/project_directory 22 | //! ``` 23 | //! 24 | //! # Currently supported 25 | //! * Only STM32F0 26 | //! * GPIO, RCC, SPI, USART, I2C 27 | 28 | #![warn(rust_2018_idioms)] 29 | 30 | #[macro_use] 31 | extern crate fstrings; 32 | 33 | #[macro_use] 34 | mod utils; 35 | mod db; 36 | mod generate; 37 | mod gpio; 38 | mod i2c; 39 | mod rcc; 40 | mod spi; 41 | mod usart; 42 | 43 | use std::collections::HashMap; 44 | use std::fs; 45 | use std::fs::OpenOptions; 46 | use std::io::Write; 47 | use std::path::Path; 48 | use std::process::Command; 49 | 50 | use anyhow::{anyhow, bail, ensure, Context}; 51 | 52 | use crate::gpio::GpioPin; 53 | use crate::i2c::I2C; 54 | use crate::rcc::RCC; 55 | use crate::spi::SPI; 56 | use crate::usart::USART; 57 | use crate::utils::*; 58 | 59 | type ConfigParams<'a> = HashMap<&'a str, HashMap<&'a str, &'a str>>; 60 | 61 | /// A struct containing all the collected information from the ioc file 62 | #[derive(Debug)] 63 | pub struct Config { 64 | pub version: String, 65 | pub mcu_family: MCUFamily, 66 | pub mcu_name: String, 67 | pub rcc: RCC, 68 | pub gpios: Vec, 69 | pub ports: Vec, 70 | pub spis: Vec, 71 | pub usarts: Vec, 72 | pub i2cs: Vec, 73 | } 74 | 75 | /// Loads a project configuration from the ioc file content 76 | pub fn load_ioc(file_content: &str) -> anyhow::Result { 77 | let config_params = parse_ioc(file_content); 78 | 79 | let version = String::from( 80 | *config_params 81 | .get("File") 82 | .ok_or_else(|| anyhow!("Couldn't check ioc version"))? 83 | .get("Version") 84 | .ok_or_else(|| anyhow!("Couldn't check ioc version"))?, 85 | ); 86 | 87 | let mcu = config_params 88 | .get("Mcu") 89 | .ok_or_else(|| anyhow!("Couldn't check MCU information"))?; 90 | 91 | let mcu_family = parse_mandatory_param(mcu, "Family")?; 92 | 93 | let mcu_name = mcu 94 | .get("UserName") 95 | .ok_or_else(|| anyhow!("Couldn't check MCU name"))? 96 | .to_string(); 97 | 98 | let rcc = rcc::get_rcc(&config_params).context("Parsing of RCC")?; 99 | 100 | let (ports, gpios) = gpio::get_gpios(&config_params).context("Parsing of GPIOs")?; 101 | 102 | let spis = spi::get_spis(&config_params).context("Parsing of SPIs")?; 103 | 104 | let usarts = usart::get_usarts(&config_params).context("Parsing of USARTs")?; 105 | 106 | let i2cs = i2c::get_i2cs(&config_params).context("Parsing of I2Cs")?; 107 | 108 | Ok(Config { 109 | version, 110 | mcu_family, 111 | mcu_name, 112 | rcc, 113 | gpios, 114 | ports, 115 | spis, 116 | usarts, 117 | i2cs, 118 | }) 119 | } 120 | 121 | /// Parses the ioc file content into nested HashMaps 122 | pub fn parse_ioc(file_content: &str) -> ConfigParams<'_> { 123 | let mut config_params = HashMap::new(); 124 | 125 | for line in file_content.lines() { 126 | let name_and_value: Vec<&str> = line.split('=').collect(); 127 | 128 | if let [name, value] = name_and_value[..] { 129 | let object_and_parameter: Vec<&str> = name.split('.').collect(); 130 | if let [object_name, parameter_name] = object_and_parameter[..] { 131 | config_params 132 | .entry(object_name) 133 | .or_insert_with(HashMap::new) 134 | .insert(parameter_name, value); 135 | } 136 | } 137 | } 138 | 139 | config_params 140 | } 141 | 142 | fn cargo_init(project_dir: &Path) -> anyhow::Result { 143 | let output = if project_dir.eq(Path::new("")) { 144 | // empty path as current_dir doesn't work, not sure why 145 | Command::new("cargo").arg("init").output() 146 | } else { 147 | Command::new("cargo") 148 | .arg("init") 149 | .current_dir(project_dir) 150 | .output() 151 | } 152 | .context("cargo init")?; 153 | 154 | let output = String::from_utf8(output.stderr).unwrap(); 155 | Ok(output.contains("Created binary (application) package")) 156 | } 157 | 158 | /// Generates a rust project from the given configuration 159 | pub fn generate(project_dir: &Path, config: Config) -> anyhow::Result<()> { 160 | ensure!( 161 | config.version == "6", 162 | "only File.Version=6 supported in ioc file" 163 | ); 164 | 165 | // run cargo init 166 | let package_created = cargo_init(project_dir)?; 167 | 168 | // append to Cargo.toml 169 | // TODO replace this with calls to cargo add, once cargo #5586 is through 170 | if package_created { 171 | println!("Ran cargo init"); 172 | let cargo_toml = project_dir.join("Cargo.toml"); 173 | let mut file = OpenOptions::new().append(true).open(cargo_toml)?; 174 | 175 | let dependencies = generate::generate_dependencies(&config)?; 176 | write!(file, "{}", dependencies)?; 177 | println!("Added dependencies to Cargo.toml"); 178 | } else { 179 | println!("Detected existing project"); 180 | } 181 | 182 | // src/main.rs 183 | let main_rs = generate::generate_main(&config)?; 184 | println!("Generated src/main.rs"); 185 | 186 | let path_to_main = project_dir.join("src/main.rs"); 187 | fs::write(path_to_main, main_rs).context("write to main.rs")?; 188 | 189 | // .cargo/config 190 | let cargo_config = generate::generate_cargo_config(&config); 191 | 192 | let path_to_cargo_cofig = project_dir.join(".cargo/config"); 193 | fs::create_dir_all(path_to_cargo_cofig.parent().unwrap()).unwrap(); 194 | fs::write(path_to_cargo_cofig, cargo_config).context("write to config")?; 195 | println!("Generated .cargo/config"); 196 | 197 | // memory.x 198 | let memory_config = generate::generate_memory_x(&config)?; 199 | 200 | let path_to_memory_x = project_dir.join("memory.x"); 201 | fs::write(path_to_memory_x, memory_config).context("write to memory.x")?; 202 | println!("Generated memory.x"); 203 | 204 | Ok(()) 205 | } 206 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | use std::{env, fs, panic, process}; 3 | 4 | use anyhow::anyhow; 5 | 6 | fn get_path_to_ioc_file(dir: &Path) -> anyhow::Result { 7 | let mut path_to_ioc_file: Option = None; 8 | 9 | for entry in dir.read_dir()? { 10 | let entry = entry?; 11 | match entry.file_name().to_str() { 12 | None => {} 13 | Some(filename) => { 14 | if filename.ends_with(".ioc") { 15 | if path_to_ioc_file.is_none() { 16 | path_to_ioc_file = Some(entry.path()); 17 | } else { 18 | return Err(anyhow!("More than one .ioc file")); 19 | } 20 | } 21 | } 22 | } 23 | } 24 | 25 | path_to_ioc_file.ok_or_else(|| anyhow!("No .ioc file fond")) 26 | } 27 | 28 | fn run() -> anyhow::Result<()> { 29 | let args: Vec = env::args().collect(); 30 | let default_path = String::new(); 31 | let project_dir = Path::new(args.get(1).unwrap_or(&default_path)); 32 | 33 | let path_to_ioc_file: PathBuf = get_path_to_ioc_file(project_dir)?; 34 | 35 | println!("Found ioc file {:?}", path_to_ioc_file.file_name().unwrap()); 36 | let filecontent = fs::read_to_string(path_to_ioc_file)?; 37 | 38 | let config = cube2rust::load_ioc(&filecontent)?; 39 | println!("Loaded ioc file"); 40 | 41 | cube2rust::generate(project_dir, config) 42 | } 43 | 44 | fn main() -> anyhow::Result<()> { 45 | panic::catch_unwind(run).unwrap_or_else(|_| { 46 | println!( 47 | " 48 | Something went wrong or is not implemented yet. 49 | Please submit an issue: https://github.com/dimpolo/cube2rust" 50 | ); 51 | process::exit(1); 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /src/rcc.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub fn get_rcc(config: &ConfigParams<'_>) -> anyhow::Result { 4 | let rcc_params = config 5 | .get("RCC") 6 | .ok_or_else(|| anyhow!("No RCC configuration found"))?; 7 | 8 | // SYSCLK Config 9 | let sys_clock_source = parse_optional_param(rcc_params, "SYSCLKSource")?; 10 | let pll_clock_source = parse_optional_param(rcc_params, "PLLSourceVirtual")?; 11 | // ioc file does not set SYSCLKFreq_VALUE if it's HSI 8Mhz 12 | let sysclk_freq = parse_optional_u32(rcc_params, "SYSCLKFreq_VALUE")?; 13 | let hclk_freq = parse_optional_u32(rcc_params, "HCLKFreq_Value")?; 14 | let apb1_freq = parse_optional_u32(rcc_params, "APB1Freq_Value")?; 15 | 16 | let clock_source = get_clock_source(&sys_clock_source, &pll_clock_source, config)?; 17 | 18 | Ok(RCC { 19 | clock_source, 20 | sysclk_freq, 21 | hclk_freq, 22 | apb1_freq, 23 | }) 24 | } 25 | 26 | fn get_clock_source( 27 | sys_clock_source: &Option, 28 | pll_clock_source: &Option, 29 | config: &ConfigParams<'_>, 30 | ) -> anyhow::Result { 31 | match sys_clock_source { 32 | None | Some(SYSCLKSourceType::RCC_SYSCLKSOURCE_HSI) => Ok(ClockSource::HSI), 33 | Some(SYSCLKSourceType::RCC_SYSCLKSOURCE_HSI48) => Ok(ClockSource::HSI48), 34 | Some(SYSCLKSourceType::RCC_SYSCLKSOURCE_HSE) => Ok(ClockSource::HSE(get_hse_mode(config)?)), 35 | Some(SYSCLKSourceType::RCC_SYSCLKSOURCE_PLLCLK) => match pll_clock_source { 36 | None | Some(PLLSourceType::RCC_PLLSOURCE_HSI) => Ok(ClockSource::HSI), 37 | Some(PLLSourceType::RCC_PLLSOURCE_HSI48) => Ok(ClockSource::HSI48), 38 | Some(PLLSourceType::RCC_PLLSOURCE_HSE) => Ok(ClockSource::HSE(get_hse_mode(config)?)), 39 | }, 40 | } 41 | } 42 | 43 | fn get_hse_mode(config: &ConfigParams<'_>) -> anyhow::Result { 44 | // RCC existance was checked already 45 | let rcc_params = config.get("RCC").unwrap(); 46 | 47 | // get mode from pin configuration 48 | let &mode = config 49 | .get("PF0-OSC_IN") 50 | .map(|pfo| pfo.get("Mode")) 51 | .ok_or_else(|| anyhow!("PF0-OSC_IN required"))? 52 | .ok_or_else(|| anyhow!("PF0-OSC_IN.Mode required"))?; 53 | // get freq 54 | let freq = parse_mandatory_u32(rcc_params, "VCOOutput2Freq_Value")?; 55 | 56 | let mode: anyhow::Result = match mode { 57 | "HSE-External-Oscillator" => Ok(HSEMode::NotBypassed(freq)), 58 | "HSE-External-Clock-Source" => Ok(HSEMode::Bypassed(freq)), 59 | _ => bail!("Unknown clock source"), 60 | }; 61 | mode.context("Parsing of external clock source") 62 | } 63 | 64 | #[derive(Debug)] 65 | pub struct RCC { 66 | // TODO USBClockSource 67 | // TODO CRS 68 | pub clock_source: ClockSource, 69 | pub sysclk_freq: Option, 70 | pub hclk_freq: Option, 71 | pub apb1_freq: Option, 72 | } 73 | 74 | #[derive(Debug, PartialEq)] 75 | pub enum ClockSource { 76 | HSI, 77 | HSI48, 78 | HSE(HSEMode), 79 | } 80 | 81 | #[derive(Debug, PartialEq)] 82 | pub enum HSEMode { 83 | NotBypassed(u32), 84 | Bypassed(u32), 85 | } 86 | 87 | parameter!( 88 | SYSCLKSourceType, 89 | [ 90 | RCC_SYSCLKSOURCE_HSI, 91 | RCC_SYSCLKSOURCE_HSI48, 92 | RCC_SYSCLKSOURCE_PLLCLK, 93 | RCC_SYSCLKSOURCE_HSE 94 | ], 95 | default = RCC_SYSCLKSOURCE_HSI 96 | ); 97 | 98 | parameter!( 99 | PLLSourceType, 100 | [RCC_PLLSOURCE_HSI, RCC_PLLSOURCE_HSI48, RCC_PLLSOURCE_HSE] 101 | ); 102 | -------------------------------------------------------------------------------- /src/spi.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use std::convert::TryFrom; 3 | 4 | const MAX_SPIS: u8 = 6; 5 | 6 | #[derive(Debug)] 7 | pub struct SPI { 8 | pub name_lower: String, 9 | pub name_upper: String, 10 | pub phase: Option, 11 | pub polarity: Option, 12 | pub prescaler: BaudRatePrescaler, 13 | pub baudrate: BaudRate, 14 | } 15 | 16 | pub fn get_spis(config: &ConfigParams<'_>) -> anyhow::Result> { 17 | let mut spis = Vec::new(); 18 | 19 | for i in 1..=MAX_SPIS { 20 | let name_lower = f!("spi{i}"); 21 | let name_upper = name_lower.to_ascii_uppercase(); 22 | 23 | // search config for SPI1, SPI2, etc.. 24 | if let Some(spi_params) = config.get::(&name_upper) { 25 | let prescaler = parse_mandatory_param(spi_params, "BaudRatePrescaler")?; 26 | let baudrate = parse_mandatory_param(spi_params, "CalculateBaudRate")?; 27 | let phase = parse_optional_param(spi_params, "CLKPhase")?; 28 | let polarity = parse_optional_param(spi_params, "CLKPolarity")?; 29 | 30 | spis.push(SPI { 31 | name_lower, 32 | name_upper, 33 | phase, 34 | polarity, 35 | prescaler, 36 | baudrate, 37 | }); 38 | } 39 | } 40 | Ok(spis) 41 | } 42 | 43 | #[derive(Debug, Copy, Clone, PartialEq)] 44 | pub struct BaudRate(pub u32); 45 | 46 | impl TryFrom<&str> for BaudRate { 47 | type Error = anyhow::Error; 48 | 49 | fn try_from(string: &str) -> Result { 50 | // string looks like this 3.0 MBits/s 51 | let parts: Vec<&str> = string.split(' ').collect(); 52 | 53 | let (&freq, &unit) = match &parts[..] { 54 | [freq, unit] => (freq, unit), 55 | _ => bail!("{}", string), 56 | }; 57 | 58 | let freq: f32 = freq 59 | .parse() 60 | .map_err(|_| anyhow!("{} invalid Baudrate", string))?; 61 | 62 | let freq = match unit { 63 | "MBits/s" => freq * 1_000_000f32, 64 | "kBits/s" => freq * 1_000f32, 65 | "Bits/s" => freq, 66 | _ => bail!("Unknown unit {}", string), 67 | }; 68 | 69 | Ok(BaudRate(freq as u32)) 70 | } 71 | } 72 | 73 | parameter!( 74 | CLKPhase, 75 | [SPI_PHASE_1EDGE, SPI_PHASE_2EDGE], 76 | default = SPI_PHASE_1EDGE 77 | ); 78 | 79 | parameter!( 80 | CLKPolarity, 81 | [SPI_POLARITY_LOW, SPI_POLARITY_HIGH], 82 | default = SPI_POLARITY_LOW 83 | ); 84 | 85 | parameter!( 86 | BaudRatePrescaler, 87 | [ 88 | SPI_BAUDRATEPRESCALER_2, 89 | SPI_BAUDRATEPRESCALER_4, 90 | SPI_BAUDRATEPRESCALER_8, 91 | SPI_BAUDRATEPRESCALER_16, 92 | SPI_BAUDRATEPRESCALER_32, 93 | SPI_BAUDRATEPRESCALER_64, 94 | SPI_BAUDRATEPRESCALER_128, 95 | SPI_BAUDRATEPRESCALER_256 96 | ] 97 | ); 98 | -------------------------------------------------------------------------------- /src/usart.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | const MAX_USARTS: u8 = 8; 4 | 5 | #[derive(Debug)] 6 | pub struct USART { 7 | pub name_lower: String, 8 | pub name_upper: String, 9 | pub baudrate: Option, 10 | } 11 | 12 | pub fn get_usarts(config: &ConfigParams<'_>) -> anyhow::Result> { 13 | let mut usarts = Vec::new(); 14 | 15 | for i in 1..=MAX_USARTS { 16 | let name_lower = f!("usart{i}"); 17 | let name_upper = name_lower.to_ascii_uppercase(); 18 | 19 | // search config for USART1, USART2, etc.. 20 | if let Some(usart_params) = config.get::(&name_upper) { 21 | let baudrate = parse_optional_u32(usart_params, "BaudRate")?; 22 | 23 | usarts.push(USART { 24 | name_lower, 25 | name_upper, 26 | baudrate, 27 | }); 28 | } 29 | } 30 | Ok(usarts) 31 | } 32 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::convert::TryFrom; 3 | 4 | use anyhow::{anyhow, bail}; 5 | 6 | pub fn parse_optional_param<'a, T>( 7 | parameters: &'a HashMap<&str, &str>, 8 | param_name: &str, 9 | ) -> anyhow::Result, anyhow::Error> 10 | where 11 | T: TryFrom<&'a str, Error = anyhow::Error>, 12 | { 13 | parameters 14 | .get(param_name) 15 | .map(|s| T::try_from(*s)) 16 | .transpose() 17 | } 18 | 19 | pub fn parse_mandatory_param<'a, T>( 20 | parameters: &'a HashMap<&str, &str>, 21 | param_name: &str, 22 | ) -> anyhow::Result 23 | where 24 | T: TryFrom<&'a str, Error = anyhow::Error>, 25 | { 26 | let ¶m = parameters 27 | .get(param_name) 28 | .ok_or_else(|| anyhow!("{} parameter required", param_name))?; 29 | T::try_from(param) 30 | } 31 | 32 | pub fn parse_mandatory_u32( 33 | parameters: &HashMap<&str, &str>, 34 | param_name: &str, 35 | ) -> anyhow::Result { 36 | Ok(parameters 37 | .get(param_name) 38 | .ok_or_else(|| anyhow!(f!("{param_name} parameter not found")))? 39 | .parse() 40 | .map_err(|_| anyhow!(f!("{param_name} parameter invalid integer")))?) 41 | } 42 | 43 | pub fn parse_optional_u32( 44 | parameters: &HashMap<&str, &str>, 45 | param_name: &str, 46 | ) -> anyhow::Result> { 47 | parameters 48 | .get(param_name) 49 | .map(|s| { 50 | s.parse() 51 | .map_err(|_| anyhow!(f!("{param_name} parameter invalid integer"))) 52 | }) 53 | .transpose() 54 | } 55 | 56 | // Generate an enum with an TryFrom<&str> implementation that converts from a string to a enum variant 57 | // enum will also derive Debug, Copy, Clone, PartialEq 58 | macro_rules! parameter { 59 | ($enumname:ident, [$($variant: ident), *]) => { 60 | #[allow(non_camel_case_types)] 61 | #[derive(Debug, Copy, Clone, PartialEq)] 62 | pub enum $enumname { 63 | $( 64 | $variant, 65 | )* 66 | } 67 | 68 | impl std::convert::TryFrom<&str> for $enumname { 69 | type Error = anyhow::Error; 70 | 71 | fn try_from(text: &str) -> anyhow::Result { 72 | match text { 73 | $( 74 | stringify!($variant) => Ok($enumname::$variant), 75 | )* 76 | _ => bail!(concat!("invalid ", stringify!($enumname), " {}"), text), 77 | } 78 | } 79 | } 80 | }; 81 | ($enumname:ident, [$($variant: ident), *], default=$default_variant: ident) => { 82 | parameter!($enumname, [$($variant), *]); 83 | impl Default for $enumname { 84 | fn default() -> Self { 85 | $enumname::$default_variant 86 | } 87 | } 88 | }; 89 | } 90 | 91 | parameter!( 92 | MCUFamily, 93 | [ 94 | STM32F0, STM32F1, STM32F2, STM32F3, STM32F4, STM32F7, STM32G0, STM32G4, STM32H7, STM32L0, 95 | STM32L1, STM32L4, STM32L5, STM32MP1, STM32WB, STM32WL 96 | ] 97 | ); 98 | 99 | pub struct GeneratedString { 100 | pub string: String, 101 | indent: usize, 102 | } 103 | 104 | impl GeneratedString { 105 | pub fn new() -> Self { 106 | GeneratedString { 107 | string: String::new(), 108 | indent: 0, 109 | } 110 | } 111 | 112 | pub fn line>(&mut self, line: T) { 113 | let indent = " ".repeat(self.indent); 114 | let content = line.as_ref(); 115 | self.string.push_str(&f!("{indent}{content}\n")); 116 | } 117 | 118 | pub fn empty_line(&mut self) { 119 | self.string.push_str("\n"); 120 | } 121 | 122 | pub fn indent_right(&mut self) { 123 | self.indent += 4; 124 | } 125 | 126 | pub fn indent_left(&mut self) { 127 | self.indent = self.indent.saturating_sub(4); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /tests/stm32f042.ioc: -------------------------------------------------------------------------------- 1 | #MicroXplorer Configuration settings - do not modify 2 | File.Version=6 3 | GPIO.groupedBy=Group By Peripherals 4 | I2C1.I2C_Speed_Mode=I2C_Fast_Plus 5 | I2C1.IPParameters=I2C_Speed_Mode,Timing 6 | I2C1.Timing=0x00000001 7 | KeepUserPlacement=false 8 | Mcu.Family=STM32F0 9 | Mcu.IP0=I2C1 10 | Mcu.IP1=NVIC 11 | Mcu.IP2=RCC 12 | Mcu.IP3=SPI1 13 | Mcu.IP4=SYS 14 | Mcu.IP5=USART1 15 | Mcu.IPNb=6 16 | Mcu.Name=STM32F042C(4-6)Tx 17 | Mcu.Package=LQFP48 18 | Mcu.Pin0=PC15OSC32_OUT 19 | Mcu.Pin1=PF0-OSC_IN 20 | Mcu.Pin10=PB14 21 | Mcu.Pin11=PA9 22 | Mcu.Pin12=PA10 23 | Mcu.Pin13=PA12 24 | Mcu.Pin14=PA13 25 | Mcu.Pin15=VP_SYS_VS_Systick 26 | Mcu.Pin2=PF1-OSC_OUT 27 | Mcu.Pin3=PA5 28 | Mcu.Pin4=PA6 29 | Mcu.Pin5=PA7 30 | Mcu.Pin6=PB10 31 | Mcu.Pin7=PB11 32 | Mcu.Pin8=PB12 33 | Mcu.Pin9=PB13 34 | Mcu.PinsNb=16 35 | Mcu.ThirdPartyNb=0 36 | Mcu.UserConstants= 37 | Mcu.UserName=STM32F042C6Tx 38 | MxCube.Version=5.6.0 39 | MxDb.Version=DB.5.0.60 40 | NVIC.ForceEnableDMAVector=true 41 | NVIC.HardFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false 42 | NVIC.NonMaskableInt_IRQn=true\:0\:0\:false\:false\:true\:false\:false 43 | NVIC.PendSV_IRQn=true\:0\:0\:false\:false\:true\:false\:false 44 | NVIC.SVC_IRQn=true\:0\:0\:false\:false\:true\:false\:false 45 | NVIC.SysTick_IRQn=true\:0\:0\:false\:false\:true\:false\:true 46 | PA10.Mode=Asynchronous 47 | PA10.Signal=USART1_RX 48 | PA12.GPIOParameters=GPIO_PuPd,GPIO_Label 49 | PA12.GPIO_Label=in_1 50 | PA12.GPIO_PuPd=GPIO_PULLUP 51 | PA12.Locked=true 52 | PA12.Signal=GPIO_Input 53 | PA13.Locked=true 54 | PA13.Signal=GPIO_Input 55 | PA5.Mode=Full_Duplex_Master 56 | PA5.Signal=SPI1_SCK 57 | PA6.Mode=Full_Duplex_Master 58 | PA6.Signal=SPI1_MISO 59 | PA7.Mode=Full_Duplex_Master 60 | PA7.Signal=SPI1_MOSI 61 | PA9.Mode=Asynchronous 62 | PA9.Signal=USART1_TX 63 | PB10.Mode=I2C 64 | PB10.Signal=I2C1_SCL 65 | PB11.Mode=I2C 66 | PB11.Signal=I2C1_SDA 67 | PB12.GPIOParameters=GPIO_PuPd 68 | PB12.GPIO_PuPd=GPIO_PULLUP 69 | PB12.Locked=true 70 | PB12.Signal=GPIO_Output 71 | PB13.GPIOParameters=GPIO_Speed,PinState,GPIO_Label 72 | PB13.GPIO_Label=out_1 73 | PB13.GPIO_Speed=GPIO_SPEED_FREQ_MEDIUM 74 | PB13.Locked=true 75 | PB13.PinState=GPIO_PIN_SET 76 | PB13.Signal=GPIO_Output 77 | PB14.GPIOParameters=GPIO_Speed,GPIO_PuPd,GPIO_Label,GPIO_ModeDefaultOutputPP 78 | PB14.GPIO_Label=out_2 79 | PB14.GPIO_ModeDefaultOutputPP=GPIO_MODE_OUTPUT_OD 80 | PB14.GPIO_PuPd=GPIO_PULLUP 81 | PB14.GPIO_Speed=GPIO_SPEED_FREQ_LOW 82 | PB14.Locked=true 83 | PB14.Signal=GPIO_Output 84 | PC15OSC32_OUT.Locked=true 85 | PC15OSC32_OUT.Signal=GPIO_Analog 86 | PF0-OSC_IN.Mode=HSE-External-Clock-Source 87 | PF0-OSC_IN.Signal=RCC_OSC_IN 88 | PF1-OSC_OUT.GPIOParameters=GPIO_Label 89 | PF1-OSC_OUT.GPIO_Label=adc_1 90 | PF1-OSC_OUT.Locked=true 91 | PF1-OSC_OUT.Signal=GPIO_Analog 92 | PinOutPanel.RotationAngle=0 93 | ProjectManager.AskForMigrate=true 94 | ProjectManager.BackupPrevious=false 95 | ProjectManager.CompilerOptimize=6 96 | ProjectManager.ComputerToolchain=false 97 | ProjectManager.CoupleFile=true 98 | ProjectManager.CustomerFirmwarePackage= 99 | ProjectManager.DefaultFWLocation=true 100 | ProjectManager.DeletePrevious=true 101 | ProjectManager.DeviceId=STM32F042C6Tx 102 | ProjectManager.FirmwarePackage=STM32Cube FW_F0 V1.11.0 103 | ProjectManager.FreePins=false 104 | ProjectManager.HalAssertFull=false 105 | ProjectManager.HeapSize=0x100 106 | ProjectManager.KeepUserCode=true 107 | ProjectManager.LastFirmware=true 108 | ProjectManager.LibraryCopy=1 109 | ProjectManager.MainLocation=Core/Src 110 | ProjectManager.NoMain=false 111 | ProjectManager.PreviousToolchain= 112 | ProjectManager.ProjectBuild=false 113 | ProjectManager.ProjectFileName=stm32f042.ioc 114 | ProjectManager.ProjectName=stm32f042 115 | ProjectManager.StackSize=0x200 116 | ProjectManager.TargetToolchain=STM32CubeIDE 117 | ProjectManager.ToolChainLocation= 118 | ProjectManager.UnderRoot=true 119 | ProjectManager.functionlistsort=1-MX_GPIO_Init-GPIO-false-HAL-true,2-SystemClock_Config-RCC-false-HAL-false,3-MX_CAN_Init-CAN-false-HAL-true 120 | RCC.AHBFreq_Value=48000000 121 | RCC.APB1Freq_Value=48000000 122 | RCC.APB1TimFreq_Value=48000000 123 | RCC.CECFreq_Value=32786.88524590164 124 | RCC.FCLKCortexFreq_Value=48000000 125 | RCC.FamilyName=M 126 | RCC.HCLKFreq_Value=48000000 127 | RCC.HSICECFreq_Value=32786.88524590164 128 | RCC.I2SFreq_Value=48000000 129 | RCC.IPParameters=AHBFreq_Value,APB1Freq_Value,APB1TimFreq_Value,CECFreq_Value,FCLKCortexFreq_Value,FamilyName,HCLKFreq_Value,HSICECFreq_Value,I2SFreq_Value,MCOFreq_Value,PLLCLKFreq_Value,PLLMCOFreq_Value,PLLMUL,PLLSourceVirtual,SYSCLKFreq_VALUE,SYSCLKSource,TimSysFreq_Value,USART1Freq_Value,VCOOutput2Freq_Value 130 | RCC.MCOFreq_Value=48000000 131 | RCC.PLLCLKFreq_Value=48000000 132 | RCC.PLLMCOFreq_Value=48000000 133 | RCC.PLLMUL=RCC_PLL_MUL6 134 | RCC.PLLSourceVirtual=RCC_PLLSOURCE_HSE 135 | RCC.SYSCLKFreq_VALUE=48000000 136 | RCC.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK 137 | RCC.TimSysFreq_Value=48000000 138 | RCC.USART1Freq_Value=48000000 139 | RCC.VCOOutput2Freq_Value=8000000 140 | SPI1.BaudRatePrescaler=SPI_BAUDRATEPRESCALER_2 141 | SPI1.CalculateBaudRate=24.0 MBits/s 142 | SPI1.Direction=SPI_DIRECTION_2LINES 143 | SPI1.IPParameters=VirtualType,Mode,Direction,BaudRatePrescaler,CalculateBaudRate 144 | SPI1.Mode=SPI_MODE_MASTER 145 | SPI1.VirtualType=VM_MASTER 146 | USART1.BaudRate=9600 147 | USART1.IPParameters=BaudRate,VirtualMode-Asynchronous 148 | USART1.VirtualMode-Asynchronous=VM_ASYNC 149 | VP_SYS_VS_Systick.Mode=SysTick 150 | VP_SYS_VS_Systick.Signal=SYS_VS_Systick 151 | board=custom 152 | -------------------------------------------------------------------------------- /tests/test.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::process::{Command, Stdio}; 3 | 4 | const IOC_FILE: &str = "tests/stm32f042.ioc"; 5 | 6 | /// makes a test_project folder, copies IOC_FILE to it, 7 | /// runs cube2rust in it and then tries to build 8 | /// 9 | /// run this "test" with --nocapture 10 | #[test] 11 | fn integration_test() { 12 | fs::create_dir_all("../test_project/").expect("Failed to create test_project directory"); 13 | fs::copy(IOC_FILE, "../test_project/test_project.ioc").expect("Failed to copy ioc file"); 14 | 15 | Command::new("cargo") 16 | .arg("run") 17 | .arg("--color=always") 18 | .arg("--") 19 | .arg("../test_project") 20 | .stdout(Stdio::inherit()) 21 | .stderr(Stdio::inherit()) 22 | .output() 23 | .expect("Failed to execute cube2rust"); 24 | 25 | Command::new("cargo") 26 | .arg("build") 27 | .arg("--color=always") 28 | .current_dir("../test_project") 29 | .stdout(Stdio::inherit()) 30 | .stderr(Stdio::inherit()) 31 | .output() 32 | .expect("Failed to execute cargo build"); 33 | } 34 | 35 | /// Load in ioc file, parse it and print out parsed config 36 | #[test] 37 | fn test_config() { 38 | let filecontent = fs::read_to_string(IOC_FILE).expect("read failed"); 39 | 40 | let config = cube2rust::load_ioc(&filecontent); 41 | 42 | dbg!(&config); 43 | 44 | assert!(config.is_ok()); 45 | } 46 | 47 | /// Load in ioc file and print it 48 | #[test] 49 | fn test_params() { 50 | let file_content = fs::read_to_string(IOC_FILE).expect("read failed"); 51 | 52 | let config_params = cube2rust::parse_ioc(&file_content); 53 | 54 | dbg!(config_params); 55 | } 56 | --------------------------------------------------------------------------------