C与汇编深入分析

汇编怎么调用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指令
c 复制代码
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编程

看原理图确定引脚

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

    需要使能GPIOA/USART1模块
    需要设置GPIOA的寄存器,选择引脚功能:所以要使能GPIOA模块。
    GPIOA模块、USART1模块的使能都是在同一个寄存器里实现。


USART寄存器,用结构体来表示比较方便:

c 复制代码
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通信寄存器,非常重要,因为这些寄存器的值可能会在程序的正常流程之外被外部事件改变。

c 复制代码
#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条件成立,那么在这里结束条件编译块。

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

相关推荐
Q_Q5110082853 分钟前
python题库及试卷管理系统
开发语言·spring boot·python·django·flask·node.js·php
电商API_1800790524730 分钟前
淘宝天猫商品数据爬取方案:官方API与非官方接口对比
linux·服务器·开发语言·前端·爬虫·python·数据挖掘
mxpan38 分钟前
golang 一种并发安全 map 的实现
开发语言·golang
漫无目的行走的月亮1 小时前
微信小程序,微信授权手机号码
开发语言·javascript·ecmascript
我命由我123451 小时前
Android 开发问题:CardView 的阴影效果会受到父容器的裁切
android·java·开发语言·java-ee·android studio·android-studio·android runtime
追风赶月、1 小时前
【QT】控件二(输入类控件、多元素控件、容器类控件与布局管理器)
开发语言·qt
API_technology1 小时前
亚马逊 API 实战:商品详情页实时数据采集接口开发与调用
大数据·开发语言·python·数据挖掘
vhgcc2 小时前
在 Java 中使用 Apache Tika 读取 doc、docx等格式文件内容
java·开发语言·自然语言处理·apache·ai编程
全干engineer1 天前
Web3-Web3.js核心操作:Metamask、合约调用、事件订阅全指南
开发语言·javascript·web3·区块链·智能合约
刘一说1 天前
资深Java工程师的面试题目(六)数据存储
java·开发语言·数据库·面试·性能优化