C与汇编深入分析

汇编怎么调用C函数

直接调用

BL main

传参数
在arm中有个ATPCS规则(ARM-THUMB procedure call standard)(ARM-Thumb过程调用标准)。

约定r0-r15寄存器的用途:

  • r0-r3:调用者和被调用者之间传递参数
  • r4-r11:函数可能被使用,所以在函数的入口保存它们,在函数的出口恢复它们。

C与汇编深入分析_第1张图片

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函数执行过程

在这里插入图片描述

反汇编示例

C与汇编深入分析_第2张图片
地址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上继续执行

纯汇编点灯

C与汇编深入分析_第3张图片
C与汇编深入分析_第4张图片

写程序

寄存器操作
对于寄存器的操作,主要涉及读、修改、写。

  • 读可以使用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

C与汇编深入分析_第5张图片
先看原理图,我们使用KEY1来控制红色LED,按下KEY1在灯亮,松开后灯灭。
KEY1的引脚是PA0

使能GPIOA模块,RCC_APB2ENR:0x40021000+0x18

串口的硬件介绍

UART的全称是Universal Asynchronous Receiver and Transmitter,即通用异步收发器。
串口在嵌入式中用途非常的广泛,主要的用途有:

  • 打印调试信息
  • 外接各种模块:GPS、蓝牙

串口因为结构简单、稳定可靠,广受欢迎。

通过三根线即可,发送、接收、地线。
C与汇编深入分析_第6张图片
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机在数据位的中间读取引脚状态。

C与汇编深入分析_第7张图片
RS-232的电平比TTL/CMOS高,能传输更远的距离,在工业上用得比较多。

市面上大多数ARM芯片都不止一个串口,一般使用串口0来调试,其它串口来外接模块。

C与汇编深入分析_第8张图片
115200,8n1。
一秒钟可以传输115200个bit,传送一个字节需要:1个起始位+8个数据位+1个停止位=10bit。
所以一秒钟可以传输11520个字节。

UART编程

看原理图确定引脚

  1. 有很多串口,使用哪一个?看原理图确定。
  2. 配置引脚为UART功能
    至少用到发送、接收引脚:txd、rxd
    需要把这些引脚配置为UART功能,并使能UART模块
  3. 设置串口参数
  4. 根据状态寄存器读写数据
    肯定有一个数据寄存器,程序把数据写入,即可通过串口向外发送数据。
    肯定有一个数据寄存器,程序读取这个寄存器,就可以获得先前接收到的数据。
    很多有状态寄存器:判断数据是否发送出去?是否发送成功?判断是否接收到了数据?
    在这里插入图片描述
    需要使能GPIOA/USART1模块
    需要设置GPIOA的寄存器,选择引脚功能:所以要使能GPIOA模块。
    GPIOA模块、USART1模块的使能都是在同一个寄存器里实现。

C与汇编深入分析_第9张图片
C与汇编深入分析_第10张图片
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_H

void uart_init();
char getchar();
char putchar(char c);

#endif

这段代码是一种常见的C/C++预处理器约定,用于防止头文件(header file)被多次包含,避免引起重定义错误。

  1. #ifndef:这是预处理指令,表示if not defined,即如果之前没有定义过指定的标识符,那么执行接下来的代码。
  2. __UART_H:这是一个宏标识符,通常是头文件名的大写形式,加上双下划线前缀以确保标识符的唯一性。在这里,它用于作为一个宏,防止头文件被多次包含。
  3. #define __UART_H:如果之前没有定义过__UART_H,那么用这个#define指令定义它。这样,当头文件再次被引用时,#ifndef检查将会失败,因为__UART_H已经被定义了。
  4. #endif:表示条件编译的结束。如果之前#ifndef条件成立,那么在这里结束条件编译块。

这种写法的目的是确保一个头文件只会被编译一次,即使它在多个地方被引用。这可以防止由于头文件被重复引用而导致的重定义错误。这是一种预防措施,用于提高代码的可维护性和可移植性。

你可能感兴趣的:(stm32,c语言,汇编,开发语言)