My first serious uC programming in C was with the ATMEGA8. It "just worked" and I did not have to think much about details. I just used the header files, for the address definitions of I/O stuff, and I could very much just start by putting some contents into main().
Later I wanted to look into STM32, and that was a whole other sort of adventure. The biggest problem is an extreme overload of low quality crap, that can barely be mentioned "information", for example the 66 pages of:
https://www.eevblog.com/forum/microcontrollers/is-st-cube-ide-a-piece-of-buggy-crap/I really disliked eclipse with immediately phoning home (or elsewhere) each time I start the software, and the idea of ST for "helping" people is to make an ever bigger pile of crap and then say "push this button".
So I dove deeper, and learned a lot about what's needed for a "minimum" environment.
1). First: GCC. I use the arm-none-eabi-gcc variant.
2). You need "startup code". this is usually provided by the microcontroller vendor, and you do not write it yourself. It's also dependent on the compiler. For example, the commercial compiler vendors use another compiler (duh) and thus also different startup code. This startup code is usually written in assembly, but (most, or a part) can be written in C too. The startup code has a table with ISR adresses, and it does the initial initialisation of the environment. For example it clears memory locations that should be initialized as "zero" in C. And it loads other initialized variables to a place where they can be used in C. For example, if you have: "int Baudrate = 9600;" in your code, then the startup code assigns an address to "Baudrate", and loads the "9600" from flash and writes it to that variable. The last thing the startup code does is a call of main(), and that is where your own program starts. (I skipped some details).
3). You need a linker script. A linker script is a list of addressees and sizes of memory regions. Such as: What is the address range of FLASH in this uC? How much RAM does it have, and what are it's adresses. The linker script is also provided by the uC vendor.
4). Makefile. A makefile not strictly necessary, but without it, you have to type very long command lines to tell the compiler what to do. GCC has very many options (optimization, which warnings to create, etc.) The makefile automates this. Apart from controlling how the C (and C++) source code is compiled, it also links the parts together and it can call other programs such as objcopy to create intel hex, Srecord, or binary files that can be used to program the uC. The syntax of makefiles was devised by some devious minds in the '60-ies or early '70-ies. It's quite powerful, but horrible to read and write. There are also other build systems such as SCONS (used by platformio for example). In principle you could also write a simple python program to spit out the right command lines to compile and link your code. Some IDE's skip the makefile, and generate the build commands internally in some way.
5). Your source code. At least a text file that contains the "main(){}" function.
6). Header files. At a minimum, you want a header file that defines all the locations of important registers. You do not want to search through datasheets to figure out the address of an I/O register.
7). C standard headers and libraries. These are optional. Code to manipulate for example strings is very common. Therefore, a part of the C standard are pre-defined functions. These may be distributed as source code, but often are already compiled blobs that have to be linked into your code if you want to use them.
https://en.wikipedia.org/wiki/C_standard_library
. "Framework". STM32 has the HAL and the LL stuff. These are also optional. It looks like the HAL is bloated obfuscation, while the LL is a very thin layer between your software and the hardware. I think I like the LL layer. I have for example a library that uses RS485 over a uart, and with the LL layer, I can make this code run on both AVR processors and STM32. LL code is also quite readable, and is at least a learning aid with examples of how to program the STM32 processors. (Unfortunately) these have a quite complex setup. You have to configure PLL's and routing / enabling of different clock domains to make use of the processor.
9). There are other "more elaborate" frameworks. such as "arduino" (ugly obfuscation bloatware) and mbed. (officially discontinued now). In general, I don't like such frameworks much. Such bigger frameworks often make a lot of assumptions. It's easy to get a blinking led going, but to do "useful stuff" you get stuck very quickly. For example, integrating my (already written and optimised) ISR driven uart/rs485 library going on such a bigger framework is difficult. As it hyjacks the serial port for it's own use. Yuch!
10). Third party library code. There are all sorts of libraries. From things like an RTOS, MP3 encoders, TCP/ip networking, LCD controller libraries etc.
11). IDE. For learning how all the parts fit together, a simple text editor is perfectly usable tool to write all code. I use Geany myself, and SciTe before, when I was still using windows. Both these editors also have syntax highlighting, and this helps with making code more readable, and finding simple typo bugs. But when your programs become more complex and you're using more "template" code, you want a more capable IDE, that can for example find win which files functions are defined, and then quickly jump to them. The "modern frameworks" can easily have 50 files with source code and manually loading and scrolling though those files to find a particular library function is quite horrible, but it's easy to manage in a "good" IDE. "Eclipse" (used by ST / CUBE) is a powerful IDE, but very much bloated (long startup times), and when "something breaks" in the build system, it's nearly impossible to repair. Some IDE from microsoft seems to be popular, but I don't use software from that brand. I liked QT Creator, but it complains too much about the "community edition". Used Monodevelop for a while, but that got bought and killed by microsoft. Anjuta never got to a "good usable" state. For now I'm using Codeblocks, which seems a reasonable (for me) compromise between functionality, features and simplicity. Maybe I'll look deeper into "Codelite" some day. I once did some experiments and it did seem usable. Main thing for me is that with a working makefile, the IDE is "optional". I can always fall back to the simplest text editor, and then use "make all", "make flash", etc. from a terminal window.