Best way to start STM32 project: CubeIDE Empty vs CubeMX vs CMake - Page 1
Forum section: electronics / general

author: flab99
author_rank: Newbie
timestamp: September 10, 2025, 08:44:37 am
content: Hi all,I’m starting new STM32 projects and I’m wondering what is the best setup workflow:STM32CubeIDE “Empty Project” (basic HAL startup, managed makefile).STM32CubeMX + generated project (auto-peripheral setup).Manual Empty Project with CMake (for modern workflow, CI/CD, VS Code/CLion).My use case: I already have some experience with embedded systems, but I want to adopt a clean and scalable project structure that works well long term. I’d like something that integrates nicely with version control and possibly CI/CD, but I also don’t want to waste time reinventing the wheel if CubeIDE is “good enough.”So my question is:What do you recommend as the best starting point for STM32 projects in 2025? Should I stick with CubeIDE, use CubeMX and then migrate, or set up CMake from day one?Thanks for your advice!

author: radiolistener
author_rank: Super Contributor
timestamp: September 10, 2025, 09:00:25 am
content: It’s hard to say definitively what’s “best” for someone starting out. Personally, I find the code generated by CubeMX/CubeIDE often includes a lot of boilerplate and clutter, so I prefer starting with a clean CMake-based project where I implement all initialization and peripheral setup myself. I usually run CubeMX just to see how it initializes peripherals and then replicate that logic in my own code.That said, this approach can be tedious and somewhat challenging, especially for beginners. If you’re looking for something that’s simpler to get started with and avoids reinventing the wheel, sticking with CubeIDE (or CubeMX-generated projects) is a reasonable choice. It allows you to focus on your application logic early

author: Siwastaja
author_rank: Super Contributor
timestamp: September 10, 2025, 09:12:48 am
content: Manual empty project without cmake.

author: Smokey
author_rank: Super Contributor
timestamp: September 10, 2025, 09:19:38 am
content: STM32CubeMX + generated project (auto-peripheral setup).Get it running, then refactor if there is a compelling reason to do so. Don't waste your time writing code that can be autogenerated if you aren't up against a real and unavoidable constraint. You will spend less time optimizing than you would writing from scratch. Take out as much HAL stuff as you want once it's running.CubeIDE is fine. More than good enough.

author: peter-h
author_rank: Super Contributor
timestamp: September 11, 2025, 07:15:18 am
content: My last project was started by somebody else using Cube MX, then MX was dropped and only Cube IDE used since. He then used MX here and there to generate code snippets, took them and dumped the MX project.I've never used MX during my few years on it.MX is good for configuring pins, setting up the PLL for the clock, clock config, etc.

author: SiliconWizard
author_rank: Super Contributor
timestamp: September 11, 2025, 01:17:56 pm
content: Quote from: Siwastaja on September 10, 2025, 09:12:48 amThat's what I would recommend as well, but it's also hard to recommend an approach without knowing the OP's background. It may be slightly different whether they have no prior experiencce in programming altogether, in programming MCUs, in programming STM32 MCUs, in using various build systems.I'm not fond of Cmake, although it avoids some boilerplate needed in Makefiles that many are not comfortable with. Once you've set up the required "boilerplate" for your use cases, you can use that ad infinitum, but there's an initial learning curve. I think Cmake is more adapted to large, non-embedded projects, but it has become so "de-facto" (like git) that everyone and their dog seems to be using it these days in embedded projects. Although some are now switching to Bazel, the new hype on the block.As an alternative, you can consider Xmake, which is Lua-scriptable and isn't bad.

