STM32最基本的,最底层的,就是对寄存器的直接操作。通过操作特定寄存器的特定位,来实现相对应的功能。
本文通过GPIO点亮LED来演示。
GPIO
查阅数据手册,了解相关内容。
启动代码
旧版的keil,在建立工程时会提示自动导入启动代码。
但是新版在建立工程后,会弹出运行时环境配置。选择相应的启动代码。
为了选择这个startup,需要先选择CMSIS里的CORE内核模块。
之后生成了3个启动代码。
旧版只有1个启动代码startup_stm32f10x_hd.s,没有多余的文件,咋回事?
在回答这个问题之前,我们来了解下STM32的启动文件。
在使用 STM32CubeMX 开发 STM32时,往往会看到一个.s 的文件,这个文件就是 STM32 的启动文件,里面包含多个功能,例如堆栈大小配置、STM32 复位后初始化和建立中断服务入口地址等功能。
在支持包中,可以找到相应的启动文件。
启动代码在这里:
F:\czpBuiltin\keil5 mdk\Arm\Packs\Keil\STM32F1xx_DFP\2.4.0\Device\Source\ARM
这里有很多种启动代码,到底应该怎么选呢?
startup_stm32f10x_cl.s 互联型的STM32F105xx,STM32F107xx
startup_stm32f10x_hd.s 大容量的STM32F101xx,STM32F102xx,STM32F103xx
startup_stm32f10x_hd_vl.s 超值型大容量的STM32F100xx
startup_stm32f10x_ld.s 小容量的STM32F101xx,STM32F102xx,STM32F103xx
startup_stm32f10x_ld_vl.s 超值型小容量的STM32F100xx
startup_stm32f10x_md.s 中容量的STM32F101xx,STM32F102xx,STM32F103xx
startup_stm32f10x_md_vl.s 超值型中容量的STM32F100xx
startup_stm32f10x_xl.s超大容量FLASH在512K到1024K字节的STM32F101xx,STM32F102xx,STM32F103xx
ROM容量为:
16~32K就是LD
64K~128K的就是MD
256~512K的就是HD
我这里用的是F103C8,64K的flash,所以用的是md,startup_stm32f10x_md.s。
现尝试将这个启动文件单独复制到工程中。
代码实现
编译后有一个error:
.\Objects\regled.axf: Error: L6218E: Undefined symbol SystemInit (referred from startup_stm32f10x_md.o).
没定义的符号SystemInit,也就是说,该启动文件没有初始化的实现。其中关键的是没有开启时钟,所以无法工作。
来看看这个符号在哪?启动文件中有这一段:
这是复位之后的处理代码,先系统初始化,然后再跳转到main函数中执行。
两种解决方法,要么把带有SystemInit的文件导入进来,要么自己定义一个,可以不写实现,保持为空,也可以直接在这里面开启时钟。
/************************************************************************************************* *PB8-PB15,这8个端口依次控制着8颗LED的正极 * *GPIOB的起始地址为:0x40010C00 * *1、 *GPIOB_CRH进行模式和输出配置,8个端口均为推挽输出,所以该寄存机的值应为0x11111111 *该寄存器的偏移地址为04h,所以GPIOB_CRH寄存器的地址为:0x40010C04 * *2、 *我们先点亮第这8颗LED,所以接下来要先配置输出寄存器GPIOB_ODR,偏移量0x0C,所以地址为:0x40010C08 *要赋予的值为:Ox0000FF00 * *************************************************************************************************/ void SystemInit(void); int main(void) { unsigned int *ledCfg = (unsigned int *)0x40010C04; unsigned int *ledData = (unsigned int *)0x40010C0C; *ledCfg = 0x33333333; *ledData = 0x00005500; while(1); } void SystemInit() { //开启GPIOB时钟,置RCC_APB2ENR寄存器的第3位为1,赋值0x00000008 //RCC基地址为0x40021000,该寄存器的偏移量为18H,则地址为:0x40021018 unsigned int *RCC_APB2ENR = (unsigned int *)0x40021018; *RCC_APB2ENR = 0x00000008; }
注意,因为手册上没有写明GPIOB_ODR的偏移量,所以,按照我的想法,它的地址是按照4个字节来递增的,上一个偏移量是08H,下一个偏移量是10H,所以,我在想,08和10之间不可能隔了4个数呀?咋回事?难道是和上一个寄存器共用一个地址?结果,代码就错了。
这里犯了思维定势的错误。08和10都是16进制的数,那么08H和10H之间不就正好差了8个字节吗?08H和10H之间的不就是0CH吗?我还在疑惑,C换成十进制是12,怎么可能在10H之前呢?所以,千万别搞混了十进制和十六进制。
另外,还可以直接通过复位或者置位寄存器来控制电平高低。操作方法一样,不赘述。
再补充一点,以上代码可以通过宏定义(比如重复格式代码宏定义,基地址宏定义,基地址+偏移量方式宏定义等等)、位操作或者拆分头文件等方式来优化,不赘述。
归根结底
通过操作寄存器来实现对应的功能,是最底层的方式,需要我们通过查阅数据手册,找到对应寄存器的地址,并且了解对应寄存器相应位所表示的含义,从而实现相应功能。
要学会通过串口来调试程序。