在STM32里面,其实是可以把一个数组转换成函数指针来执行的。
以STM32F103C8单片机为例,首先我们新建一个test.s文件,用汇编语言编写一个GPIO模拟产生18MHz SPI信号的函数。
函数原理:CS片选为PA1,CLK时钟为PA2,DATA数据为PA3。通过写GPIOA->BSRR寄存器控制I/O口输出,GPIOA->BSRR寄存器的低16位决定PA0~15是否输出高电平,高16位决定PA0~15是否输出低电平。mov指令给CPU寄存器(指r0~r15这些寄存器)的低16位赋值,同时将高16位清零。movt指令只给CPU寄存器的高16位赋值。str指令负责给外设寄存器(如GPIOA->BSRR这样的寄存器)赋值,外设寄存器的地址和值由CPU寄存器提供。
area |.text|, code, readonly
export io_test
align ; 保证函数的地址能被4整除
io_test proc
push {r0-r12, lr} ; 保存寄存器的原有内容, 以及函数的返回地址 (lr就是r14寄存器)
; 以免篡改原函数里面用寄存器存储的局部变量
mov r0, #0x00 ; r0寄存器存放CLK=0, DATA=0的操作
movt r0, #0x0c
mov r1, #0x08 ; r1寄存器存放CLK=0, DATA=1的操作
movt r1, #0x04
mov r2, #0x00 ; r2寄存器存放CS=0, CLK=0, DATA=0的操作
movt r2, #0x0e
mov r4, #0x04 ; r4寄存器存放CLK=1的操作
mov r5, #0x02 ; r5寄存器存放CS=1, CLK=0的操作
movt r5, #0x04
mov r6, #0x0810 ; r6寄存器保存GPIOA->BSRR寄存器的地址
movt r6, #0x4001
; 产生SPI时序
str r2, [r6]
str r4, [r6]
str r1, [r6]
str r4, [r6]
str r0, [r6]
str r4, [r6]
str r0, [r6]
str r4, [r6]
str r1, [r6]
str r4, [r6]
str r0, [r6]
str r4, [r6]
str r5, [r6]
pop {r0-r12, pc} ; 恢复寄存器的原有内容并返回 (pc就是r15寄存器)
endp
end
(写好这个汇编函数后,若要在C语言里面调用,先声明一下void io_test(void);,然后执行io_test();。不声明直接调用也可以,只是编译器会有警告)
编译Keil工程,打开Listings\test.lst文件,可以看到汇编程序的编译结果:
ARM Macro Assembler Page 1
1 00000000 area |.text|, code, readonly
2 00000000 export io_test
3 00000000 align ; 保证函数的地址能?
?整除
4 00000000 io_test proc
5 00000000 E92D 5FFF push {r0-r12, lr} ; 保存寄存器的原有
内容, 以及函数的返?
氐刂?(lr就是r14寄?
嫫?
6 00000004 ; 以免篡改原函数里面用寄存器存储的局部变量
7 00000004
8 00000004 F04F 0000 mov r0, #0x00 ; r0寄存器存放CLK=0
, DATA=0的操作
9 00000008 F2C0 000C movt r0, #0x0c
10 0000000C F04F 0108 mov r1, #0x08 ; r1寄存器存放CLK=0
, DATA=1的操作
11 00000010 F2C0 0104 movt r1, #0x04
12 00000014 F04F 0200 mov r2, #0x00 ; r2寄存器存放CS=0,
CLK=0, DATA=0的操?
?
13 00000018 F2C0 020E movt r2, #0x0e
14 0000001C F04F 0404 mov r4, #0x04 ; r4寄存器存放CLK=1
的操作
15 00000020 F04F 0502 mov r5, #0x02 ; r5寄存器存放CS=1,
CLK=0的操作
16 00000024 F2C0 0504 movt r5, #0x04
17 00000028 F44F 6601 mov r6, #0x0810 ; r6寄存器保存GPIOA
->BSRR寄存器的地址
18 0000002C F2C4 0601 movt r6, #0x4001
19 00000030
20 00000030 ; 产生SPI时序
21 00000030 6032 str r2, [r6]
22 00000032 6034 str r4, [r6]
23 00000034 6031 str r1, [r6]
24 00000036 6034 str r4, [r6]
25 00000038 6030 str r0, [r6]
26 0000003A 6034 str r4, [r6]
27 0000003C 6030 str r0, [r6]
28 0000003E 6034 str r4, [r6]
29 00000040 6031 str r1, [r6]
30 00000042 6034 str r4, [r6]
31 00000044 6030 str r0, [r6]
32 00000046 6034 str r4, [r6]
33 00000048 6035 str r5, [r6]
34 0000004A
35 0000004A E8BD 9FFF pop {r0-r12, pc} ; 恢复寄存器的原有
内容并返回 (pc就是r
15寄存器)
36 0000004E endp
37 0000004E end
Command Line: --debug --xref --diag_suppress=9931 --cpu=Cortex-M3 --apcs=interw
ork --depend=.\objects\test.d -o.\objects\test.o -I.\RTE\_Target_1 -IC:\Keil_v5
\ARM\PACK\Keil\STM32F1xx_DFP\2.3.0\Device\Include -IC:\Keil_v5\ARM\CMSIS\Includ
e --predefine="__UVISION_VERSION SETA 527" --predefine="STM32F10X_MD SETA 1" --
list=.\listings\test.lst test.s
ARM Macro Assembler Page 1 Alphabetic symbol ordering
Relocatable symbols
.text 00000000
Symbol: .text
Definitions
At line 1 in file test.s
Uses
None
Comment: .text unused
io_test 00000000
Symbol: io_test
Definitions
At line 4 in file test.s
Uses
At line 2 in file test.s
Comment: io_test used once
2 symbols
334 symbols in table
把编译出来的机器代码放到C语言数组里面,转换成函数指针并执行:(系统时钟和串口的初始化代码已省略)
#include <stdio.h>
#include <stm32f1xx.h>
#include "common.h"
typedef void (*Runnable)(void);
uint16_t buffer[] = {
0xe92d, 0x5fff, // push {r0-r12, lr}
0xf04f, 0x0000, // mov r0, #0x00
0xf2c0, 0x000c, // movt r0, #0x0c
0xf04f, 0x0108, // mov r1, #0x08
0xf2c0, 0x0104, // movt r1, #0x04
0xf04f, 0x0200, // mov r2, #0x00
0xf2c0, 0x020e, // movt r2, #0x0e
0xf04f, 0x0404, // mov r4, #0x04
0xf04f, 0x0502, // mov r5, #0x02
0xf2c0, 0x0504, // movt r5, #0x04
0xf44f, 0x6601, // mov r6, #0x0810
0xf2c4, 0x0601, // movt r6, #0x4001
0x6032, // str r2, [r6]
0x6034, // str r4, [r6]
0x6031, // str r1, [r6]
0x6034, // str r4, [r6]
0x6030, // str r0, [r6]
0x6034, // str r4, [r6]
0x6030, // str r0, [r6]
0x6034, // str r4, [r6]
0x6031, // str r1, [r6]
0x6034, // str r4, [r6]
0x6030, // str r0, [r6]
0x6034, // str r4, [r6]
0x6035, // str r5, [r6]
0xe8bd, 0x9fff // pop {r0-r12, pc}
};
static void io_init(void)
{
GPIO_InitTypeDef gpio;
__HAL_RCC_GPIOA_CLK_ENABLE();
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET);
gpio.Mode = GPIO_MODE_OUTPUT_PP;
gpio.Pin = GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3;
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &gpio);
}
int main(void)
{
Runnable run;
HAL_Init();
clock_init();
usart_init(115200);
printf("STM32F103C8 FuncTest\n");
printf("SystemCoreClock=%u\n", SystemCoreClock);
io_init();
run = (Runnable)((uintptr_t)buffer + 1);
while (1)
{
run();
HAL_Delay(1);
}
}
注意,buffer的地址要加1后才能赋给函数指针run。不能把(uintptr_t)buffer + 1写成buffer + 1或者&buffer[1],否则加的是sizeof(uint16_t)=2而不是1。
程序运行结果:
(1)buffer变量不加const,在SRAM里面执行函数:
(注意:全局变量加const表示把此变量存放到Flash里面,不加const表示把此变量存放到SRAM里面。对于局部变量,无论是否加const,都是存放到SRAM里面。)
(2)buffer变量加const,在Flash里面执行函数:
这说明,在Flash里面执行函数,比在SRAM里面执行快。
在Flash里面我们可以得到频率为18.2MHz,占空比为47.27%的SPI信号,但是在SRAM里面我们只能得到不规则频率的SPI信号。
从F103的datasheet里面可以看到,GPIO的最大翻转频率就是18MHz。
知道了怎样在数组里面执行代码,我们就有办法在STM32单片机上模拟操作系统运行应用程序。
我们把STM32函数代码以文件的形式保存到W25Q128或者SD卡中,命名为exe文件。然后用FatFs的f_open函数读取出来,放入SRAM的数组里面。用一个函数指针指向这个数组,最后执行。这样,我们就实现了STM32单片机动态运行保存在磁盘上的exe程序。
我们甚至还可以运行FreeRTOS操作系统,读取多个exe文件到SRAM里面,然后用xTaskCreate函数以创建任务的方式运行。
甚至同一个exe文件可以用xTaskCreate多次创建任务。