首先,这篇blog的主要内容是在C语言中调用汇编语言写的函数。即在我们常用的标准库函数工程中,对外设(这里指led)使用汇编语言完成相关的寄存器配置和控制。
课程的需要:微机原理课程设计中,需要使用汇编语言/部分汇编语言进行课程设计的开发。
网上资料的稀缺:汇编相对于高级语言,更加偏向底层,编写代码的效率很低,资料较少,stm32汇编语言与教科书上8086汇编有一些差别,资料更加少。以下引用的例子是网上比较常见的关于stm32使用汇编点亮led灯的教程。
STM32用汇编点亮LED灯
基于 MDK 创建 STM32 汇编程序:串口输出 Hello world
虽然上面两个例子给出了点亮led/串口输出的功能代码,但对其原理方面的描述不是很清晰。
在keil5中,按F1
便可调出ARM内核的工具书,里面包含了ARM内核汇编的指导,如下图所示:
stm32相关的汇编相关的汇编指令
常用的两种函数相互调用方式是在C语言中调用汇编函数,在汇编语言中调用C函数。这里仅仅是函数的调用,调用指.c
文件调用.s
文件中的函数,.s
文件调用.c
文件中的函数,嵌入代码块与这两种不同。(附:C语言中嵌入汇编代码块)
两种调用方式的具体实现教程可以查阅下面的博客:
两种函数调用方式
在C语言中,我们点亮led灯首先需要对led灯对应引脚进行配置,即void LED_INIT(void)
,对led进行开关,即void LED_ON(void)
和void LED_OFF(void)
。然后我们就可以在main
函数中调用这些函数,实现对led的控制。
现在我们的目标是,对void LED_INIT(void)
,void LED_ON(void)
和void LED_OFF(void)
这三个函数用汇编语言编写,这三个函数位于led.s
中,在main.c
文件中被main
函数调用。
实验平台:stm32f103c8t6、MDK5.28
led引脚:C14、C15
端口C的基地址是0x4001 1000
,再由上图我们可以知道AHB系统总线通过桥接1和桥接2连接到了APB2和APB1总线,而AHB总线包含RCC时钟控制,GPIOC是属于APB2的,因此我们可以找到时钟使能寄存器的地址即复位和时钟控制RCC的地址是从0x4002 1000
开始。
通过查阅数据手册,如上图 所示我们可以知道APB2外设时钟使能寄存器的地址为RCC时钟的地址为+0x18
,即RCC_APB2ENR= 0x40021018
。找到APB2外设时钟使能寄存器的地址后,需要对其进行赋值,根据参考手册里面描述,第四位为端口C的时钟使能位。因此将第四位赋为1便可以使能端口C的时钟。即0001 0000
,对于0x10
。
使能外设时钟的代码如下:
LDR R0,=RCC_APB2ENR;LDR是把地址装载到寄存器中(比如R0)。
ORR R0,R0,#0x10 ;按位或ORR R0,R1,R2; R0=R1 | R2
LDR R1,=RCC_APB2ENR;R1存了RCC_APB2ENR的地址
STR R0,[R1];使能端口C的时钟
2.接下来配置PC14、PC15为通用输出。
控制LED需要输出高电平或是低电平,所以需要配置为输出。由于STM32的每个IO都需要4个位来配置,所以一个32位的寄存器最大只能配置8个IO(32位的单片机的寄存器就是32位的)。STM32中,用端口配置低寄存器(GPIOx_CRL)来配置引脚Px0-Px7, 用端口配置高寄存器(GPIOx_CRH)来配置引脚Px8-Px15。
配置引脚PC14,PC15使用的寄存器是GPIOC_CRH。下面我们来寻找这个寄存器的地址。
从上图中,我们可以推得到端口C的配置高寄存器为端口C的基地址是0x40011000+0x04
即0x40011004
,即GPIOC_CRH=0x40011004
。PC15、PC14对应位31:28
,接下来对该寄存器赋值。
复位值是0x4444 4444
,并不是0x0000 0000
。所谓的复位值,就是指如果没有操作这个寄存器时,寄存器存放的默认值。复位值按位拆分0x4 = 0b0100
,0x表示16进制,0b表示二进制,也就是默认CNF 01,MODE 00,是浮空输入。
我们需要的是输出高低电平,所以要设置为输出。输出模式设置为推挽输出,速度为50MHZ。那么PC15、PC14所对应的位31:28
应赋值为:0011 0011
。
代码如下:
;设置pc14、pc15
LDR R0,=GPIOC_CRH
BIC R0,R0,#0xffffffff ;R0清零
ORR R0,#0x33000000
LDR R1,=GPIOC_CRH
STR R0,[R1]
0x33000000
也代表是0011 0011 0000 0000 0000 0000 0000 0000
。
这里对GPIOC_CRH寄存器进行赋值时可能会改变其他位,进而修改到其他引脚的配置,因此尽可能只改变相应的位,不改变其他位。
2.接下来设置PC14、PC15的初始电平状态。(端口输出数据寄存器)
中文数据手册有一个小小的BUG,0x0C写成了0Ch,如上图所示,可以参考英文原版。得知地址的偏移是0x0C,所以引脚PC15的端口输出数据寄存器的地址就是GPIOC_ODR =0x4001 100C
。对 位15 置 1 ,引脚PC15便可以输出高电平。如下代码所示:
LDR R1,=GPIOC_ODR
LDR R0,[R1] ;加载GPIOC_ODR寄存器的值
MOV R1,#~(1<<15)
AND R0,R0,R1
LDR R1,=GPIOC_ODR
STR R0,[R1]
因为修改GPIOC_ODR
寄存器的值可能会修改到其他位,进而改变其他引脚的电平,因此尽可能只改变相应的位,不修改其他位。这里使用#~(1<<15)
与R0
进行与操作,保证只对 位15 进行操作将 位15 置0,输出低电平。
对C14进行重复操作,这时候是#~(1<<14)
这样就完成了对C14、C15的初始化。即完成了LED_INIT
函数
以PC15为例:
LED3_ON;亮灯
PUSH {R0,R1, LR}
LDR R1,=GPIOC_ODR
LDR R0,[R1];加载GPIOC_ODR寄存器的值
MOV R1,#~(1<<15)
AND R0,R0,R1
LDR R1,=GPIOC_ODR
STR R0,[R1]
POP {R0,R1,PC}
具体是#~(1<<15)
还是#(1<<15)
需要查看电路的解法,是高电平亮还是低电平亮。
LED3_OFF;熄灯
PUSH {R0,R1, LR}
LDR R1,=GPIOC_ODR
LDR R0,[R1]
MOV R1,#(1<<15)
ORR R0,R0,R1
LDR R1,=GPIOC_ODR
STR R0,[R1]
POP {R0,R1,PC}
在.c文件中声明函数
extern void LED_INIT(void);
extern void LED3_ON(void);
extern void LED3_OFF(void);
然后像正常函数那样调用就可以了。
stm32汇编跟8086汇编有些不同,使用stm32汇编进行代码逻辑编写确实有些难度,可以使用stm32汇编进行底层代码的编写,底层涉及到的是寄存器,也就是这篇文章所讲的内容。
代码工程附在最下面。实现的功能是将PS2遥控手柄的数据通过串口发送给上位机。因此可以当做3个模块进行学习:汇编实现LED底层,PS2游戏手柄操控,汇编实现串口底层。
本教程的代码仓库:(路径:user/led.s)
STM32汇编语言点亮led灯
拓展资料:
纯汇编实现led、LCD、串口、按键的代码工程
有问题可以评论/发邮件,[email protected]