由图可知,用C语言进行stm32的程序开发,仍然是:写代码--->编译、连接--->下载到flash这样一个过程。只不过除此以外,我认为比较重要的还需要
知道这样几点:
1、如何访问此种单片机的外围设备寄存器;
2、如何书写此种单片机的中断服务程序;
3、此种单片机复位后,从什么地址处开始执行代码;然后我们如何告诉编译工具把代码按照这个入口地址开始安排我们的代码。
4、需不需要为构建C语言的运行环境作一些工作,也就是启动代码。
5、通过命令行选项通知编译器为特定的单片机生成代码。
三、编写一个最精简的代码
1、一个main函数就足够了吗?
先让我们简单回顾一下在PC机,一个程序的执行过程大概是怎样的。因为程序是在操作系统的管理下运行的,过程大概为:
操作系统----------> 启动代码(编译器自动加入,做一些堆栈、全局变量的初始化工作)-----------> main
然而在裸奔的单片机上,操作系统没有了,所以原来由操作系统和编译器作的事情,现在需要我们手工DIY了(如果交叉编译工具没有为我们做好这些事
情的话,因为我也不知道gcc现在有没有为stm32做好这一切,所以我暂时假定什么都得靠自己)。
2、C程序的典型内存布局
+-------------------------------+
| |
| 堆栈 |
| |
+ - - - - - - - - - - - - - - - +
| |
| |
| |
| |
| |
| |
| |
| |
| |
+ - - - - - - - - - - - - - - - +
| |
| 堆 |
| |
+-------------------------------+
| |
| 未初始化的数据 |
| .bss段 |
| |
+-------------------------------+
| |
| 初始化的数据 |
| .data段 |
| |
+-------------------------------+
| |
| 正文 |
| .text段 |
| .rodata段 |
| |
+-------------------------------+
上图中,正文对应的是可执行代码.text和常量表格数据等.rodata,.data对应初始化了的全局变量,编译后将位于可执行文件中,由启动代码负责加载
到数据区中(在单片机中这部分数据会存于flash中,需要有启动代码把这部分内容拷贝到sram中),.bss段是没有初始值的全局变量,由启动代码把这
部分内容全初始化为0;为了保证C程序的执行,还需要设置好程序运行时的堆栈区。
在有了这些基础知识后,除了main以外,我们还需要做些什么就比较清楚了:设置堆栈区,把编译好的内容放到单片机中正确的地方中去。
3、设置堆栈区和启动代码
Cortex-m3内核在地址0x0000 0000处存放一个向量表,向量表的第0个单元,也即地址0x0000 0000处存放的是堆栈顶的地址,Cortex-m3复位后即从该处
取出数据用以初始化MSP寄存器。向量表中的内容是32位的地址,这些地址是中断异常服务程序的入口地址,其中向量表的第一个单元,
即地址0x0000 0004处存放的是复位向量,也就是说Cortex-m3复位后,执行该向量(可理解为函数指针)指向的复位代码。看看代码吧:
__attribute__ ((section(".stackarea")))
static unsigned long pulStack[STACK_SIZE];
这一句定义了一个pulStack的数组,程序把这个数组作为了堆栈区。这条语句使用了__attribute__ ((section(".stackarea"))) 把数组定位在
了.stackarea这个段中。
typedef void (* pfnISR)(void);
__attribute__ ((section(".isr_vector")))
pfnISR VectorTable[] =
{
(pfnISR)((unsigned long)pulStack + sizeof(pulStack)), // The initial stack pointer
ResetISR, // The reset handler
NMIException,
HardFaultException
};
定义了一个数组VectorTable,作为向量表,定位于.isr_vector段中。通过链接脚本的控制这个表将放在正文区的最开始,正文区又将从flash的最开始
存放,这样这个向量表就会起到相当于存放在0x0000 0000开始的地址空间的效果。
向量表的第0个单元是((unsigned long)pulStack + sizeof(pulStack)),这是数组的最后一个元素,因为Cortex-m3的堆栈是向下增长的。
向量表的第1个单元是ResetISR,它指向复位处理的代码,也是整个程序的入口。本程序用它来实现启动代码的功能。
extern unsigned long _etext;
extern unsigned long _data;
extern unsigned long _edata;
extern unsigned long _bss;
extern unsigned long _ebss;
void ResetISR(void)
{
unsigned long *pulSrc, *pulDest;
//
// Copy the data segment initializers from flash to SRAM.
//
pulSrc = &_etext;
for(pulDest = &_data; pulDest < &_edata; )
{
*pulDest++ = *pulSrc++;
}
//
// Zero fill the bss segment.
//
for(pulDest = &_bss; pulDest < &_ebss; )
{
*pulDest++ = 0;
}
//
// Call the application's entry point.
//
main();
}
这段代码用到了通过连接器赋值的几个变量值。_etext的值为正文段结尾处的地址,这之后的flash空间是初始化的数据值,应该复制到sram中去,
_data、_edata的值分别为数据段的开始和结尾处的地址,这部分应该是sram的地址。
pulSrc = &_etext;
for(pulDest = &_data; pulDest < &_edata; )
{
*pulDest++ = *pulSrc++;
}
这部分代码就是将保存于flash中的初始化数据复制到sram中。
上面代码中的第二个循环是将.bss段清零。最后调用main进入到我们的主程序。
4、访问外围设备寄存器
Cortex-m3的外围设备寄存器位于线性的4GB地址空间中,所以定义指向该外围设备所处地址的指针即可访问了。
#define GPIOC_CRL (*((volatile unsigned int*)(0x40011000)))
#define GPIOC_BSRR (*((volatile unsigned int*)(0x40011010)))
#define GPIOC_BRR (*((volatile unsigned int*)(0x40011014)))
#define RCC_APB2ENR (*((volatile unsigned int*)(0x40021018)))
使用宏GPIOC_CRL等即可访问相应的寄存器。
5、链接
gcc编译C源程序文件后,得到目标文件,目标文件需要连接得到最后的可执行文件,程序才能执行。一般来说,目标文件包含
.text段: 可执行代码
.rodata段: 只读的数据,对应程序中的常量
.data段: 初始化的全局变量
.bss段: 未初始化的全局变量
连接器所作的工作简单的讲就是,把所有目标文件相应的段连接到一起,并把目标文件中的“变量地址”“函数地址”重定位至正确的地址空间;
比如,对于stm32来说向量表,.text和.rodata就应该放到从0x0800 0000开始的flash,.data,.bss和堆栈就应该定位至从0x2000 0000开始的sram中。
这些定位都可以通过链接脚本进行控制。
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 0x20000
SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 0x5000
}
这些语句说明了flash和sram开始的地址以及大小。
.text :
{
KEEP(*(.isr_vector .isr_vector.*))
*(.text .text.*)
*(.rodata .rodata*)
_etext = .;
} > FLASH
按.isr_vector, .text, .rodata的顺序排列正文段的内容;回忆前述VectorTable[]数组就被定为与.isr_vector段中,所以这段脚本就保证了向量表为
与正文区的最开端,将存放于0x0800 0000开始的位置了。但向量表不是应该从0x0000 0000开始吗?原来stm32可以通过boot0、boot1引脚的配置将
flash映射到0x0000 0000处。具体可参考stm32的数据手册。
_etext = .; 这条语句把计数器“.”的值赋给了变量_etext;“.”现在的值就为.text的尾部。
另,后面的.data、.bss、.stackarea部分可自行分析,原理一样。
四、编译程序
step1: arm-elf-gcc -mcpu=cortex-m3 -mthumb gpio_test.c -nostartfiles -T stm32f103VBT6.ld -o gpio_test.o
注意参数 -nostartfiles指示不要包含编译器自带的启动代码,-T stm32f103VBT6.ld表示使用stm32f103VBT6.ld这个链接脚本。
step2: arm-elf-ld -T stm32f103VBT6.ld -o gpio_test.out gpio_test.o
同样使用stm32f103VBT6.ld这个链接脚本。
step3: arm-elf-objcopy -Obinary gpio_test.out gpio_test.bin
从elf的文件格式中得到最终需要的gpio_test.bin二进制目标文件。
step4: 使用官方的flash下载demo程序将得到的gpio_test.bin通过usart1烧录至芯片。
/* filename: gpio_test.c */
#define GPIOC_CRL (*((volatile unsigned int*)(0x40011000)))
#define GPIOC_BSRR (*((volatile unsigned int*)(0x40011010)))
#define GPIOC_BRR (*((volatile unsigned int*)(0x40011014)))
#define RCC_APB2ENR (*((volatile unsigned int*)(0x40021018)))
#ifndef STACK_SIZE
#define STACK_SIZE 64
#endif
void ResetISR(void);
void NMIException(void);
void HardFaultException(void);
void delay(void);
typedef void (* pfnISR)(void); // Pointer to exception handle function
// mthomas: added section -> alignment thru linker-script
__attribute__ ((section(".stackarea")))
static unsigned long pulStack[STACK_SIZE];
__attribute__ ((section(".isr_vector")))
pfnISR VectorTable[] =
{
(pfnISR)((unsigned long)pulStack + sizeof(pulStack)), // The initial stack pointer
ResetISR, // The reset handler
NMIException,
HardFaultException
};
void delay(void)
{
unsigned int i;
for( i = 0; i < 0x3ffff; ++i)
asm("nop");
}
int main(void)
{
RCC_APB2ENR |= (1<<4);
GPIOC_CRL &= 0x0000FFFF;
GPIOC_CRL |= 0x33330000;
while(1){
GPIOC_BRR = (1<<4);
GPIOC_BSRR = (1<<7);
delay();
GPIOC_BRR = (1<<7);
GPIOC_BSRR = (1<<6);
delay();
GPIOC_BRR = (1<<6);
GPIOC_BSRR = (1<<5);
delay();
GPIOC_BRR = (1<<5);
GPIOC_BSRR = (1<<4);
delay();
}
}
//*****************************************************************************
//
// The following are constructs created by the linker, indicating where the
// the "data" and "bss" segments reside in memory. The initializers for the
// for the "data" segment resides immediately following the "text" segment.
//
//*****************************************************************************
extern unsigned long _etext;
extern unsigned long _data;
extern unsigned long _edata;
extern unsigned long _bss;
extern unsigned long _ebss;
void ResetISR(void)
{
unsigned long *pulSrc, *pulDest;
//
// Copy the data segment initializers from flash to SRAM.
//
pulSrc = &_etext;
for(pulDest = &_data; pulDest < &_edata; )
{
*pulDest++ = *pulSrc++;
}
//
// Zero fill the bss segment.
//
for(pulDest = &_bss; pulDest < &_ebss; )
{
*pulDest++ = 0;
}
//
// Call the application's entry point.
//
main();
}
void NMIException(void)
{
return;
}
void HardFaultException(void)
{
return;
}
/*************************************************/
/* filename: stm32f103VBT6.ld */
/* linkscript for STM32F103VBT6 microcontroller */
/* */
/* modified by XiaoFeng Li (ifree64) China */
/* */
/* */
/* E-Mail: leexiaofeng@gmail.com */
/* 2008-9-6 */
/*************************************************/
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 0x20000
SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 0x5000
}
/* Section Definitions */
SECTIONS
{
.text :
{
KEEP(*(.isr_vector .isr_vector.*))
*(.text .text.*)
*(.rodata .rodata*)
_etext = .;
} > FLASH
.data : AT (_etext)
{
_data = .;
*(.data .data.*)
. = ALIGN(4);
_edata = . ;
} > SRAM
/* .bss section which is used for uninitialized data */
.bss (NOLOAD) :
{
_bss = . ;
*(.bss .bss.*)
*(COMMON)
. = ALIGN(4);
_ebss = . ;
} > SRAM
.stackarea (NOLOAD) :
{
. = ALIGN(8);
*(.stackarea .stackarea.*)
. = ALIGN(8);
} > SRAM
. = ALIGN(4);
_end = . ;
}
step1: arm-elf-gcc -mcpu=cortex-m3 -mthumb gpio_test.c -nostartfiles -T stm32f103VBT6.ld -o gpio_test.o
step2: arm-elf-ld -T stm32f103VBT6.ld -o gpio_test.out gpio_test.o
step3: arm-elf-objcopy -Obinary gpio_test.out gpio_test.bin
step4: download gpio_test.bin to stm32, ok!