首页 > 编程语言 >代码优化:使用C语言重构单片机汇编启动代码

代码优化:使用C语言重构单片机汇编启动代码

时间:2024-08-04 14:25:28浏览次数:12  
标签:初始化 代码 ALIGN C语言 单片机 代码优化 Handler main uint32

目录

前言

在近期的工作需求中,我需要对实时操作系统(RTOS)的底层代码进行一些必要的调整。为此,我重新深入研究了ARM架构的相关知识点。在这一过程中,我回顾了之前一直让我感到困惑的一个技术问题:在几乎所有的单片机项目代码库中,都存在一个名为start.s的汇编语言启动代码文件。由于我并没有系统地学习过汇编语言,因此对于这段代码的理解总是停留在一知半解的层面。

在整个项目中,通常只有一个汇编文件,专门去学习汇编语言似乎显得有些不必要。因此,我考虑是否可以使用C语言来实现这一汇编代码的功能。这样的做法不仅可以避免学习新的语言,而且可能在某些情况下提供更高的代码可读性和可维护性。

实现思路

单片机启动流程

单片机的启动流程可以大致归纳为以下几步:

  1. 上电或复位信号触发,单片机开始启动过程。
  2. 硬件检查栈指针初始化,通常指向预设的栈顶地址。
  3. 硬件从中断向量表中读取复位处理程序的入口地址。
  4. 执行复位处理程序(Reset_Handler),进行系统初始化。
  5. 复位处理程序从 Flash 复制初始化数据到 RAM。
  6. 复位处理程序清零 BSS 段,初始化未初始化的数据段。
  7. 复位处理程序跳转到 main 函数,开始执行主程序。

因此理论上,只要能完成以上步骤,无论使用用什么编程语言,都可以实现,其中 2 中栈起始位置的设置,4 - 7 Reset_Handler函数的实现是我们需要做的,其余是硬件已经做好的。

汇编语言分析

先看一段CubeMX生成的项目中的startup.s

  .syntax unified
  .cpu cortex-m3
  .fpu softvfp
  .thumb

.global g_pfnVectors
.global Default_Handler


.word _sidata
.word _sdata
.word _edata
.word _sbss
.word _ebss

.equ  BootRAM, 0xF108F85F

  .section .text.Reset_Handler
  .weak Reset_Handler
  .type Reset_Handler, %function
Reset_Handler:

  ldr r0, =_sdata
  ldr r1, =_edata
  ldr r2, =_sidata
  movs r3, #0
  b LoopCopyDataInit

CopyDataInit:
  ldr r4, [r2, r3]
  str r4, [r0, r3]
  adds r3, r3, #4

LoopCopyDataInit:
  adds r4, r0, r3
  cmp r4, r1
  bcc CopyDataInit
  
  ldr r2, =_sbss
  ldr r4, =_ebss
  movs r3, #0
  b LoopFillZerobss

FillZerobss:
  str  r3, [r2]
  adds r2, r2, #4

LoopFillZerobss:
  cmp r2, r4
  bcc FillZerobss

  bl __libc_init_array
  bl main
  bx lr
.size Reset_Handler, .-Reset_Handler

   .section .text.Default_Handler,"ax",%progbits
Default_Handler:
Infinite_Loop:
  b Infinite_Loop
  .size Default_Handler, .-Default_Handler

  .section .isr_vector,"a",%progbits
  .type g_pfnVectors, %object
  .size g_pfnVectors, .-g_pfnVectors
g_pfnVectors:

  .word _estack
  .word Reset_Handler
  .word NMI_Handler
  .word HardFault_Handler
  /* 省略后面的项 */
  /*	...     */


  .weak NMI_Handler
  .thumb_set NMI_Handler,Default_Handler

  .weak HardFault_Handler
  .thumb_set HardFault_Handler,Default_Handler
  /* 省略后面的项 */
  /*	...     */

在这段代码中,其实最主要事就是两件

  • 定义了入口函数Reset_Handler,通过它最终进入main函数
  • 定义中断向量表,其中最主要的是前面两项:设置栈起始位置,设置入口函数

