极术小姐姐 · 2020年07月30日

Cortex-M3 入门指南(四):中断向量表与内存初始化

作者:Andy Lok
原文首发知乎:https://zhuanlan.zhihu.com/p/73921538
这一节我们要进一步深入 Cortex-M3 的初始化阶段和初步学习中断向量表。由于这一节内容与 从零开始构建实时抢占式内核 高度重合,本篇文章将作为内核系列的前序文章,源码也作为 Preemptive 项目的 Chapter1 放在了 Github 上。
在前几篇文章中我们并没有涉及内存初始化的内容,这是因为我们引入了 cortex-m-rt ,这个库帮助我们完成了中断向量表的定义以及内存初始化的工作。在本章中我们将丢开 cortex-m-rt ,手动实现一遍这些内容,这非常有助于进一步理解 Cortex-M3 基础知识以及以后更好地使用 cortex-m-rt。

中断

中断是嵌入式系统的重要功能,它类似于平时常见的事件,程序可以进行中断响应,与事件不同的是中断由 CPU 产生。其实桌面 CPU 也有中断功能,只是这些中断一般都由操作系统处理,而在裸机单片机编程中,中断将全权由我们控制。产生中断时,中断处理函数可以打断主程序的执行,并且在处理完成后重新交回给主程序。
很多种情况都可以产生中断,比如说:

  • 单片机重启复位
  • 发生未知错误
  • 时钟计时完成
  • 外部引脚电平发生改变
  • 串口数据发送完成
  • 收到串口信息

本章唯一使用到的中断是重启复位中断,它会在单片机上电以及重置的时候被激活,我们可以(也只能)在这里进行单片机的初始化。

我们在 src/startup.rs 中定义它:

//! src/startup.rs

#[no_mangle]
pub unsafe extern "C" fn reset_handler() {
    init_data(&mut _sidata, &mut _sdata, &mut _edata);
    zero_bss(&mut _sbss, &mut _ebss);

    crate::main();
}

init_data() 和 zero_bss() 会对全局变量进行初始化,我们一会再讲。

crate::main() 是用户主函数,在内存初始化完成后将被调用,我们把它定义在 src/main.rs:

//! src/main.rs

mod startup;

#[no_mangle]
#[inline(never)]
fn main() -> ! {
    loop {}
}

中断向量表

中断被激活时,硬件会尝试跳转到我们提供的中断处理函数。而硬件怎么找到我们的中断处理函数呢?每种架构的 CPU 都会有一个固定的中断向量表,中断向量表按照中断号顺序存储中断处理函数的函数地址,当发生中断时,硬件会顺序查找对应的函数地址并跳转。

比如说,下面就是 Cortex-M3 规范中定义的中断向量表。
2.jpg
我们只需将 Reset 中断对应的处理函数地址指向我们的初始化函数即可。这里要注意,中断向量表的第 0 个向量定义的不是中断函数,而是栈的起始地址,后面我们会将它设置为 RAM 区的结束地址(因为栈地址是从高地址向低地址增长的)。

我们在 src/startup.rs 中定义它:

//! src/startup.rs

// interrupt vertor that will be linked to the very start of FLASH
#[link_section = ".isr_vector"]
#[used]
pub static ISR_VECTOR: [unsafe extern "C" fn(); 1] = [reset_handler];

[link_section] 指定将这个全局数组链接到 .isr_vector。

链接脚本

中断向量表中中断号的顺序是由架构定义的,而中断向量表本身存储的位置是由芯片厂商规定的,比方说 STM32F103 根据启动引脚的接线方式,一共有 3 种中断向量表的储存位置。一般情况下我们会将中断向量表存放在 ROM 的最开头,本文先跳过另外两种不常见的启动方式。

STM32F103 的 ROM 段地址起始于 0x08000000,那么我们需要将 RAM 区地址结尾作为栈开始地址写入 0x08000000,Reset 中断处理函数地址写入 0x08000004。我们使用链接脚本来定义这样的内存排布。我们常用的是 GNU Linker Script 格式。没接触过的朋友可能会觉得链接脚本很生疏难懂,其实大家都是这么过来的,看多了就习惯了(笑)。

先看一段简单的链接脚本,保存为 layout.ld:

//! layout.ld

MEMORY
{
    FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 128K
    RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
} 

ENTRY(reset_handler);

SECTIONS
{
    _sstack = ORIGIN(RAM) + LENGTH(RAM);

    .isr_vector :
    {
        . = ALIGN(4);

        /* Initial Stack Pointer (SP) value */
        LONG(_sstack);

        KEEP(*(.isr_vector))

        . = ALIGN(4);
    } > FLASH

    .text :
    {
        . = ALIGN(4);

        *(.text .text.*)
        *(.rodata .rodata.*)

        . = ALIGN(4);

    } > FLASH
}

我们把它分开来看:

首先 MEMORY 定义了 STM32F103 的 RAM 和 ROM 的地址与大小,这些数据都是可以通过芯片手册获取的。

MEMORY
{
    FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 128K
    RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}

接下来,ENTRY 定义了程序的起始地址,也就是重置中断处理函数。因为我们给 reset_handler 添加了 #[no_mangle] 标签,所以我们在可以通过函数名直接引用它,不然它的函数名会被 Rust 编译器添加上一个随机生成的后缀。

ENTRY(reset_handler);
然后我们在 SECTIONS 中定义了栈的起始地址 _sstack,由 ORIGIN(RAM) + LENGTH(RAM) 字面很容易理解,即为 RAM 区的结尾。

