2
votes

I have a STM32F103C8 MCU, and I want to control GPIO registers without Cube MX. The MCU has an embedded LED and I want control it. I'm currently using CubeMX and IAR Software, and I make the pin an output (in CubeMX) with this code:

HAL_GPIO_TogglePin(Ld2_GPIO_Port,Ld2_Pin); 
HAL_Delay(1000); 

This works, but I want to do it without Cube and HAL library; I want to edit the register files directly.

2

2 Answers

7
votes

Using GPIO using registers is very easy. You fo not have to write your own startup (as ion the @old_timer answer). Only 2 steps are needed

you will need the STM provided CMSIS headers with datatypes declarations and human readable #defines and the reference manual

  1. Enable GPIO port clock. ecample: RCC -> APB2ENR |= RCC_APB2ENR_IOPAEN;
  2. Configure the pins using CRL/CRH GPIO registers
#define GPIO_OUTPUT_2MHz (0b10)
#define GPIO_OUTPUT_PUSH_PULL (0 << 2)
  GPIOA -> CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0);
  GPIOA -> CRL |= GPIO_OUTPUT_2MHz | GPIO_OUTPUT_PUSH_PULL; 
  1. Manipulate the output
  /* to toggle */
  GPIOA -> ODR ^= (1 << pinNummer);
  /* to set */
  GPIOA -> BSRR = (1 << pinNummer);
  /* to reset */
  GPIOA -> BRR = (1 << pinNummer);
  //or
  GPIOA -> BSRR = (1 << (pinNummer + 16));
4
votes

It is very good to know how to do bare metal without the canned libraries, and or to be able to read through those libraries and understand what you are getting yourself into by using them.

This blinks port C pin 13 that is where you generally find the user led on the stm32 blue pill boards. You can figure it out from here and the documentation for the STM32F103C8.

flash.s

.thumb

.thumb_func
.global _start
_start:
stacktop: .word 0x20001000
.word reset
.word loop
.word loop
.word loop
.word loop
.word loop
.word loop
.word loop
.word loop
.word loop
.word loop
.word loop
.word loop
.word loop
.word loop

.thumb_func
reset:
    bl notmain
    b loop
.thumb_func
loop:   b .

.thumb_func
.globl PUT32
PUT32:
    str r1,[r0]
    bx lr

.thumb_func
.globl GET32
GET32:
    ldr r0,[r0]
    bx lr

so.c

void PUT32 ( unsigned int, unsigned int );
unsigned int GET32 ( unsigned int );

#define GPIOCBASE 0x40011000
#define RCCBASE 0x40021000

#define STK_CSR 0xE000E010
#define STK_RVR 0xE000E014
#define STK_CVR 0xE000E018
#define STK_MASK 0x00FFFFFF

static int delay ( unsigned int n )
{
    unsigned int ra;

    while(n--)
    {
        while(1)
        {
            ra=GET32(STK_CSR);
            if(ra&(1<<16)) break;
        }
    }
    return(0);
}

int notmain ( void )
{
    unsigned int ra;
    unsigned int rx;

    ra=GET32(RCCBASE+0x18);
    ra|=1<<4; //enable port c
    PUT32(RCCBASE+0x18,ra);
    //config
    ra=GET32(GPIOCBASE+0x04);
    ra&=~(3<<20);   //PC13
    ra|=1<<20;      //PC13
    ra&=~(3<<22);   //PC13
    ra|=0<<22;      //PC13
    PUT32(GPIOCBASE+0x04,ra);

    PUT32(STK_CSR,4);
    PUT32(STK_RVR,1000000-1);
    PUT32(STK_CVR,0x00000000);
    PUT32(STK_CSR,5);

    for(rx=0;;rx++)
    {
        PUT32(GPIOCBASE+0x10,1<<(13+0));
        delay(50);
        PUT32(GPIOCBASE+0x10,1<<(13+16));
        delay(50);
    }
    return(0);
}

flash.ld

MEMORY
{
    rom : ORIGIN = 0x08000000, LENGTH = 0x1000
    ram : ORIGIN = 0x20000000, LENGTH = 0x1000
}
SECTIONS
{
    .text : { *(.text*) } > rom
    .rodata : { *(.rodata*) } > rom
    .bss : { *(.bss*) } > ram
}

build

arm-none-eabi-as --warn --fatal-warnings flash.s -o flash.o
arm-none-eabi-gcc -Wall -Werror -O2 -nostdlib -nostartfiles -ffreestanding  -mthumb -c so.c -o so.o
arm-none-eabi-ld -o so.elf -T flash.ld flash.o so.o
arm-none-eabi-objdump -D so.elf > so.list
arm-none-eabi-objcopy so.elf so.bin -O binary

PUT32/GET32 is IMO a highly recommended style of abstraction, decades of experience and it has many benefits over the volatile pointer or worse the misuse of unions thing that is the current FAD. Not meant to be a library but to show code that does not require any libraries, only the files provided are required.

Most mcus you need to enable clocks to the peripheral before you can talk to it. You can see the read-modify-write of an RCC register.