因此我们可以用C语言去做这两件事

开发环境

  • 硬件平台

stm32f103c8t6最小系统板。便宜

  • 编译器

gcc-arm-none-eabi + make。由于要保证底层的运行都是可控的,因此使用这一套工具链使用起来会更加方便。

  • 调试器

openocd + stlink。用于烧录调试代码

  • 代码编辑器

vscode。好用,懂的都懂

代码分析

废话不多说,上代码。总共有以下三个代码文件main.clinker.ldMakefile

  • main.c实现功能的主要代码。

    1. 实现了reset_handler函数

    2. 定义了中断向量表

typedef unsigned int uint32_t;

/* 声明所需寄存器 */
#define RCC_APB2ENR *(uint32_t*)0x40021018
#define GPIOC_CRH *(uint32_t*)0x40011004
#define GPIOC_ODR *(uint32_t*)0x4001100c

/* 在ld文件中声明的地址 */
extern uint32_t _estack;  /* 栈顶地址 */
extern uint32_t _sdata;   /* 已初始化数据段起始地址(Flash)*/
extern uint32_t _edata;   /* 已初始化数据段结束地址(Flash)*/
extern uint32_t _sidata;  /* 已初始化数据段起始地址(SRAM)*/
extern uint32_t _sbss;    /* 未初始化数据段(BSS)起始地址 */
extern uint32_t _ebss;    /* 未初始化数据段(BSS)结束地址 */

/* 主函数声明 */
int main(void);           

/* 硬件复位后开始执行的启动代码 */
void reset_handler(void)
{
    uint32_t *src, *dest;
    uint32_t *end;

    /* 复制数据段初始化值从Flash到SRAM */
    src = (uint32_t *)&_sidata;
    dest = (uint32_t *)&_sdata;
    end = (uint32_t *)&_edata;
    while (dest < end) {
        *dest++ = *src++;
    }

    /* 清零BSS段 */ 
    dest = (uint32_t *)&_sbss;
    end = (uint32_t *)&_ebss;
    while (dest < end) {
        *dest++ = 0;
    }

    /* 进入main */
    main();

    /* 以防main函数返回 */
    while (1) {
        ;
    }
}

/* 设置中断向量表 */
typedef void (*irq_handler)(void); /* 中断向量类型 */

/* 将中断向量表放在.isr_vector段 */
__attribute__((section(".isr_vector")))
__attribute__((aligned(4)))
irq_handler g_pfn_vectors[] = {
    (irq_handler) &_estack,
    reset_handler,
};

/* 延时1秒 */
void delay(void)
{
    volatile int times = 1000000;
    while (times--) {
        ;
    }
}

/* LED(PC13)闪烁,间隔1秒 */
int main(void)
{
    RCC_APB2ENR |= (1 << 4);
    GPIOC_CRH |= (1 << 20); /* GIPOC13 */

    while (1) {
        GPIOC_ODR |= (1 << 13); /* 拉高默认低 */
        delay();
        GPIOC_ODR &= ~(1 << 13); /* 拉低 */
        delay();
    }
}

  • linker.ld规定将生成的二进制文件放在内存中对应的位置,比如中断向量表存放在0x08000000这个位置,就是在这个文件中规定的。
    • 基本都是固定写法,用汇编和C区别不大。定义了内存中各个段的位置变量,方便在其它文件中进行操作,如main.c对各个段的初始化操作都需要用到这里定义的变量,中断向量表存放的段也在这里声明。
ENTRY(reset_handler) /* 程序入口地址(可以不写,为了可读性) */

_estack = ORIGIN(RAM) + LENGTH(RAM); /* 栈向下生长,起始位置为RAM地址最后 */
_Min_Heap_Size = 0x200;  /* 堆内存大小  */
_Min_Stack_Size = 0x400; /* 栈内存大小 */

/* 内存分布 */
MEMORY
{
    RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K
    FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 64K
}

