[原创]NUCLEO-F410RB 测评第一周: 运行尝试
我最近一段时间在使用STM32F0,所以算是接触并稍微了解了STM32系列。拿到NUCLEO-F410RB,板子外观几乎和自己的NUCLEO-F091RC是一样的。不过学习了一下手册,就知道核心的差别大着呢。F091, F072都是ARM Cortex-M0的,核心是单一总线;F410是ARM Cortex-M4,核心有三条总线分别用于指令,数据和外设,何况还有Flash内存加速器,所以就算是同样的时钟下执行同样的指令,F410效率对比F0系列的优势也是明显的。指令集的对比就不用多说了,我看了一晚上M4编程手册,指令真是眼花缭乱!
好在貌似M4系和M0系的外设接口有很多是兼容的,代码挪过来编译应该问题不大。于是我尝试把F072做的最小测试程序移植过来跑一下,就让LED闪亮吧:在NUCLEO上绿色的LED是接在PA5口上的,操作GPIO就可以控制亮和灭了。移植过来的程序是这样的:
#include "stm32f4xx.h"
int main(void)
{
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; // enable GPIO port A clock
GPIOA->MODER = GPIO_MODER_MODER5_0; // PA5 as general output (LED)
RCC->APB1ENR |= RCC_APB1ENR_TIM6EN; // enable basic timer 6
TIM6->PSC = 9999; // prescaler
TIM6->ARR = 399; // auto reload value
TIM6->CR1 = TIM_CR1_URS|TIM_CR1_CEN; // start counter
while(1)
{
static char a=0;
if(TIM6->SR & TIM_SR_UIF) // check if overflow
{
TIM6->SR &= ~TIM_SR_UIF; // clear flag
if(a==0)
{
GPIOA->BSRRL = (1<<5);
a=1;
}
else
{
GPIOA->BSRRH = (1<<5);
a=0;
}
}
}
}
复制代码
在F072上,RCC中控制AHB时钟使能的只是AHBENR寄存器,到了F410上面变成AHB1ENR, 又新出了AHB1LPENR,这里就得改。TIMER6的操作假定是一样的,先试着编译吧。上面这个程序还需要额外的几个文件,包括startup code,都从别的地方挖来用。编译好以后,用STVP下载也成功了,但是,居然LED不亮
在F072上没有问题的,怎么会错呢? 我百思不得其解啊。竟然在这里被卡住了。
后来想还是找现成的例子来试吧,于是去下载STM32CubeF4这个近300M的zip包,从里面把GPIO_Toggle的例子扒出来,把所依赖的文件也搜罗出来,单独弄到一个目录下。编译的命令都写到.bat文件里
arm-none-eabi-gcc -c -O2 -I. -mcpu=cortex-m4 -mthumb -DSTM32F410Rx main.c
arm-none-eabi-gcc -c -O2 -I. -mcpu=cortex-m4 -mthumb -DSTM32F410Rx stm*.c
arm-none-eabi-gcc -c -O2 -I. -mcpu=cortex-m4 -mthumb -DSTM32F410Rx system*.c
arm-none-eabi-gcc -c -mcpu=cortex-m4 -mthumb start*.s
arm-none-eabi-ld *.o -Le:\arm-2014q3\arm-none-eabi\lib\armv7e-m -Le:\arm-2014q3\lib\gcc\arm-none-eabi\4.8.4\armv7e-m -T STM32F410RBTx_FLASH.ld -o blink.elf
arm-none-eabi-objcopy -Oihex blink.elf blink.hex
复制代码
这个测试例子顺利运行了,LED闪亮了。它的主程序也简单,删掉注释是这样
int main(void)
{
HAL_Init();
SystemClock_Config();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FAST;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
while (1)
{
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
HAL_Delay(100);
}
}
复制代码
当然,里面是用了HAL库。对于这样的小程序来说,用HAL无疑是做了很多的不必要的工作。我认为小程序直接操作寄存器就够了,记住几个关键寄存器的用法就可以写出来。回过头来看,既然HAL可以实现的功能,我自己写的又怎么不工作呢?于是使用替换法,排查下来发现问题出在
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
用HAL库书写同样功能的代码是
__HAL_RCC_GPIOA_CLK_ENABLE();
这有什么问题?不就是读-改写一个RCC外设寄存器么。追溯后面这个函数的定义,在stm32f4xx_hal_rcc.h里面有
#define __HAL_RCC_GPIOA_CLK_ENABLE() do { \
__IO uint32_t tmpreg; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\
UNUSED(tmpreg); \
} while(0)
复制代码
奇怪了,为什么要这样去写呢?while(0)说明这个程序段并没有循环,再追溯 SET_BIT 宏的定义,就是我直接写的那样一个逻辑或操作。 实在想不出道理,但现实问题是我自己写的没有正确运行,只好反汇编比较了。
下面把代码差异保留到仅仅那一行C代码上,分别编译,使用objdump -S查看main()的内容。我直接写出来是
80001dc: 4b16 ldr r3, [pc, #88] ; (8000238 <main+0x5c>)
80001de: 4a17 ldr r2, [pc, #92] ; (800023c <main+0x60>)
80001e0: 4917 ldr r1, [pc, #92] ; (8000240 <main+0x64>)
80001e2: b4f0 push {r4, r5, r6, r7}
80001e4: 6b1e ldr r6, [r3, #48] ; 0x30
80001e6: 4c17 ldr r4, [pc, #92] ; (8000244 <main+0x68>)
80001e8: f046 0601 orr.w r6, r6, #1
80001ec: f44f 6580 mov.w r5, #1024 ; 0x400
80001f0: f64f 70ff movw r0, #65535 ; 0xffff
80001f4: 631e str r6, [r3, #48] ; 0x30
80001f6: 6025 str r5, [r4, #0]
80001f8: 6160 str r0, [r4, #20]
80001fa: 6c1f ldr r7, [r3, #64] ; 0x40
80001fc: f242 760f movw r6, #9999 ; 0x270f
8000200: f240 158f movw r5, #399 ; 0x18f
复制代码
使用了HAL库书写的结果是
80001dc: 4b19 ldr r3, [pc, #100] ; (8000244 <main+0x68>)
80001de: 4a1a ldr r2, [pc, #104] ; (8000248 <main+0x6c>)
80001e0: 6b18 ldr r0, [r3, #48] ; 0x30
80001e2: 491a ldr r1, [pc, #104] ; (800024c <main+0x70>)
80001e4: b4f0 push {r4, r5, r6, r7}
80001e6: f040 0001 orr.w r0, r0, #1
80001ea: 6318 str r0, [r3, #48] ; 0x30
80001ec: 6b18 ldr r0, [r3, #48] ; 0x30
80001ee: 4c18 ldr r4, [pc, #96] ; (8000250 <main+0x74>)
80001f0: b082 sub sp, #8
80001f2: f000 0001 and.w r0, r0, #1
80001f6: 9001 str r0, [sp, #4]
80001f8: f44f 6580 mov.w r5, #1024 ; 0x400
80001fc: f64f 70ff movw r0, #65535 ; 0xffff
8000200: 9e01 ldr r6, [sp, #4]
8000202: 6025 str r5, [r4, #0]
8000204: 6160 str r0, [r4, #20]
8000206: 6c1f ldr r7, [r3, #64] ; 0x40
8000208: f242 760f movw r6, #9999 ; 0x270f
800020c: f240 158f movw r5, #399 ; 0x18f
复制代码
机器代码有区别是毋庸置疑的,用了HAL的这段代码要长一些,将 AHB1ENR 改写之后又多读了一次。读能影响寄存器的结果?不可能的事。我写的代码编译结果也没有问题,那究竟为什么LED没有亮?
假如我手工写汇编来写这个程序的话,写出来不会是这个样子的。一条条读就能看出来,编译器似乎对指令顺序进行了调整优化——也许是优化了流水线使M4上运行更快。我注意到这两处连续的写操作:
80001f4: 631e str r6, [r3, #48] ; 0x30
80001f6: 6025 str r5, [r4, #0]
前一个的目标地址是 RCC 的 AHB1ENR, 后一个不用说应该是 GPIOA 的 MODER. 会不会是GPIOA还没有被使能,导致紧接着的 MODER 寄存器操作无效了?我很怀疑这一点。于是,在C程序中,写 AHB1ENR 之后插入一个 NOP 指令,也就是 __NOP(); 结果,果然问题就绕过去了。
其它的,比如改变编译优化选项为 -O 而不用 -O2, 结果代码中没有那两条连续的STR指令,LED也同样闪烁了。这,是因为Cortex-M4运行太快了么?