Most MCUs the GPIO pins reset to inputs so you need to set one to an output to drive/blink an led. Even within the STM32 world but certainly across brands/families the GPIO (and other) peripherals are not expected to be identical nor even compatible so you have to refer to the documentation for that part and it will show how to make a pin an output. very good idea to read-modify-write instead of just write, but since you are in complete control over the chip you can just write if you wish, try that later.

This chip has a nice register that allows us to change the output state of one or more but not necessarily all GPIO outputs in a single write, no read-modify-write required. So I can set or clear pin 13 of GPIOC without affecting the state of the other GPIOC pins.

Some cortex-ms have a systick timer, for example not all cortex-m3s have to have one it is up to the chip folks usually and some cores may not have the option. This chip does so you can use it. In this example the timer is set to roll over every 1 million clocks, the delay function waits for N number of rollovers before returning. so 50,000,000 clocks between led state changes. since this code runs right from reset without messing with the clocking or other systems, the internal HSI 8MHz clock is used 50/8 = 6.25 seconds between led state changes. systick is very easy to use, but remember it is a 24 bit counter not 32 so if you want to do now vs then you must mask it.

I don't remember if it is an up counter

elapsed = (now - then) & 0x00FFFFFF;

or down

elapsed = (then - now) & 0x00FFFFFF;

(now = GET32(systick count register address))

The systick timer is in the arm documentation not the chip documentation necessarily although sometimes ST produces their own version, you want the arm one for sure and maybe then the st one. infocenter.arm.com (you have to give up an email address or you can Google sometimes you get lucky, someone will illegally post them somewhere) this chip will tell you it uses a cortex-m3 so find the technical reference manual for the cortex-m3 in that you will find it is based on architecture armv7-m so under architecture find the armv7-m documentation, between these you see how the vector table works, the systick timer and its addresses, etc.

Examine vector table

Disassembly of section .text:

08000000 <_start>:
 8000000:   20001000    andcs   r1, r0, r0
 8000004:   08000041    stmdaeq r0, {r0, r6}
 8000008:   08000047    stmdaeq r0, {r0, r1, r2, r6}
 800000c:   08000047    stmdaeq r0, {r0, r1, r2, r6}
 8000010:   08000047    stmdaeq r0, {r0, r1, r2, r6}
 8000014:   08000047    stmdaeq r0, {r0, r1, r2, r6}
 8000018:   08000047    stmdaeq r0, {r0, r1, r2, r6}
 800001c:   08000047    stmdaeq r0, {r0, r1, r2, r6}
 8000020:   08000047    stmdaeq r0, {r0, r1, r2, r6}
 8000024:   08000047    stmdaeq r0, {r0, r1, r2, r6}
 8000028:   08000047    stmdaeq r0, {r0, r1, r2, r6}
 800002c:   08000047    stmdaeq r0, {r0, r1, r2, r6}
 8000030:   08000047    stmdaeq r0, {r0, r1, r2, r6}
 8000034:   08000047    stmdaeq r0, {r0, r1, r2, r6}
 8000038:   08000047    stmdaeq r0, {r0, r1, r2, r6}
 800003c:   08000047    stmdaeq r0, {r0, r1, r2, r6}

08000040 <reset>:
 8000040:   f000 f806   bl  8000050 <notmain>
 8000044:   e7ff        b.n 8000046 <loop>

08000046 <loop>:
 8000046:   e7fe        b.n 8000046 <loop>

The entry point code with our vector table which starts off with the value we would like to put in the stack pointer on reset should be the first thing, then vector tables which are the address of the handler ORRed with 1 (not as easy to find in the docs sometimes). the disassembly of these addresses is because I used the disassembler to view them those are not actual instructions in the vector table it is a table of vectors. the tool is just doing its best to disassemble everything, if you look at the rest of the output it also disassembles the ascii tables and other things which are also not code.

.data is not supported in this example a bunch more work would be required.

I recommend if/when you get yours working you then examine the HAL library sources to see that when you dig through layers of sometimes bloated or scary code, you will end up with the same core registers, they may choose to always configure all the gpio registers for example, speed and pull up/down, turn off the alternate function, etc. Or not. the above knows it is coming out of reset and the state of the system so doesn't go to those lengths for some peripherals you can pop the reset for that peripheral and put it in a known state rather than try to make a library that anticipates it being left in any condition and trying to configure from that state. YMMV.

It is good professionally to know how to work at this level as well as how to use libraries. An MCU chip vendor will often have two libraries, certainly for older parts like these, the current library product and the legacy library product, when a new library comes out to keep it fresh and competitive (looking) the oldest one will drop off from support and you sometimes have current and prior. depends on the vendor, depends on the part, depends on how they manage their software products (same goes for their IDEs and other tools).

Most of the stm32 parts esp a blue pill and other boards you can get do not require the fancy IDEs to program but external hardware is sometimes required unless you get a NUCLEO or Discovery board then you have at least enough to program the part with free software not attached to ST. with a nucleo it is mbed style where you simply copy the .bin file to the virtual usb drive and the board takes care of programming the development MCU.