/* 定义输出段 */
SECTIONS
{
    /* 启动代码从FLASH的第一段开始 */
    .isr_vector :
    {
        . = ALIGN(4);
        KEEP(*(.isr_vector)) /* 向量表 */
        . = ALIGN(4);
    } >FLASH

    /* 程序代码和其他数据进入FLASH */
    .text :
    {
        . = ALIGN(4);
        *(.text)     
        *(.text*)    /* 代码 */
        KEEP (*(.init))
        KEEP (*(.fini))
        . = ALIGN(4);
        _etext = .;   
    } >FLASH

    /* 只读数据段存放在FLASH中 */
    .rodata :
    {
        . = ALIGN(4);
        *(.rodata)         /* 只读数据 */
        *(.rodata*) 
        . = ALIGN(4);
    } >FLASH

    /* 用于初始化数据段 */
    _sidata = LOADADDR(.data);

    /* 已初始化的数据段会加载到RAM中,加载内存地址副本会在代码段之后被创建 */
    .data : 
    {
        . = ALIGN(4);
        _sdata = .;        /* create a global symbol at data start */
        *(.data)           /* .data sections */
        *(.data*)          /* .data* sections */

        . = ALIGN(4);
        _edata = .;        /* define a global symbol at data end */
    } >RAM AT> FLASH


    /* 未初始化数据段 */
    . = ALIGN(4);
    .bss :
    {
        _sbss = .;
        __bss_start__ = _sbss;
        *(.bss)
        *(.bss*)
        *(COMMON)

    . = ALIGN(4);
    _ebss = .;
    __bss_end__ = _ebss;
    } >RAM

    /* 
     * 此段不占内存,用于检查RAM中是否足够存储stack和heap 
     * 如果内存不够,链接器会报错
     */
    ._user_heap_stack : 
    {
    . = ALIGN(8);
    PROVIDE ( end = . );
    PROVIDE ( _end = . );
    . = . + _Min_Heap_Size;
    . = . + _Min_Stack_Size;
    . = ALIGN(8);
    } >RAM

    /* 移除标准库中没用使用的信息,减少最后生成文件的内存 */
    /DISCARD/ :
    {
    libc.a ( * )
    libm.a ( * )
    libgcc.a ( * )
    }

    .ARM.attributes 0 : { *(.ARM.attributes) }
}

  • Makefile文件多的时候方便构建所有代码,本次由于编译选项较多,为了方便用make构建,也可以不用。
#编译器
PREFIX = arm-none-eabi-
CC = $(PREFIX)gcc
AS = $(PREFIX)gcc -x assembler-with-cpp
OBJCOPY = $(PREFIX)objcopy
OBJDUMP = $(PREFIX)objdump
SZ = $(PREFIX)size

#生成目标
TARGET = main

#可选
OPT = -Og

# C文件
C_SOURCES =  main.c
ASM_SOURCES =

HEX = $(OBJCOPY) -O ihex
BIN = $(OBJCOPY) -O binary -S

CPU = -mcpu=cortex-m3

MCU = $(CPU) -mthumb $(FPU) $(FLOAT-ABI)

ASFLAGS = $(MCU) $(OPT) -Wall -fdata-sections -ffunction-sections
CFLAGS += $(MCU) $(OPT) -Wall -fdata-sections -ffunction-sections

CFLAGS += -g -gdwarf-2 -MMD -MP -MF"$(@:%.o=%.d)"

#链接脚本
LDSCRIPT = c8t6.ld

LIBS = -lc -lm -lnosys 
LIBDIR = 
LDFLAGS = $(MCU) -specs=nano.specs -T$(LDSCRIPT) $(LIBDIR) $(LIBS) -Wl,-Map=$(TARGET).map,--cref -Wl,--gc-sections -nostartfiles

all: $(TARGET).elf $(TARGET).hex $(TARGET).bin

OBJECTS = main.o 

%.o: %.c $(LDSCRIPT)
	$(CC) -c $(CFLAGS) -Wa,-a,-ad,-alms=$(notdir $(<:.c=.lst)) $< -o $@

%.o: %.s $(LDSCRIPT)
	$(AS) -c $(CFLAGS) $< -o $@