author: peter-h
author_rank: Super Contributor
timestamp: September 11, 2025, 01:26:09 pm
content: The learning curve, if all you have is the 2000 page RM, is steep.I guess nowadays Claude "AI" could dig up usable code. It finds stuff for which google is pretty painful. I have used it for self contained algorithms and it probably found them in some 1985 Usenet archiveIn this case, everything is online, including every piece of ST's devkit (DISCO boards etc, HAL...) code.Putting my money where my mouth is, typing this into ClaudeCan you give me C code for configuring a 32F407 for a 168MHz clock from a 25MHz crystalproduces thisCode:#include "stm32f4xx.h"// Flash latency configuration for 168MHz operation#define FLASH_LATENCY_168MHZ FLASH_ACR_LATENCY_5WS// PLL configuration values for 25MHz crystal -> 168MHz SYSCLK#define PLL_M 25 // Input divider: 25MHz / 25 = 1MHz#define PLL_N 336 // Multiplier: 1MHz * 336 = 336MHz (VCO)#define PLL_P 2 // Output divider: 336MHz / 2 = 168MHz#define PLL_Q 7 // USB clock: 336MHz / 7 = 48MHz// System clock configuration structuretypedef struct {uint32_t SYSCLK_Frequency;uint32_t HCLK_Frequency;uint32_t PCLK1_Frequency;uint32_t PCLK2_Frequency;} ClockConfig_t;// Function prototypesvoid SystemClock_Config_168MHz(void);void Configure_Flash_Prefetch(void);void Configure_Power_Regulator(void);ClockConfig_t Get_Clock_Frequencies(void);/*** @brief Configure system clock to 168MHz using 25MHz external crystal* @param None* @retval None*/void SystemClock_Config_168MHz(void){// Enable Power Control clockRCC->APB1ENR |= RCC_APB1ENR_PWREN;// Configure voltage regulator for high performance (Scale 1)Configure_Power_Regulator();// Configure Flash latency and prefetchConfigure_Flash_Prefetch();// Enable HSE (High Speed External) oscillatorRCC->CR |= RCC_CR_HSEON;// Wait for HSE to be readywhile(!(RCC->CR & RCC_CR_HSERDY));// Configure PLLRCC->PLLCFGR = (PLL_M << 0) | // PLLM: Input divider(PLL_N << 6) | // PLLN: Multiplier(((PLL_P >> 1) - 1) << 16) | // PLLP: Main output divider(RCC_PLLCFGR_PLLSRC_HSE) | // HSE as PLL source(PLL_Q << 24); // PLLQ: USB/SDIO clock divider// Enable PLLRCC->CR |= RCC_CR_PLLON;// Wait for PLL to lockwhile(!(RCC->CR & RCC_CR_PLLRDY));// Configure AHB, APB1, APB2 prescalersRCC->CFGR |= RCC_CFGR_HPRE_DIV1; // AHB prescaler = 1 (168MHz)RCC->CFGR |= RCC_CFGR_PPRE2_DIV2; // APB2 prescaler = 2 (84MHz)RCC->CFGR |= RCC_CFGR_PPRE1_DIV4; // APB1 prescaler = 4 (42MHz)// Select PLL as system clock sourceRCC->CFGR &= ~RCC_CFGR_SW;RCC->CFGR |= RCC_CFGR_SW_PLL;// Wait for PLL to be used as system clock sourcewhile((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);// Update SystemCoreClock variableSystemCoreClock = 168000000;}/*** @brief Configure Flash memory settings for 168MHz operation* @param None* @retval None*/void Configure_Flash_Prefetch(void){// Configure Flash prefetch, Instruction cache, Data cache and wait stateFLASH->ACR = FLASH_ACR_PRFTEN | // Enable prefetchFLASH_ACR_ICEN | // Enable instruction cacheFLASH_ACR_DCEN | // Enable data cacheFLASH_LATENCY_168MHZ; // 5 wait states for 168MHz}/*** @brief Configure power regulator voltage scaling* @param None* @retval None*/void Configure_Power_Regulator(void){// Set voltage regulator to Scale 1 mode (high performance)PWR->CR &= ~PWR_CR_VOS;PWR->CR |= PWR_CR_VOS; // Scale 1 mode (1.8V core voltage)}/*** @brief Get current clock frequencies* @param None* @retval ClockConfig_t structure with frequency values*/ClockConfig_t Get_Clock_Frequencies(void){ClockConfig_t clocks;uint32_t tmp, pllvco, pllp, pllsource, pllm;// Get SYSCLK sourcetmp = RCC->CFGR & RCC_CFGR_SWS;switch (tmp){case 0x00: // HSI used as system clockclocks.SYSCLK_Frequency = 16000000;break;case 0x04: // HSE used as system clockclocks.SYSCLK_Frequency = 25000000;break;case 0x08: // PLL used as system clock// PLL_VCO = (HSE_VALUE or HSI_VALUE / PLLM) * PLLN// SYSCLK = PLL_VCO / PLLPpllsource = (RCC->PLLCFGR & RCC_PLLCFGR_PLLSRC) >> 22;pllm = RCC->PLLCFGR & RCC_PLLCFGR_PLLM;if (pllsource != 0){// HSE used as PLL clock sourcepllvco = (25000000 / pllm) * ((RCC->PLLCFGR & RCC_PLLCFGR_PLLN) >> 6);}else{// HSI used as PLL clock sourcepllvco = (16000000 / pllm) * ((RCC->PLLCFGR & RCC_PLLCFGR_PLLN) >> 6);}pllp = (((RCC->PLLCFGR & RCC_PLLCFGR_PLLP) >> 16) + 1) * 2;clocks.SYSCLK_Frequency = pllvco / pllp;break;default:clocks.SYSCLK_Frequency = 16000000;break;}// Compute HCLK, PCLK1 and PCLK2 clocks frequencies// Get HCLK prescalertmp = (RCC->CFGR & RCC_CFGR_HPRE) >> 4;uint8_t ahb_prescaler_table = {0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 6, 7, 8, 9};tmp = ahb_prescaler_table;clocks.HCLK_Frequency = clocks.SYSCLK_Frequency >> tmp;// Get PCLK1 prescalertmp = (RCC->CFGR & RCC_CFGR_PPRE1) >> 10;uint8_t apb_prescaler_table = {0, 0, 0, 0, 1, 2, 3, 4};tmp = apb_prescaler_table;clocks.PCLK1_Frequency = clocks.HCLK_Frequency >> tmp;// Get PCLK2 prescalertmp = (RCC->CFGR & RCC_CFGR_PPRE2) >> 13;tmp = apb_prescaler_table;clocks.PCLK2_Frequency = clocks.HCLK_Frequency >> tmp;return clocks;}/*** @brief Verify clock configuration* @param None* @retval 1 if clocks are correctly configured, 0 otherwise*/uint8_t Verify_Clock_Config(void){ClockConfig_t clocks = Get_Clock_Frequencies();// Check if frequencies match expected valuesif (clocks.SYSCLK_Frequency == 168000000 &&clocks.HCLK_Frequency == 168000000 &&clocks.PCLK2_Frequency == 84000000 &&clocks.PCLK1_Frequency == 42000000){return 1; // Success}return 0; // Configuration error}/*** @brief Alternative configuration using register macros (more readable)* @param None* @retval None*/void SystemClock_Config_168MHz_Alternative(void){// Enable PWR clockSET_BIT(RCC->APB1ENR, RCC_APB1ENR_PWREN);// Set voltage scaling to Scale 1 (high performance)MODIFY_REG(PWR->CR, PWR_CR_VOS, PWR_CR_VOS);// Configure Flash latencyMODIFY_REG(FLASH->ACR, FLASH_ACR_LATENCY, FLASH_LATENCY_168MHZ);SET_BIT(FLASH->ACR, FLASH_ACR_PRFTEN | FLASH_ACR_ICEN | FLASH_ACR_DCEN);// Enable HSESET_BIT(RCC->CR, RCC_CR_HSEON);while(!READ_BIT(RCC->CR, RCC_CR_HSERDY));// Configure PLL: 25MHz / 25 * 336 / 2 = 168MHzWRITE_REG(RCC->PLLCFGR,(PLL_M << RCC_PLLCFGR_PLLM_Pos) |(PLL_N << RCC_PLLCFGR_PLLN_Pos) |(((PLL_P >> 1) - 1) << RCC_PLLCFGR_PLLP_Pos) |RCC_PLLCFGR_PLLSRC_HSE |(PLL_Q << RCC_PLLCFGR_PLLQ_Pos));// Enable PLLSET_BIT(RCC->CR, RCC_CR_PLLON);while(!READ_BIT(RCC->CR, RCC_CR_PLLRDY));// Configure prescalersMODIFY_REG(RCC->CFGR, RCC_CFGR_HPRE, RCC_CFGR_HPRE_DIV1); // AHB /1MODIFY_REG(RCC->CFGR, RCC_CFGR_PPRE2, RCC_CFGR_PPRE2_DIV2); // APB2 /2MODIFY_REG(RCC->CFGR, RCC_CFGR_PPRE1, RCC_CFGR_PPRE1_DIV4); // APB1 /4// Switch to PLLMODIFY_REG(RCC->CFGR, RCC_CFGR_SW, RCC_CFGR_SW_PLL);while(READ_BIT(RCC->CFGR, RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);// Update system core clock variableSystemCoreClock = 168000000;}/*** @brief Example usage in main function* @param None* @retval int*/int main(void){// Configure system clock to 168MHzSystemClock_Config_168MHz();// Verify configurationif (Verify_Clock_Config()){// Clock configuration successful// Your application code herewhile(1){// Main application loop}}else{// Clock configuration failed// Error handlingwhile(1){// Error indication loop}}return 0;}which looks more or less right. Especially the bit where they read back some config register for that switch statement - that is very much ST's wayThe explanation it gives is better than using MX:Here's C code to configure an STM32F407 for 168MHz system clock using a 25MHz external crystal:This code configures the STM32F407 to run at 168MHz using a 25MHz external crystal. Here are the key aspects:## PLL Configuration**Formula:** `SYSCLK = (HSE / PLLM) × PLLN / PLLP`- **HSE:** 25MHz external crystal- **PLLM:** 25 (divider) → 25MHz ÷ 25 = 1MHz- **PLLN:** 336 (multiplier) → 1MHz × 336 = 336MHz (VCO)- **PLLP:** 2 (divider) → 336MHz ÷ 2 = **168MHz**- **PLLQ:** 7 → 336MHz ÷ 7 = 48MHz (USB clock)## Critical Configuration Steps**1. Power Management:**- Voltage regulator set to Scale 1 (high performance mode)- Required for 168MHz operation**2. Flash Memory:**- 5 wait states for 168MHz operation- Prefetch, instruction cache, and data cache enabled**3. Bus Prescalers:**- **AHB:** ÷1 → 168MHz- **APB2:** ÷2 → 84MHz (max 84MHz)- **APB1:** ÷4 → 42MHz (max 42MHz)## Key Features- **Verification function** to confirm clock settings- **Alternative implementation** using HAL-style macros- **Clock frequency calculation** function- **Error handling** for failed configuration- **Proper sequencing** of configuration steps## UsageSimply call `SystemClock_Config_168MHz()` early in your `main()` function. The code includes verification to ensure the configuration succeeded.**Note:** Make sure your 25MHz crystal and load capacitors are properly connected to OSC_IN/OSC_OUT pins, and that your PCB layout follows STM32 design guidelines for the crystal oscillator circuit.