另外我们定义了 .isr_vector 段,用来摆放中断向量表,由结尾的 '> FLASH' 可知它会被安排在 FLASH 区,而按顺序,它将刚好在 FLASH 区的开头。

'. = ALIGN(4);' 语句中的句点表示当前地址,整句的意思是强制保证当前地址与 32 位对齐。

'LONG(_sstack);' 到这里为止刚好是中断向量表第一个向量,用来存放栈的起始地址。

'KEEP(*(.isr_vector))' 导入 src/startup.rs 中定义的中断向量表。
image.png
.text 段存放的是程序和只读变量:
image.png

内存初始化

上面我们提到使用 init_data() 和 zero_bss() 对全局变量进行初始化。全局变量有两种,一种是有初始值的,一种是无初始值的。用 C 作例子:

// 有初始值
static int COUNT = 0;

// 无初始值
static int VAR;
编译器会分别将有初始值和无初始值链接到 .data 和 .bss 段。我们在链接脚本中把它们加到 .text 段后面:

//! layout.ld

SECTIONS
{
    // code omitted

    .data :
    {
        . = ALIGN(4);

        *(.data)
        *(.data.*)

        . = ALIGN(4);
    } > RAM AT > FLASH

    /* VMA of .data */
    _sdata = ADDR(.data);
    _edata = ADDR(.data) + SIZEOF(.data);

    /* LMA of .data */
    _sidata = LOADADDR(.data);

    .bss (NOLOAD) :
    {
        . = ALIGN(4);

        *(.bss)
        *(.bss.*)

        . = ALIGN(4);
    } > RAM

    _sbss = ADDR(.bss);
    _ebss = ADDR(.bss) + SIZEOF(.bss);
}

全局变量都为可变变量,所以存放在 RAM 区,其中 .bss 段存储无初始值全局变量,所以只需划分出相应大小的内存块,而不用真实导入数据,所以加上了 (NOLOAD) 标记。.data 段就比较复杂,因为它有初始值,所以我们不仅要在 RAM 区划出内存块,还要在 ROM 区保存一份它的初始值。'> RAM AT > FLASH' 刚好就可以定义这样的内存排布,'AT >' 意为真实数据存放这里。

我们还定义了几个链接地址 _sdata, _edata, _sidata, _sbss 和 _ebss。分别为 .data 开始地址,结束地址,初始值地址,.bss 开始地址和结束地址。

接下来便很简单了,只需在 reset_handler 中将 .bss 段内存清零,给 .data 段赋予初始值即可。

   //! src/startup.rs

extern "C" {
    static mut _sidata: u32;
    static mut _sdata: u32;
    static mut _edata: u32;
    static mut _sbss: u32;
    static mut _ebss: u32;
}

unsafe fn init_data(mut sidata: *const u32, mut sdata: *mut u32, edata: *mut u32) {
    while sdata < edata {
        sdata.write(sidata.read());
        sdata = sdata.offset(1);
        sidata = sidata.offset(1);
    }
} 

unsafe fn zero_bss(mut sbss: *mut u32, ebss: *mut u32) {
     while sbss < ebss {
        sbss.write_volatile(0);
        sbss = sbss.offset(1);
    }
}

Blinky
到这里为止内存初始化和中断向量表都已经完成了,按照国际惯例,此处应有 Blinky (笑)。

写入 Cargo.toml:

[dependencies]
cortex-m = "0.5"
stm32f103xx = "0.11"

写入 src/led.rs:

//! src/led.rs

pub fn led_init(rcc: &mut stm32f103xx::RCC, gpiob: &mut     stm32f103xx::GPIOB) {
    // enable gpiob
    rcc.apb2enr.write(|w| w.iopben().enabled());

    // configurate PB12 as push-pull output
    gpiob.crh.write(|w| w.mode12().output50().cnf12().push());
}

pub fn set(on: bool) {
    let gpiob = unsafe { &*stm32f103xx::GPIOB::ptr() };
    if on {
        gpiob.bsrr.write(|w| w.br12().reset());
    } else {
        gpiob.bsrr.write(|w| w.bs12().set());
    }
}

这里的知识在 Cortex-M3 入门指南(二):寄存器 有详细介绍,看不懂的话可以去复习一下。
写入 src/main.rs:
image.png
image.png
panic_fmt 是 Rust 编译器要求必须定义的 panic 处理函数,我们就先暂时简单糊弄一下。

运行

1.

openocd
64-bits Open On-Chip Debugger 0.10.0-dev-00289-g5eb5e34 (2016-09-03-09:40)
Licensed under GNU GPL v2

Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints
2.

cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.93s
Running `target\thumbv7m-none-eabi\debug\preemptive`

Reading symbols from target\thumbv7m-none-eabi\debug\preemptive...done.
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x080008fc msp: 0x20005000
Loading section .isr_vector, size 0x40 lma 0x8000000
Loading section .text, size 0x2a08 lma 0x8000040
Start address 0x80008fc, load size 10824
Transfer rate: 14 KB/sec, 5412 bytes/write.
(gdb) continue
Continuing.
then Blink Blink Blink

推荐阅读

Cortex-M3 入门指南(二):寄存器
Cortex-M3 入门指南(三):时钟总线与复位时钟控制器

推荐阅读
关注数
23361
内容数
884
Arm相关的技术博客,提供最新Arm技术干货,欢迎关注
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息