$(TARGET).elf: $(OBJECTS) Makefile
	$(CC) $(OBJECTS) $(LDFLAGS) -o $@
	$(SZ) $@

%.hex: %.elf
	$(HEX) $< $@
	
%.bin: %.elf
	$(BIN) $< $@	

#下载命令
install: $(TARGET).hex
	openocd -f openocd.cfg -c "program main.hex verify reset" -c "reset run" -c "exit"

.PHONY: clean
clean:
	-rm -f $(OBJECTS) $(TARGET).elf $(TARGET).hex $(TARGET).lst $(TARGET).bin $(TARGET).map ./*.d 

总结

第一次发帖,可能讲不够细致,欢迎大家评论,我都会一一回复。

标签:初始化,代码,ALIGN,C语言,单片机,代码优化,Handler,main,uint32
From: https://blog.csdn.net/weixin_44884878/article/details/140905771

相关文章

  • 15.75.【C语言】表达式求值
    目录一.整型提升1.定义2.一.整型提升1.定义C语言中整型算术运算总是至少以缺省(默认)整型类型的精度来进行的。为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升2.整型提升的原因:表达式的整型运算要在CPU的相应运算器件......
  • 关于C语言中素数的求解
    什么是素数?一个大于1的自然数,除了1和它自身外,不能被其他自然数整除的数叫做素数。(素数=质数)在C语言中求解素数的几种方法。方法一:直接试数法(从1开始逐一试数)例:求解100到200之间的素数。#include<stdio.h>intmain(){ inti=0; intcount=0; for(i=100;i<=......
  • C语言关于函数的基本介绍
    目录一、前言二、为什么需要函数?三、什么是函数?四、函数的作用及分类。五、函数的基本用法。六、主函数的简单介绍。七、函数原型以及一些常用的系统函数的介绍。一、前言    这些都是作者学习C语言过程中了解到的只是,有的地方可能不是写的特别清楚,同时这也是......
  • 【C语言】C语言期末突击/考研--函数
    目录一、函数的声明与定义-嵌套调用1.1.函数的声明与定义1.2.函数的分类与调用二、函数的递归调用三、局部变量与全局变量3.1.全局变量解析形参实参解析3.2.局部变量与全局变量四、练习题及解析一、函数的声明与定义-嵌套调用1.1.函数的声明与定义    函数间......
  • 【C语言】字符函数和字符串函数详解
    ......
  • 【C语言】结构体内存布局解析——字节对齐
    ......
  • C语言:动态内存管理
    动态内存管理一、动态分配内存的必要性普通内存分配动态内存分配二、动态内存分配函数(一)malloc(二)calloc(三)realloc(四)free三、常见的错误(一)对空指针进行解引用操作(二)对动态分配空间越界访问(三)free释放动态分配空间的一部分(四)动态开辟内存忘记释放四、柔性数组(一)柔性数组......
  • C语言基础8数组
    什么是数组数组是相同类型,有序数据的集合。数组的特征数组中的数据被称为数组的元素,是同构的数组中的元素存放在内存空间里  衍生概念:下标(索引)下标或索引代表了数组中元素距离第一个元素的偏移位置数组中元素的地址值,下标越大,地址值越大。(每一块内存空间都有一个独有的......
  • C语言基础7循环结构
    什么是循环        代码的重复执行,就叫做循环。循环的分类无限循环:程序设计中尽量避免无限循环。(程序中的无限循环必须可控)有限循环:循环限定循环次数或者循环的条件循环的构成循环体循环条件当型循环的实现while语法:while(循环条件){循环语句;}......
  • C语言基础6分支结构
    分支结构分支结构:又被称之为选择结构概念选择结构:根据条件成立与否,选择相应的操作条件建构关系表达式:含有关系运算符的表达式(<,>,>=,<=,!=,==)逻辑表达式:含有逻辑运算符的表达式(&&,||,!),往往用来构建复杂的符合条件:比如:常量/变量:值是否非0,取值(0|1)注意:类似以下写法,没有意义......