author: Siwastaja
author_rank: Super Contributor
timestamp: September 11, 2025, 06:06:04 pm
content: Quote from: peter-h on September 11, 2025, 01:26:09 pm273 lines of code - 8130 characters - and no real abstraction, no API - just autogeneration. No wonder people think this stuff is difficult. Meanwhile, example of doing basically the same on some STM32H743 project:Code:/*sys_ck (for CPU, etc.) is pll1_p_ckFvco = Fref * divNFout = Fvco / (postdiv_reg+1)VCO must be between 192 and 836MHzMultiplier (divN) must be from 4 to 512Fractional PLL mode not needed - initialize in integer modeInput clock = 8 MHzLet:AHB1 aka hclk1 = 200 MHz HPRE = 2AHB2 aka hclk2 = 200 MHz (HPRE = 2)AHB3 aka hclk3 = 200 MHz (HPRE = 2)APB1 aka pclk1 = 100 MHz D2PPRE1 = 2APB2 aka pclk2 = 100 MHz D2PPRE2 = 2APB3 aka pclk3 = 100 MHz D1PPRE = 2APB4 aka pclk4 = 100 MHz D3PPRE = 2Some APB* peripheral kernel clocks are 100MHz max, and cannot be connected toPLL1. So, if 200MHz bus on APB1 or APB2 is ever needed, PLL2 needs to be configuredto drive the kernel clocks for such peripherals. For example, SPI4,5.For now, it's simplest to drive these peripherals directly on the bus clock, so weuse 100MHz bus. It's highly unlikely this would never be an performance issue.The most busy/biggest bandwidth SPI is SPI1, which is in APB2. Consider increasing APB2to 200MHz.PLL1_P: 400MHz: sysck (cpu, etc.)PLL1_Q: 100MHz: SPI1,2,3PLL2_P: 33.33MHz: ADC1,2, ADC3TODO: WARNING: The new STM32H7 chip revision V will have a /2 divider inside the ADC -if the new chip revision is assembled, need to change PLL2_P to 66.67MHz.*/RCC->CR |= 1UL<<16; // HSE clock onwhile(!(RCC->CR & 1UL<<17)) ; // Wait for HSE oscillator stabilization// M prescalers - set to 0 when PLL is disabled. 1 = div by 1 (bypass)RCC->PLLCKSELR = 0UL<<20 /* PLL3 M(prescaler)*/ |1UL<<12 /* PLL2 M(prescaler)*/ |1UL<<4 /* PLL1 M(prescaler)*/ |0b10UL /*HSE as PLL source*/;RCC->PLLCFGR = 0b000UL<<22 /*PLL3 divider output enables: R,Q,P */ |0b001UL<<19 /*PLL2 divider output enables: R,Q,P */ |0b011UL<<16 /*PLL1 divider output enables: R,Q,P */ |0b10UL<<10 /*PLL3 input between 4..8MHz*/ |0b10UL<<6 /*PLL2 input between 4..8MHz*/ |0b10UL<<2 /*PLL1 input between 4..8MHz*/;RCC->PLL1DIVR = 0UL<<24 /*R*/ | (4UL -1UL)<<16 /*Q ->pll1_q_ck (100MHz)*/ | (1UL -1UL)<<9 /*P ->sys_ck (400MHz) */ | (50UL -1UL)<<0 /*N 8MHz->400MHz*/;RCC->PLL2DIVR = 0UL<<24 /*R*/ | 0UL<<16 /*Q */ | (12UL -1UL)<<9 /*P ->ADC (33.33MHz)*/ | (50UL -1UL)<<0 /*N 8MHz->400MHz*/;RCC->CR |= 1UL<<24; // PLL1 on// Configure other registers while waiting PLL1 stabilization:FLASH->ACR = 0b10UL<<4 /*WRHIGHFREQ programming delay: 185..210MHz AXI bus in VOS1 */ |2UL<<0 /*LATENCY = 2 wait states: 185..210MHz in VOS1 */;RCC->D1CFGR = 0UL<<8 /*D1CPRE = sysck divider = 1*/ | 0b100UL<<4 /*D1PPRE = 2*/ | 0b1000UL<<0 /*HPRE = 2*/;RCC->D2CFGR = 0b100UL<<8 /*D2PPRE2 = 2*/ | 0b100UL<<4 /*D2PPRE1 = 2*/;RCC->D3CFGR = 0b100UL<<4 /*D3PPRE = 2*/;while(!(RCC->CR & 1UL<<25)) ; // Wait for PLL1 readyRCC->CFGR |= 0b011; // Change PLL to system clockwhile((RCC->CFGR & (0b111UL<<3)) != (0b011UL<<3)) ; // Wait for switchover to PLL.RCC->CR |= 1UL<<26; // PLL2 onwhile(!(RCC->CR & 1UL<<27)) ; // Wait for PLL2 ready76 lines, 3061 characters. And most of it is comments that actually document what is going on, including documenting legal limits given in the reference manual, so that one does not accidentally change the values beyond them. It also works, which is of course priceless.

