从一个简单问题说起:STM32的GPIO翻转速度(比如用来模拟时序)最快能多快?
写段代码测试一下:
- void test(void)
- {
- for(;;)
- {
- GPIOA->ODR = (1<<5);
- GPIOA->ODR = 0;
- GPIOA->ODR = (1<<5);
- GPIOA->ODR = 0;
- GPIOA->ODR = (1<<5);
- GPIOA->ODR = 0;
- GPIOA->ODR = (1<<5);
- GPIOA->ODR = 0;
- GPIOA->ODR = (1<<5);
- GPIOA->ODR = 0;
- GPIOA->ODR = (1<<5);
- GPIOA->ODR = 0;
- GPIOA->ODR = (1<<5);
- GPIOA->ODR = 0;
- GPIOA->ODR = (1<<5);
- GPIOA->ODR = 0;
- GPIOA->ODR = (1<<5);
- GPIOA->ODR = 0;
- GPIOA->ODR = (1<<5);
- GPIOA->ODR = 0;
- GPIOA->ODR = (1<<5);
- GPIOA->ODR = 0;
- GPIOA->ODR = (1<<5);
- GPIOA->ODR = 0;
- GPIOA->ODR = (1<<5);
- GPIOA->ODR = 0;
- GPIOA->ODR = (1<<5);
- GPIOA->ODR = 0;
- GPIOA->ODR = (1<<5);
- GPIOA->ODR = 0;
- GPIOA->ODR = (1<<5);
- GPIOA->ODR = 0;
- GPIOA->ODR = (1<<5);
- GPIOA->ODR = 0;
- GPIOA->ODR = (1<<5);
- GPIOA->ODR = 0;
- GPIOA->ODR = (1<<5);
- GPIOA->ODR = 0;
- GPIOA->ODR = (1<<5);
- GPIOA->ODR = 0;
- }
- }
复制代码
这段代码作用是让 PA5 在高低状态来回翻转,连续20次之后会有一次跳转间隔一下。经过编译器优化处理,变成了一连串的 STR 指令:
......
100001c0: 6159
str r1, [r3, #20]
100001c2: 615a
str r2, [r3, #20]
100001c4: 6159
str r1, [r3, #20]
100001c6: 615a
str r2, [r3, #20]
100001c8: 6159
str r1, [r3, #20]
100001ca: 615a
str r2, [r3, #20]
100001cc: 6159
str r1, [r3, #20]
......
这样子的,向同一个内存地址(GPIO ODR寄存器的地址)交替写不同的两个值,引起I/O口电平的变化。
如果每条 STR 指令的执行只需要1个机器周期的话(这是最理想的情况),以上程序可以让GPIO输出一半系统时钟频率的方波。实际上
是否每条 STR 指令时间是1个机器周期?
我曾经在玩8位的AVR的时候,用汇编写过一个ISP下载器的程序,模拟时序也用到了。我到现在也记得清楚,AVR手册上写明了,OUT 指令写I/O空间寄存器是1个机器周期,ST 指令写寄存器或者SRAM要2个机器周期。ARM 的情况呢?我不记得手册上有没有指令周期数的描述,不过可以先测试一下。
为了示波器测量方便,我先把系统时钟频率降到 200kHz. 然后……
这个很不错,除了连续20个脉冲之后因为有跳转指令多停顿了一下之外,每段高和低电平都持续了5us,也就是一条STR指令只用了5us,
对应正好一个机器周期。
不过先不要太激动,上面这个测试中,代码是在 STM32L452 的 SRAM2 当中执行的。现在我再把代码放在 SRAM1 中执行,结果:
是不是很奇怪,不仅变慢了,而且同样的指令执行时间还会变化。我可以猜测,如果在 Cortex-m0 上执行同样的代码,也近乎后面这个效果。
差异从何而来? 如果你对计算机怎么工作的有所了解的话,能想得到,CPU需要先取得指令,才能够执行指令。那么指令从何处取得呢?单片机上最常用的是 Flash ROM, 还可能是从 SRAM, 甚至从片外挂的 SRAM, SDRAM, NOR Flash 等等。CPU要读内存设备取得指令,就要访问
总线。上面的程序里,CPU执行 STR 指令写 GPIO 的寄存器,又是一次总线写操作。好,问题在这里:从总线读内存,与向总线写GPIO设备,这两个操作
能否同时进行?
然而单片机内部总线也是按照固定的时钟频率来操作的,一个总线 master 在每个总线周期最多只能发出一次请求。下图是 STM32F0x 系列(Cortex-m0)的内部结构
CPU 核心和外面的数据通道只有一条 System Bus, 因此它访问 SRAM/Flash 与访问 GPIO 在时间上必须错开。于是就会出现上面第二个画面——总线争用的结果。虽然我的实验并非在 Cortex-m0 上进行,原因是一样的。
那么第一幅画面,那个理想的结果是怎么来的? 看看我实验的这款 Cortex-m4 内部结构图:
注意了,注意了,Cortex-m4 出来有
三条总线,它们是可以并行访问的。分别叫做 I-Code bus, D-Code bus 和 System bus. 这样一来,CPU在从 System bus 写 AHB2 总线上的 GPIO 设备时,CPU还可以同时从 I-Code bus 读 SRAM2 中的程序指令。于是(考虑到流水线操作的结果)就可以达到每个机器周期翻转一次 GPIO 的效果。
上面我的两次实验,代码在 SRAM2 和 SRAM1 中执行效率不同,是因为 SRAM1 是从 System bus 访问的,和访问 GPIO 设备产生了总线争用的问题。倘若把代码放在 Flash 中执行呢?也可能达到和 SRAM2 中执行(从I-Code bus访问)同样的效率,不过有一些条件,因为 Flash 的速度没有 SRAM 快,在CPU频率高的时候必须要插入等待,如果没有缓存(Cache)就会影响速度了。
再说 SRAM1 的问题,上面这个图里面,SRAM1 可是和 Cortex-m4 的三条总线都有连接的呀。我曾经就问过这个问题
https://bbs.eeworld.com.cn/forum.php?mod=viewthread&tid=508085&extra=,现在回答一下:这是按地址空间分的。在 0x20000000 以上的地址,Cortex-m4 (m3也是) 从 System bus 访问,而在 0x20000000 以下的地址,从 I-Code 和 D-Code bus 访问。倘若要提高 SRAM1 中代码的执行效率,需要启用地址重映射:
总结一下,Cortex-m0 CPU 只有一条总线(因此它属于冯·诺依曼结构,指令和数据统一寻址),就算是执行同样的机器指令程序,也跟有三条总线的 Cortex-m3/m4 (它们属于哈佛结构,指令和数据分开寻址)效率有所差异。为了在 Cortex-m3/m4 上充分发挥这个优势,注意尽量让程序在能从 I-Code bus 访问的存储器设备中执行。