汇编怎么调用C函数
直接调用
BL main
传参数
在arm中有个ATPCS规则(ARM-THUMB procedure call standard)(ARM-Thumb过程调用标准)。
约定r0-r15寄存器的用途:
- r0-r3:调用者和被调用者之间传递参数
- r4-r11:函数可能被使用,所以在函数的入口保存它们,在函数的出口恢复它们。
int delay(unsigned int d)
{while(d--);return 0;
}
ldr r0,=1000000
dl delay
cmp r0,#0
返回值保存在r0
A函数调用B函数
假设A函数里需要用到R4寄存器,B函数里也需要用到R4寄存器。
因此B函数就有会覆盖A函数R4寄存器的值。
于是需要在B函数执行前,保存R4-R11寄存器的值在A的栈中 。
B函数执行完后,返回A函数前,要从A的栈中恢复R4-R11寄存器。
C函数的反汇编码阅读
要解决几个问题:
- 为什么调用C函数前要设置栈?栈的作用是?
- C函数传参
- C函数执行过程
反汇编示例
地址0x08000010,Flash上烧写20010000。
启动流程
上电后:
- 设置栈:CPU会从0x08000000读取值,用来设置SP(我们的程序里再次设置了SP)。
- 跳转:CPU会从0x08000004得到地址值,根据它的BIT0切换到ARM状态或Thumb状态,然后跳转。
对于Cortex M3/M4,它只支持Thumb状态,所以0x08000004上的值bit0必定是1。
对0x08000004上的值=Reset_Handler+1。
从Reset_Handler上继续执行
纯汇编点灯
写程序
寄存器操作
对于寄存器的操作,主要涉及读、修改、写。
- 读可以使用LDR指令,代码为LDR R1,[R0]
- 写使用STR指令,代码为STR R1,[R0]
- 修改:清除位使用BIC或AND指令,设置位使用ORR指令
LDR R0, =(1<<20) | (1<<21)
BIC R1,R1,R0 ;清除R1的bit20,bit21
LDR R0, =(1<<20)
ORR R1,R1,R0 ;设置R1的bit20
- 函数里的条件判断
比如减1操作,代码为SUB R0,R0,#1
顺便使用减1后的结果影响程序状态寄存器,代码为SUBS R0,R0,#1 - 程序的调用与返回值
传参,代码为LDR R0,=VAL
调用,代码为BL delay,它顺便把下一条指令的地址保存在LR寄存器了。
返回,代码为MOV PC,LR
使用按键控制LED
先看原理图,我们使用KEY1来控制红色LED,按下KEY1在灯亮,松开后灯灭。
KEY1的引脚是PA0
使能GPIOA模块,RCC_APB2ENR:0x40021000+0x18
串口的硬件介绍
UART的全称是Universal Asynchronous Receiver and Transmitter,即通用异步收发器。
串口在嵌入式中用途非常的广泛,主要的用途有:
- 打印调试信息
- 外接各种模块:GPS、蓝牙
串口因为结构简单、稳定可靠,广受欢迎。
通过三根线即可,发送、接收、地线。
TXD线把PC机要发送的信息发送给ARM开发板。
最下面的地线统一参考地。
串口的参数
- 波特率:一般选波特率都会有9600、19200、115200等选项。其实意思就是每秒传输这么多个比特位数(bit)。
- 起始位:先发出一个逻辑0的信号,表示传输数据的开始。
- 数据位:可以是5~8位逻辑0或1.如ASCII码(7位)、扩展BCD码(8位)。小端传输。
- 校验位:数据位加上这一位后,使得1的位数应为偶数(偶校验)或奇数(奇校验),以此来校验数据传送的正确性。
- 停止位:它是一个字符数据的结束标志。
怎么发送一字节数据,比如A?
'A’的ASCII值是0x41,二进制就是0x01000001,怎么把这8位数据发送给PC机呢?
- 双方约定好波特率(每一位占据的时间)
- 规定传输协议
原来是高电平,ARM拉低电平,保持1bit时间。
PC在低电平处开始计时。
ARM根据数据依次驱动TXD电平,同时PC依次读取RXD电平,获得数据。
PC机在数据位的中间读取引脚状态。
RS-232的电平比TTL/CMOS高,能传输更远的距离,在工业上用得比较多。
市面上大多数ARM芯片都不止一个串口,一般使用串口0来调试,其它串口来外接模块。
115200,8n1。
一秒钟可以传输115200个bit,传送一个字节需要:1个起始位+8个数据位+1个停止位=10bit。
所以一秒钟可以传输11520个字节。
UART编程
看原理图确定引脚
- 有很多串口,使用哪一个?看原理图确定。
- 配置引脚为UART功能
至少用到发送、接收引脚:txd、rxd
需要把这些引脚配置为UART功能,并使能UART模块 - 设置串口参数
- 根据状态寄存器读写数据
肯定有一个数据寄存器,程序把数据写入,即可通过串口向外发送数据。
肯定有一个数据寄存器,程序读取这个寄存器,就可以获得先前接收到的数据。
很多有状态寄存器:判断数据是否发送出去?是否发送成功?判断是否接收到了数据?
需要使能GPIOA/USART1模块
需要设置GPIOA的寄存器,选择引脚功能:所以要使能GPIOA模块。
GPIOA模块、USART1模块的使能都是在同一个寄存器里实现。
USART寄存器,用结构体来表示比较方便:
typedef unsigned int uint32_t;
typedef struct
{volatile uint32_t SR; /*!< USART Status register, Address offset: 0x00 */volatile uint32_t DR; /*!< USART Data register, Address offset: 0x04 */volatile uint32_t BRR; /*!< USART Baud rate register, Address offset: 0x08 */volatile uint32_t CR1; /*!< USART Control register 1, Address offset: 0x0C */volatile uint32_t CR2; /*!< USART Control register 2, Address offset: 0x10 */volatile uint32_t CR3; /*!< USART Control register 3, Address offset: 0x14 */volatile uint32_t GTPR; /*!< USART Guard time and prescaler register, Address
}USART_TypeDef;
USART_TypeDef *usart1 = (USART_TypeDef *)0x40013800;
在C语言中,编译器通常会对代码进行各种优化,以提高程序的执行效率。这些优化包括重新排序指令、删除看似无用的变量读取等。
然而,在与硬件通信的情景下,有些变量的值可能会被硬件或者其他事件异步地修改,而这种修改通常是在程序控制流之外发生的。
假设有一个代表硬件寄存器的变量,如果没有使用’volatile’关键字,编译器可能会进行一些优化,认为这个变量的值在程序的某个地方被设置后就不再改变。这就可能导致一些问题,因为实际上这个变量的值可能会在程序的其它地方被异步地修改。
通过在变量声明中加入’volatile’关键字,告诉编译器不要对这个变量进行过多的优化,以确保每次访问这个变量时都从内存中读取最新的值,而不是使用之前缓存的值。
这对于与硬件直接交互的变量,比如你提供的USART通信寄存器,非常重要,因为这些寄存器的值可能会在程序的正常流程之外被外部事件改变。
#ifndef __UART_H
#define __UART_Hvoid uart_init();
char getchar();
char putchar(char c);#endif
这段代码是一种常见的C/C++预处理器约定,用于防止头文件(header file)被多次包含,避免引起重定义错误。
- #ifndef:这是预处理指令,表示if not defined,即如果之前没有定义过指定的标识符,那么执行接下来的代码。
- __UART_H:这是一个宏标识符,通常是头文件名的大写形式,加上双下划线前缀以确保标识符的唯一性。在这里,它用于作为一个宏,防止头文件被多次包含。
- #define __UART_H:如果之前没有定义过__UART_H,那么用这个#define指令定义它。这样,当头文件再次被引用时,#ifndef检查将会失败,因为__UART_H已经被定义了。
- #endif:表示条件编译的结束。如果之前#ifndef条件成立,那么在这里结束条件编译块。
这种写法的目的是确保一个头文件只会被编译一次,即使它在多个地方被引用。这可以防止由于头文件被重复引用而导致的重定义错误。这是一种预防措施,用于提高代码的可维护性和可移植性。