author: asmi
author_rank: Super Contributor
timestamp: September 11, 2025, 06:29:59 pm
content: If you are a beginner, ignore all snobs talking about creating project from scratch, and use CubeIDE/CubeMX to get yourself up and running quickly. Then as your experience grows, you won't have to ask these questions anymore as you will know answers already. ST's vendor libraries are full of oddities and questionable design choices, but none of these would matter for a beginner, while experienced folks would know what's good for them without asking anybody.

author: Doctorandus_P
author_rank: Super Contributor
timestamp: September 12, 2025, 12:02:23 am
content: This week it took another two days out of my life just to get to a blinking led on an STM32G031. It started with Java errors in Cube 1.19, then a lot of hassle for signing in to the ST website and downloading some older versions of Cube. Later I deleted the ".metadata" directory because that was not compatible with the Cube V1.17 I have installed now. (The bloody Cube shit goes *&^%$#@! on the internet while installing under sudo!). Messing around with more logins later to get a project initialized.A day later, my LED failed after the first update of the user program (I forgot to enable SW_DIO and SW_CLK in the IDE, so the target could not be programmed the second time. Bunch of fiddling later with the cheap chinese ST-Link V2 dongles I discovered that the target reset is wired wrongly, so I rewired that. Setting below works (also turned out to be the default)Cube / Run / Debug Configuration / Debugger / Reset behavior / Type: Connect under resetEvery time Cube starts My PC fan spins up for around 40 seconds while cube is furiously doing "something" with the web (Have not yet managed to disable that).I have an extreme dislike for ST's boiler plate code, but at the same time, the complexity of especially the clock configuration is such that it would drive most people mad attempting to set up the clocks manually. That pretty much excludes other options, and you have to use the project configurator to get started.I think MX is that program (part) with the GUI to click around to enable peripherals and set I/O pins. That can be standalone (mine version crashes with Java errors at the moment), but it's also included in Cube (in V1.17 it worked a few days ago).I also need C++, and eclipse messes up C and C++ in one project (That's another struggle), as C++ project generation does not work well in Cube).At the moment, my intention is to maintain two separate versions of a project. After I verified the whole project setup in Cube is correct, I backup (can be git) the project, and then transform it into something usable. If I later need to use the MX configurator tool again, then I will do it with the old template project, and then manually merge all changes into my current project.If eclipse "worked properly" I would keep on using it, but it's got so many *&^%$#@! settings (and falls over too often) that I want to move away from it. ([Ctrl + Scrollwheel does not work to change font size, I am addicted to that, often I want big letters wile typing code, sometimes I want more text on screen for an overview, but that's yet another topic).I have had Cube generate a make file (Made notes in 2023) and that makefile looks quite decent. You can use that makefile, and then detach the whole thing later from Cube.It is nice to have debugging capabilities (and it works in cube), but I am willing to give that up just to get an IDE that works without falling over on every blue moon. I've been writing uC code for >25 years (on a hobby level, 3 years for work) and I've always managed without a debugger on the uC.Quote from: Siwastaja on September 11, 2025, 06:06:04 pmJust forget that. Those AI things can do amazing things, but they fill in all gaps with lies. I have very little confidence that such code would actually work. It's much more likely that it mixes up configuration snippets for different processors, and then you have to debug the garbage template code. At the moment, AI is like a 4 year old child with the memory of internet behind it.

author: peter-h
author_rank: Super Contributor
timestamp: September 12, 2025, 06:41:02 am
content: Yes that is about it... but a lot of people manage to get by with code snippets from Claude. Using MX to generate the startup is better for sure. You can also assign and configure the pins etc.The ST website keeps older Cube versions but only the x.x.0 releases whereas the x.x.1 release was generally the working oneOften the debugger is broken in the .0 releases.I am on 1.14.1 which works fine. I can let you have that if you need it. There is nothing in later versions, apart from support for later chips but what exactly is that?? MX of course needs updates for later chips because it has to contain the package diagrams, pin options, etc.No point in using fake debuggers; this stuff costs almost nothing anyway.I find stepping through code in Cube is a great timesaver. You have to watch cases where there is no code to set a breakpoint on e.g. you can set a bp on a commentI use -Og optimisation all the time and that works well. -O0 generates some 30-50% more code for no gain.You can disable checking for updates and telemetry in Cube. It's not too hard to find.

author: Siwastaja
author_rank: Super Contributor
timestamp: September 12, 2025, 09:41:34 am
content: If you are a beginner, ignore all snobs talking about CubeMX or CubeIDE because quite frankly, I don't remember seeing many beginners succeeding with them. Most give up. What time sinks. Experienced people manage, although I have seen one project driven into unmaintainable mess when Cube and Cmake where introduced later in product phase "just because".I suggest looking at ataradov's MCU starter projects for inspiration.Generally, avoid manufacturer-specific anything in ARM ecosystem. Unnecessary vendor lock-in.

author: peter-h
author_rank: Super Contributor
timestamp: September 12, 2025, 10:04:20 am
content: That's one point of view.Another POV is that if you have a tool which works, you can just keep using it once you have it set up. And in any business context that isexactly what you want. Well, unless you are a hourly paid coder (and with a slightly stupid employer) in which case all time is good time even if it is wastedThis is not a Z80 where you stick in some EPROM and some RAM, and some HC273 etc latches for I/O, a PAL for address decoding, get some assembler out (or even a C compiler) and off you go, all set up in a couple of days. These chips have huge learning curves, but once set up it is easy. I haven't changed my Cube config in about 5 years. Well, I dropped the Ramtex graphics library into the project but that was just making a new subdirectory; Cube took care of the dependencies etc.And Cube IDE is self contained, uses no licensing, no online checking (once you disable that), the whole project can be zipped up into a nice archive.There are people here who know these chips backwards but I never had a desire to know a 32F417 backwards

author: Karel
author_rank: Super Contributor
timestamp: September 12, 2025, 12:07:45 pm
content: I use a makefile and a linkerscript plus a bash script for programming.Writing a makefile is not too difficult. The hardest part (at least to me) was to find a good linkerscript.Years ago, I took a linkerscript that was created by AC6 Systemworkbench and I modified it according to my needs.Setting up the clock & pll is not too difficult. I started to write comments in the code like this:Code:/* STM32L452xx PLL, PLLSAI1 characteristics+------------+-------------------------------+-------------------------+--------+-----+-----+-------+| Symbol | Parameter | Conditions | Min | Typ | Max | Unit |+------------+-------------------------------+-------------------------+--------+-----+-----+-------+| fPLL_IN | PLL input clock | - | 4 | - | 16 | MHz || | PLL input clock duty cycle | _ | 45 | - | 55 | % |+------------+-------------------------------+-------------------------+--------+-----+-----+-------+| fPLL_P_OUT | PLL multiplier output clock P | Voltage scaling Range 1 | 3.0986 | - | 80 | MHz || | | Voltage scaling Range 2 | 3.0986 | - | 26 | |+------------+-------------------------------+-------------------------+--------+-----+-----+-------+| fPLL_Q_OUT | PLL multiplier output clock Q | Voltage scaling Range 1 | 12 | - | 80 | MHz || | | Voltage scaling Range 2 | 12 | - | 26 | |+------------+-------------------------------+-------------------------+--------+-----+-----+-------+| fPLL_R_OUT | PLL multiplier output clock R | Voltage scaling Range 1 | 12 | - | 80 | MHz || | | Voltage scaling Range 2 | 12 | - | 26 | |+------------+-------------------------------+-------------------------+--------+-----+-----+-------+| fVCO_OUT | PLL VCO output | Voltage scaling Range 1 | 96 | - | 344 | MHz || | | Voltage scaling Range 2 | 96 | - | 128 | |+------------+-------------------------------+-------------------------+--------+-----+-----+-------+| tLOCK | PLL lock time | - | - | 15 | 40 | us |+------------+-------------------------------+-------------------------+--------+-----+-----+-------+| Jitter | RMS cycle-to-cycle jitter | System clock 80 MHz | - | 40 | - | +/-ps || | RMS period jitter | | - | 30 | - | |+------------+-------------------------------+-------------------------+--------+-----+-----+-------+*//** XO 8 MHz -> :1 -> x20 -> 160 MHz -> :2 -> 80 MHz** MSI/HSI/HSE -> :M -> phase comparator -> VCO ->--+--> :R -> sysclock* ^ |* | v* | |* +---<--- :N <-----<------+* :M 1 - 8* :N 8 - 86* :R 2,4,6 or 8*/

author: westfw
author_rank: Super Contributor
timestamp: September 12, 2025, 11:04:17 pm
content: QuoteThe documentation and libraries all seem to do a fine job of making it seem more difficult than it really is.Apparently the people (not just at ST) who design those "clock systems" are very proud of them, and want to make sure that every possible option is fully described and given "equal time" in the documentation (over 100 pages of the SAMD51 datasheet is about the clocks and oscillators!)This is really unfortunate - you have one of the more complex "systems" of the chip that you "need" to understand before you even get started. (Of course, you COULD just run with the default clock(s) (much slower than the documented speed.))It gets a lot simpler if you narrow your sight to the particular clock source you want to use.Back during the "minimal ARM" discussion, we got the clock initialization for running a 32F103 at 72MHz (using the PLL) down to about 25 instructions (including setting up the external crystal and the flash wait states needed.)https://github.com/WestfW/Minimal-ARM/blob/master/Blink/blink.S

author: Karel
author_rank: Super Contributor
timestamp: September 13, 2025, 07:13:49 am
content: I forgot, apart from the linker script, there's another important file "startup_stm32.asm".This is the code that gets called when the processor first starts execution following a reset event.Only the absolutely necessary set is performed, after which the application supplied main() routine is called.- Copy the data segment initializers from flash to SRAM- Zero fill the bss segment- Initialize (weak) all interrupt vectors. This is the code that gets called when the processor receives anunexpected interrupt. This simply enters an infinite loop, preserving the system state for examination by a debugger.Imho, it's practically impossible for a beginner to write these two files from scratch. I copied them fromAC6 Systemworkbench but I guess nowadays you can get them using STM32CubeMX etc.

author: Siwastaja
author_rank: Super Contributor
timestamp: September 13, 2025, 07:37:04 am
content: No one's suggesting writing linker script and startup code on a blank sheet of parchment with a quill then scan and OCR it. You can copy-paste known good linker script and startup code and modify e.g. memory addresses as necessary. Maybe even the the linker file + startup code that come with Cube are OK, I don't know. Or you can look at ataradov's starter projects as mentioned. That doesn't mean you have to fully lock into Cube and autogenerated crap peripheral code.

author: Karel
author_rank: Super Contributor
timestamp: September 13, 2025, 09:59:37 am
content: Yes, but when I started with Arm (STM32) I didn't have this knowledge and I wasn't aware of the sources available.I more or less had to find my way almost completely on myself. So, I started with AC6 Systemworkbench andfigured out these important files and copied them. Then I started to find out how it uses OpenOCD in order to programthe target device.Later on I discovered that I didn't need the installation of AC6 Systemworkbench. Now I simply download the compiler fromhttps://developer.arm.com/downloads/-/arm-gnu-toolchain-downloadsIn retrospect, you can say "You should have done it so or so" but believe me, starting with STM32 without any prior experiencewith Arm mcu's (my experience was with Microchip, no Arm) is pritty difficult, specially without an IDE like STM32CubeMX etc.Having said that, I'm happy that I spend the time to find my way with STM32 and I enjoy the freedom of using opensource tools.I detest Microchip with their "commercial" compiler licenses based on GCC...

author: radiolistener
author_rank: Super Contributor
timestamp: September 13, 2025, 10:28:31 am
content: Quote from: Doctorandus_P on September 12, 2025, 12:02:23 amYes, Cube is indeed quite heavy bloatware, and can make your PC work harder when it starts. Sometimes it even unable to open previously saved project. I don't like it for this. But on the other hand, it provides a very quick way to generate initialization code for a MCU you haven’t worked with before. As you gain experience, you’ll likely develop your own smaller, more convenient initialization routines. But when starting with a new MCU, Cube remains one of the fastest ways to configure clocks and peripherals without digging deep through a lot of pages of documentation.

author: DavidAlfa
author_rank: Super Contributor
timestamp: September 13, 2025, 12:18:40 pm
content: If you're new to STM32, MX + HAL is the best option to get started (New->STM32 project).For 99% of the time HAL is ok, if you need more control or performance, go LL (Set in MX adv. project settings), but you'll see it takes way more time to get it working.But the footprint is much smaller, no handlers hogging the ram, no lengthy ISRs, and flash usage is much smaller, specially noticeable in small MCUs.LL isn't bloated, it's made of macros and basic functions which are basically the same you'd do when going baremetal.For example:Code:GPIOA->BSRR = 1; // LED ONLL_GPIO_SetOutputPin(GPIOA, 1);Code:__STATIC_INLINE void LL_GPIO_SetOutputPin(GPIO_TypeDef *GPIOx, uint32_t PinMask){WRITE_REG(GPIOx->BSRR, PinMask);}Code:#define WRITE_REG(REG, VAL) ((REG) = (VAL))So, LL_GPIO_SetOutputPin expands to exactly the same thing:Code:GPIOA->BSRR = 1;Baremetal is just crazy unless your company is ok to pay you for weeks of work to make basically the same as what LL does, and possibly introducing more bugs.The stressful time will cause a lot of balding + getting sterile in the process or developing ED.Plus, portability is much better.Sometimes HAL and LL are doing more than we think, not just bloated, but solving erratas.

author: peter-h
author_rank: Super Contributor
timestamp: September 14, 2025, 06:14:31 am
content: I looked for LL_GPIO_SetOutputPin() etc and it isn't used in my project. It seems to be just a macro.I think the MX-generated "HAL" code is OK for setting up the chip because timing is hardly an issue. Later, when you want performance, you can take e.g. the HAL function for reading SPI and strip out all the crap. STM devs have a weird habit of reading back device registers to find previous config and modifying that, even when the register was written fully somewhere further back in the same function. You can strip out that stuff for a start. Strip out CRC code if not using CRC...This is an example of a "HAL" function. A bit crazy but will workCode:/*** @brief GPIO Init structure definition*/typedef struct{uint32_t Pin; /*!< Specifies the GPIO pins to be configured.This parameter can be any value of @ref GPIO_pins_define */uint32_t Mode; /*!< Specifies the operating mode for the selected pins.This parameter can be a value of @ref GPIO_mode_define */uint32_t Pull; /*!< Specifies the Pull-up or Pull-Down activation for the selected pins.This parameter can be a value of @ref GPIO_pull_define */uint32_t Speed; /*!< Specifies the speed for the selected pins.This parameter can be a value of @ref GPIO_speed_define */uint32_t Alternate; /*!< Peripheral to be connected to the selected pins.This parameter can be a value of @ref GPIO_Alternate_function_selection */}GPIO_InitTypeDef;/*** @brief Initializes the GPIOx peripheral according to the specified parameters in the GPIO_Init.* @param GPIOx where x can be (A..K) to select the GPIO peripheral for STM32F429X device or* x can be (A..I) to select the GPIO peripheral for STM32F40XX and STM32F427X devices.* @param GPIO_Init pointer to a GPIO_InitTypeDef structure that contains* the configuration information for the specified GPIO peripheral.* @retval None*/void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init){uint32_t position;uint32_t ioposition = 0x00U;uint32_t iocurrent = 0x00U;uint32_t temp = 0x00U;/* Configure the port pins */for(position = 0U; position < GPIO_NUMBER; position++){/* Get the IO position */ioposition = 0x01U << position;/* Get the current IO position */iocurrent = (uint32_t)(GPIO_Init->Pin) & ioposition;if(iocurrent == ioposition){/*--------------------- GPIO Mode Configuration ------------------------*//* In case of Alternate function mode selection */if((GPIO_Init->Mode == GPIO_MODE_AF_PP) || (GPIO_Init->Mode == GPIO_MODE_AF_OD)){/* Check the Alternate function parameter */assert_param(IS_GPIO_AF(GPIO_Init->Alternate));/* Configure Alternate function mapped with the current IO */temp = GPIOx->AFR[position >> 3U];temp &= ~(0xFU << ((uint32_t)(position & 0x07U) * 4U)) ;temp |= ((uint32_t)(GPIO_Init->Alternate) << (((uint32_t)position & 0x07U) * 4U));GPIOx->AFR[position >> 3U] = temp;}/* Configure IO Direction mode (Input, Output, Alternate or Analog) */temp = GPIOx->MODER;temp &= ~(GPIO_MODER_MODER0 << (position * 2U));temp |= ((GPIO_Init->Mode & GPIO_MODE) << (position * 2U));GPIOx->MODER = temp;/* In case of Output or Alternate function mode selection */if((GPIO_Init->Mode == GPIO_MODE_OUTPUT_PP) || (GPIO_Init->Mode == GPIO_MODE_AF_PP) ||(GPIO_Init->Mode == GPIO_MODE_OUTPUT_OD) || (GPIO_Init->Mode == GPIO_MODE_AF_OD)){/* Configure the IO Speed */temp = GPIOx->OSPEEDR;temp &= ~(GPIO_OSPEEDER_OSPEEDR0 << (position * 2U));temp |= (GPIO_Init->Speed << (position * 2U));GPIOx->OSPEEDR = temp;/* Configure the IO Output Type */temp = GPIOx->OTYPER;temp &= ~(GPIO_OTYPER_OT_0 << position) ;temp |= (((GPIO_Init->Mode & GPIO_OUTPUT_TYPE) >> 4U) << position);GPIOx->OTYPER = temp;}/* Activate the Pull-up or Pull down resistor for the current IO */temp = GPIOx->PUPDR;temp &= ~(GPIO_PUPDR_PUPDR0 << (position * 2U));temp |= ((GPIO_Init->Pull) << (position * 2U));GPIOx->PUPDR = temp;/*--------------------- EXTI Mode Configuration ------------------------*//* Configure the External Interrupt or event for the current IO */if((GPIO_Init->Mode & EXTI_MODE) == EXTI_MODE){/* Enable SYSCFG Clock */__HAL_RCC_SYSCFG_CLK_ENABLE();temp = SYSCFG->EXTICR[position >> 2U];temp &= ~(0x0FU << (4U * (position & 0x03U)));temp |= ((uint32_t)(GPIO_GET_INDEX(GPIOx)) << (4U * (position & 0x03U)));SYSCFG->EXTICR[position >> 2U] = temp;/* Clear EXTI line configuration */temp = EXTI->IMR;temp &= ~((uint32_t)iocurrent);if((GPIO_Init->Mode & GPIO_MODE_IT) == GPIO_MODE_IT){temp |= iocurrent;}EXTI->IMR = temp;temp = EXTI->EMR;temp &= ~((uint32_t)iocurrent);if((GPIO_Init->Mode & GPIO_MODE_EVT) == GPIO_MODE_EVT){temp |= iocurrent;}EXTI->EMR = temp;/* Clear Rising Falling edge configuration */temp = EXTI->RTSR;temp &= ~((uint32_t)iocurrent);if((GPIO_Init->Mode & RISING_EDGE) == RISING_EDGE){temp |= iocurrent;}EXTI->RTSR = temp;temp = EXTI->FTSR;temp &= ~((uint32_t)iocurrent);if((GPIO_Init->Mode & FALLING_EDGE) == FALLING_EDGE){temp |= iocurrent;}EXTI->FTSR = temp;}}}}

author: westfw
author_rank: Super Contributor
timestamp: September 14, 2025, 11:51:30 pm
content: QuoteDo they still implement "get_clock_speed" by completely reverse-engineering all the oscilllator and clock system (PLL/etc) register contents? (Hmm. Yes:https://github.com/stm32duino/Arduino_Core_STM32/blob/main/system/Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_ll_rcc.c#L343)I remember being very surprised by that, back in the early days. But I think it's become more common since then to standardize on simply having a SystemClockSpeed global variable that is kept up to date.It's a fine example of over-engineering. In some sense it is nice to be able to get the actual clock rate regardless of whether "other code" (or HW failures) has been mucking about. But the occasions were the global variable, or even a compile-time constant, is insufficient, are pretty rare.

author: peter-h
author_rank: Super Contributor
timestamp: September 15, 2025, 07:03:32 am
content: They still need a #define for the crystalSo I am not using RCC_GetSystemClockFreq() etc and just set it to 168000000.One could probably measure the crystal using the internal oscillator somehow...What I use is thisCode:SystemCoreClock=168000000L;/*** @brief Returns the HCLK frequency* @note Each time HCLK changes, this function must be called to update the* right HCLK value. Otherwise, any configuration based on this function will be incorrect.** @note The SystemCoreClock CMSIS variable is used to store System Clock Frequency* and updated within this function* @retval HCLK frequency*/uint32_t HAL_RCC_GetHCLKFreq(void){return SystemCoreClock;}/*** @brief Returns the PCLK1 frequency* @note Each time PCLK1 changes, this function must be called to update the* right PCLK1 value. Otherwise, any configuration based on this function will be incorrect.* @retval PCLK1 frequency*/uint32_t HAL_RCC_GetPCLK1Freq(void){/* Get HCLK source and Compute PCLK1 frequency ---------------------------*/uint32_t pclk1 = (HAL_RCC_GetHCLKFreq() >> APBPrescTable[(RCC->CFGR & RCC_CFGR_PPRE1)>> RCC_CFGR_PPRE1_Pos]);return pclk1;}/*** @brief Returns the PCLK2 frequency* @note Each time PCLK2 changes, this function must be called to update the* right PCLK2 value. Otherwise, any configuration based on this function will be incorrect.* @retval PCLK2 frequency*/uint32_t HAL_RCC_GetPCLK2Freq(void){/* Get HCLK source and Compute PCLK2 frequency ---------------------------*/uint32_t pclk2 = (HAL_RCC_GetHCLKFreq()>> APBPrescTable[(RCC->CFGR & RCC_CFGR_PPRE2)>> RCC_CFGR_PPRE2_Pos]);return pclk2;}