作者: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 规范中定义的中断向量表。
我们只需将 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 中定义的中断向量表。
.text 段存放的是程序和只读变量:
内存初始化
上面我们提到使用 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:
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