分享

[原创]NUCLEO-F410RB 测评第一周: 运行尝试

 知芯世界 2020-10-28


我最近一段时间在使用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就可以控制亮和灭了。移植过来的程序是这样的:

  1. #include "stm32f4xx.h"

  2. int main(void)

  3. {

  4. RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; // enable GPIO port A clock

  5. GPIOA->MODER = GPIO_MODER_MODER5_0; // PA5 as general output (LED)

  6. RCC->APB1ENR |= RCC_APB1ENR_TIM6EN; // enable basic timer 6

  7. TIM6->PSC = 9999; // prescaler

  8. TIM6->ARR = 399; // auto reload value

  9. TIM6->CR1 = TIM_CR1_URS|TIM_CR1_CEN; // start counter

  10. while(1)

  11. {

  12. static char a=0;

  13. if(TIM6->SR & TIM_SR_UIF) // check if overflow

  14. {

  15. TIM6->SR &= ~TIM_SR_UIF; // clear flag

  16. if(a==0)

  17. {

  18. GPIOA->BSRRL = (1<<5);

  19. a=1;

  20. }

  21. else

  22. {

  23. GPIOA->BSRRH = (1<<5);

  24. a=0;

  25. }

  26. }

  27. }

  28. }

复制代码

在F072上,RCC中控制AHB时钟使能的只是AHBENR寄存器,到了F410上面变成AHB1ENR, 又新出了AHB1LPENR,这里就得改。TIMER6的操作假定是一样的,先试着编译吧。上面这个程序还需要额外的几个文件,包括startup code,都从别的地方挖来用。编译好以后,用STVP下载也成功了,但是,居然LED不亮
在F072上没有问题的,怎么会错呢? 我百思不得其解啊。竟然在这里被卡住了。
后来想还是找现成的例子来试吧,于是去下载STM32CubeF4这个近300M的zip包,从里面把GPIO_Toggle的例子扒出来,把所依赖的文件也搜罗出来,单独弄到一个目录下。编译的命令都写到.bat文件里

  1. arm-none-eabi-gcc -c -O2 -I. -mcpu=cortex-m4 -mthumb -DSTM32F410Rx main.c

  2. arm-none-eabi-gcc -c -O2 -I. -mcpu=cortex-m4 -mthumb -DSTM32F410Rx stm*.c

  3. arm-none-eabi-gcc -c -O2 -I. -mcpu=cortex-m4 -mthumb -DSTM32F410Rx system*.c

  4. arm-none-eabi-gcc -c -mcpu=cortex-m4 -mthumb start*.s

  5. 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

  6. arm-none-eabi-objcopy -Oihex blink.elf blink.hex

复制代码

这个测试例子顺利运行了,LED闪亮了。它的主程序也简单,删掉注释是这样

  1. int main(void)

  2. {

  3. HAL_Init();

  4. SystemClock_Config();

  5. __HAL_RCC_GPIOA_CLK_ENABLE();

  6. GPIO_InitStruct.Pin = GPIO_PIN_5;

  7. GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;

  8. GPIO_InitStruct.Pull = GPIO_PULLUP;

  9. GPIO_InitStruct.Speed = GPIO_SPEED_FAST;

  10. HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  11. while (1)

  12. {

  13. HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);

  14. HAL_Delay(100);

  15. }

  16. }

复制代码

当然,里面是用了HAL库。对于这样的小程序来说,用HAL无疑是做了很多的不必要的工作。我认为小程序直接操作寄存器就够了,记住几个关键寄存器的用法就可以写出来。回过头来看,既然HAL可以实现的功能,我自己写的又怎么不工作呢?于是使用替换法,排查下来发现问题出在
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
用HAL库书写同样功能的代码是
__HAL_RCC_GPIOA_CLK_ENABLE();
这有什么问题?不就是读-改写一个RCC外设寄存器么。追溯后面这个函数的定义,在stm32f4xx_hal_rcc.h里面有

  1. #define __HAL_RCC_GPIOA_CLK_ENABLE() do { \

  2. __IO uint32_t tmpreg; \

  3. SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\

  4. /* Delay after an RCC peripheral clock enabling */ \

  5. tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\

  6. UNUSED(tmpreg); \

  7. } while(0)

复制代码

奇怪了,为什么要这样去写呢?while(0)说明这个程序段并没有循环,再追溯 SET_BIT 宏的定义,就是我直接写的那样一个逻辑或操作。 实在想不出道理,但现实问题是我自己写的没有正确运行,只好反汇编比较了。
下面把代码差异保留到仅仅那一行C代码上,分别编译,使用objdump -S查看main()的内容。我直接写出来是

  1. 80001dc: 4b16 ldr r3, [pc, #88] ; (8000238 <main+0x5c>)

  2. 80001de: 4a17 ldr r2, [pc, #92] ; (800023c <main+0x60>)

  3. 80001e0: 4917 ldr r1, [pc, #92] ; (8000240 <main+0x64>)

  4. 80001e2: b4f0 push {r4, r5, r6, r7}

  5. 80001e4: 6b1e ldr r6, [r3, #48] ; 0x30

  6. 80001e6: 4c17 ldr r4, [pc, #92] ; (8000244 <main+0x68>)

  7. 80001e8: f046 0601 orr.w r6, r6, #1

  8. 80001ec: f44f 6580 mov.w r5, #1024 ; 0x400

  9. 80001f0: f64f 70ff movw r0, #65535 ; 0xffff

  10. 80001f4: 631e str r6, [r3, #48] ; 0x30

  11. 80001f6: 6025 str r5, [r4, #0]

  12. 80001f8: 6160 str r0, [r4, #20]

  13. 80001fa: 6c1f ldr r7, [r3, #64] ; 0x40

  14. 80001fc: f242 760f movw r6, #9999 ; 0x270f

  15. 8000200: f240 158f movw r5, #399 ; 0x18f

复制代码

使用了HAL库书写的结果是

  1. 80001dc: 4b19 ldr r3, [pc, #100] ; (8000244 <main+0x68>)

  2. 80001de: 4a1a ldr r2, [pc, #104] ; (8000248 <main+0x6c>)

  3. 80001e0: 6b18 ldr r0, [r3, #48] ; 0x30

  4. 80001e2: 491a ldr r1, [pc, #104] ; (800024c <main+0x70>)

  5. 80001e4: b4f0 push {r4, r5, r6, r7}

  6. 80001e6: f040 0001 orr.w r0, r0, #1

  7. 80001ea: 6318 str r0, [r3, #48] ; 0x30

  8. 80001ec: 6b18 ldr r0, [r3, #48] ; 0x30

  9. 80001ee: 4c18 ldr r4, [pc, #96] ; (8000250 <main+0x74>)

  10. 80001f0: b082 sub sp, #8

  11. 80001f2: f000 0001 and.w r0, r0, #1

  12. 80001f6: 9001 str r0, [sp, #4]

  13. 80001f8: f44f 6580 mov.w r5, #1024 ; 0x400

  14. 80001fc: f64f 70ff movw r0, #65535 ; 0xffff

  15. 8000200: 9e01 ldr r6, [sp, #4]

  16. 8000202: 6025 str r5, [r4, #0]

  17. 8000204: 6160 str r0, [r4, #20]

  18. 8000206: 6c1f ldr r7, [r3, #64] ; 0x40

  19. 8000208: f242 760f movw r6, #9999 ; 0x270f

  20. 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运行太快了么?

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约