由于我们协会准备暑假组野队去打电赛,为了给学弟学妹们提供可以借鉴的资料,所以由我提前踩点,在这里先准备一些TI公司的芯片学习资料,这里是准备的TM4C的,至于MSP430的以后再说。
吐槽
不过查阅大量资料后发现,TM4C实在是不适合DIY,芯片价格远高于MSP和STM32,这使得最有性价比的还是使用TI官方的开发板,毕竟TM4C123GH6PM芯片要90块,但是完整开发板只要150块,而TM4C129更是上百块的芯片,开发板却只有300块,实在是逆天,不知道是不是卖芯片的专门买TI的开发板然后吹下来所以才这么贵。经过我查阅发现,TM4C实际上非常适合用于电机等领域的控制,而且TI很多设计实际上比STM32更优异更实用一点,但是也许就是价格的原因导致很少有基于TI的MCU的电调之类的设备的资料,我也因此放弃基于MSP和TM4C的电调设计,转而使用STM32设计。
而且在同公司的产品比较之下,MSP430价格显得十分亲民,但是TM4C相对来说性能上更优异一点,这一点就显得很令人为难,以上算是我的一点小吐槽。
准备
这篇文章主要是学习一个单片机的第一步,点亮一颗LED,也就是嵌入式的“Hello World”。
首先就是要拥有对应的开发环境,本人是基于Keil5进行开发的,不用CCS主要是实在操作不来哪个编译器,但是建议各位还是下一个CCS,以便于你们能快速获取官方例程和SDK。
Keil5开发TM4C需要准备:
1、 对应的芯片包
2、 对应的驱动库
芯片包在站内或者KEIL-ARM的官网上就能找到,这里不多赘述。
驱动库就是TM4C的底层驱动,毕竟你也不想亲自操作寄存器来实现外设功能吧。
驱动库可以打开官网,注册账号,如实填写你的学校和姓名以及邮箱,在搜索里搜你要的芯片,进入芯片介绍页面后找到软件开发,然后找到SDK软件开发套件(MSP430也一样,不过C2000系列的就不一样了,一般是找到C/C++的字眼),下载选项里选第一个(即全系芯片的库),然后点击超链接(蓝色下划线的),然后你就会跳到美国出口许可证明的页面里,在页面把军事用途改为文明用途(?)也就是Military改为Civilized(你可以试试挑战一下美国底线/斜眼笑),由于这个东西一般没什么技术含量,你完成出口许可提交后很快就会给你发送下载链接,下载好SDK后安装在一个你知道的位置里,等待安装完成,SDK安装好后,找到安装路径,把整个文件夹搬到你的Keil工程里去。一般驱动库就在driverlib文件夹里,剩下的关于新建工程的内容,你们可以去其他TM4C的教程里找找看,这里就是提供一个下载最新的官方库函数文件的方法。
开始
在STM32的LED实验中,不论是HAL库还是标准库,都需要对GPIO的引脚进行初始化,也就是调整GPIO的寄存器,使之调整到我们需要的位置上去。
对于TI的TM4C来说亦是如此,不过TI在初始化上还是存在一些差异的,顺序上基本一致:
1、 使能GPIO时钟
2、 设置GPIO模式
3、 GPIO读写
在第一步里,我们使用SysCtlPeripheralEnable(uint32_t ui32Peripheral)的函数,后续串口等也是使用这个函数来设置外设时钟,当我们需要使能GPIOF时钟时,就写入SYSCTL_PERIPH_GPIOF,如果需要使能UART,那就写SYSCTL_PERIPH_UART0,相信你已经发现,想使能对应外设,SYSCTL_PERIPH_加上对应外设名称即可,后面的文章我们再提。
在STM32里,设置完GPIO时钟后,需要设置GPIO速度(50MHz High_Speed),GPIO模式(IN,OUT_PP,OD等等),然后将结构体写完,再由GPIO_Init函数执行结构体的数据。
但是在TM4C中,我们需要直接寻找对应模式的GPIO初始化函数,例如GPIOPinTypeGPIOOutput(...)函数就是用来设置引脚为输出的,若是后续为GPIOInput,那么就是设置引脚为输入的,甚至是对应的外设引脚也是直接使用对应的函数来进行引脚初始化的,比如GPIOPinTypeUART(...)就是直接把引脚设置成串口的引脚。
从底层上来说,就是通过这个函数来直接把引脚挂载到对应的外设上去:如果只是做一个通用的输出引脚,就挂到GPIO上去;如果是做I2C,就挂到I2C上去。这与STM32略有不同,STM32是在使能外设后,先使能外设的基本参数到外设寄存器里,比如串口外设就先设置波特率,输入输出模式等等,再使能对应的GPIO引脚,然后再将外设和GPIO连接起来,由外设操控GPIO,从而操控引脚。
在GPIOPinTypeGPIOOutput函数中,我们需要填入对应GPIO寄存器地址,比如GPIO_PORTF_BASE,如果是GPIOE那就填GPIO_PORTE_BASE,其他外设也是如此,比如UART0,那就是UART0_Base(只是因为GPIO的Port比较多,所以对应地址多,需要一个个使能,总不能为了一个引脚把所有GPIO全都使能吧/笑),然后填入对应引脚,GPIO_PIN_0到GPIO_PIN_7(是的,TM4C的GPIO的分配是8个一组,不是STM32的16个一组,不过这并不代表TM4C就比STM32逊色),然后我们就完成了GPIO引脚的使能,是的,两个函数就完成了使能。
myGPIO.c
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include "inc/hw_ints.h"
#include "inc/hw_memmap.h"
#include "driverlib/gpio.h"
#include "driverlib/pin_map.h"
#include "driverlib/sysctl.h"
#include "fpu.h"
#include "uart.h"
#include "usb.h"
#include "i2c.h"
#include "myGPIO.h"
void GPIO_Init(void)
{
SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOF);
GPIOPinTypeGPIOOutput(GPIO_PORTF_BASE,GPIO_PIN_1);
GPIOPinWrite(GPIO_PORTF_BASE,GPIO_PIN_1,1);
}
这样之后,TM4C的PF1就会在初始化完成之后输出为高。
所以说TM4C的库函数相对于STM32的标准库设置起来更简洁一点,但是比全自动的CUBEMX还是稍微逊色了那么一点(笑
以为结束了?
STM32的标准库与HAL库必定会在main函数里有一个类似于Sysclock字眼的函数,那些函数的作用就是使能RCC时钟树,是使MCU进入工作的必要函数,在TI的芯片中,我们需要使能RCC时钟树,就需要设置时钟函数SysCtlClockSet(...),本质上,这是在设置TM4C的RCC时钟寄存器,以使得MCU进入到对应的工作状态,获取对应的时钟信号。我们写入SysCtlClockSet(SYSCTL_SYSDIV_1 | SYSCTL_XTAL_16MHZ | SYSCTL_USE_OSC | SYSCTL_OSC_MAIN),就可以完成使能时钟。从或运算我们可以看出,这个函数只对一个寄存器进行赋值,并且这个寄存器不同位对应了包括分频设置,时钟源,外部时钟频率等信息,比如在本例程时钟使能中,就包括了1分频(SYSCTL_SYSDIV_1,即80MHz工作,2分频就是40MHz,DIV就是division),16MHz晶振输入(SYSCTL_XTAL_16MHZ,也有1M的,8M的,25M的,XTAL就是晶振的意思),启用外部时钟(SYSCTL_USE_OSC,OSC对应三个单词OUTSIDE SYSTEM CLOCK,即外部的 系统的 时钟 ),启用外部主时钟(SYSCTL_OSC_MAIN),如此一来,TM4C就会在我们的设置下进行工作。
void SYSCLOCK_Init(void)
{
FPUDisable();
FPULazyStackingEnable();
SysCtlClockSet(SYSCTL_SYSDIV_1 | SYSCTL_XTAL_16MHZ | SYSCTL_USE_OSC | SYSCTL_OSC_MAIN);
}
至于FPU就以后再说,你们先写着吧。
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include "inc/hw_ints.h"
#include "inc/hw_memmap.h"
#include "driverlib/gpio.h"
#include "driverlib/pin_map.h"
#include "driverlib/sysctl.h"
#include "fpu.h"
#include "uart.h"
#include "usb.h"
#include "i2c.h"
#include "myGPIO.h"
void SYSCLOCK_Init(void);
int main(void)
{
//系统配置
//配置系统时钟
SYSCLOCK_Init();
GPIO_Init();
SysCtlPeripheralEnable(SYSCTL_PERIPH_UART0);
while(1)
{
GPIOPinWrite(GPIO_PORTF_BASE, GPIO_PIN_1, !GPIO_PIN_1);//置低位点亮
}
return 0;
}
void SYSCLOCK_Init(void)
{
FPUDisable();
FPULazyStackingEnable();
SysCtlClockSet(SYSCTL_SYSDIV_1 | SYSCTL_XTAL_16MHZ | SYSCTL_USE_OSC | SYSCTL_OSC_MAIN);
}
这是main.c里的内容,仅供参考。
如此一来,默认出高的PF1在非运算下就会倒置使得PF1被置低。
让我们整点深入的
那么显然我们还不满足于此,因此我们试着让LED灯闪烁起来。在STM32中,不论标准库还是HAL库,我们都会使用系统滴答定时器(SysTick之类的字眼)来做延时就能达到比较高精度的毫秒级延时,但是在TM4C中中,我们就需要注意一些不同之处了。
TM4C初级的延时函数是SysCtlDelay(uint32_t ui32Count),count即周期计数,因此其受限于系统时钟的频率,要是系统时钟给40M,那么你数40M下就是1s,但在80M系统时钟下就是半秒。
显然这样不方便,那么我们需要进行进一步的改进。
SysCtlClockGet()是一个有返回值的函数,这个函数会获取TM4C设定的时钟频率,那么我们对此基于主时钟频率的计算在SysCtlDelay(uint32_t ui32Count)函数中就是成比例的,比如对Get函数的返回值除以10就是0.1s(80MHz系统时钟,1s数80M下,若设置位数80M/10=8M下,那么就是0.1s,而80M系统时钟的信息是直接源于Get函数的),这样我们就完成了初级的延时函数的初始化。
不过这仅限于TM4C工作在40MHz以下,否则延时长度就会漂移。
GPIOPinWrite(GPIO_PORTF_BASE, GPIO_PIN_1, !GPIO_PIN_1);//置低位点亮
SysCtlDelay(SysCtlClockGet() / 10); //延时0.1s
GPIOPinWrite(GPIO_PORTF_BASE, GPIO_PIN_1, GPIO_PIN_1); //置高位熄灭
SysCtlDelay(SysCtlClockGet() / 10); //延时0.1s
这……这对吗?
刚刚提到,40MHz下,SysCtlDelay(uint32_t ui32Count)函数会出现漂移,而TM4C作为Crotex-M4的板子,一般都是用于高级控制,尤其是类似于RTOS等情况,对延时精准的敏感性较高。所以我们需要一种更精确的延时函数。
那么在此之前,我们需要理解为什么TM4C的初级延时函数有问题。
TM4C的存储结构和STM32并不完全相同,Flash、ROM和多出来的自带的EEPROM所挂总线并不一致,Flash的总线最高速度无法超过40MHz,因此我们的Flash内的代码会被限速在40MHz,如果要进行80MHz的解算任务,那么Flash的调度速度就有些捉襟见肘了。
因此,TI提供了一种API,使得我们的代码能直接存储并运行在ROM上,提高运行速度。
所以这里我们需要引入一些头文件:
#include "driverlib/rom.h"
#include "driverlib/pin_map.h"
#include "driverlib/rom_map.h"
通过这几个头文件,代码就能在ROM上运行,我们现在再修改一下主函数的内容:
while(1)
{
GPIOPinWrite(GPIO_PORTF_BASE, GPIO_PIN_1, !GPIO_PIN_1);//置低位点亮
ROM_SysCtlDelay(SysCtlClockGet() / 10);
// SysCtlDelay(SysCtlClockGet() / 10); //延时0.1s
GPIOPinWrite(GPIO_PORTF_BASE, GPIO_PIN_1, GPIO_PIN_1);//置高位熄灭
ROM_SysCtlDelay(SysCtlClockGet() / 10);
// SysCtlDelay(SysCtlClockGet() / 10);//延时0.1s
}
当然,不止延时函数,几乎所有的非ROM函数都有对应的ROM函数,但是需要注意内存管理,别爆存了。
while(1)
{
ROM_GPIOPinWrite(GPIO_PORTF_BASE, GPIO_PIN_1, !GPIO_PIN_1);
// GPIOPinWrite(GPIO_PORTF_BASE, GPIO_PIN_1, !GPIO_PIN_1);//置低位点亮
ROM_SysCtlDelay(SysCtlClockGet() / 10);
// SysCtlDelay(SysCtlClockGet() / 10); //延时0.1s
ROM_GPIOPinWrite(GPIO_PORTF_BASE, GPIO_PIN_1, GPIO_PIN_1);
// GPIOPinWrite(GPIO_PORTF_BASE, GPIO_PIN_1, GPIO_PIN_1);//置高位熄灭
ROM_SysCtlDelay(SysCtlClockGet() / 10);
// SysCtlDelay(SysCtlClockGet() / 10);//延时0.1s
}
至此,一个闪烁灯的程序的ROM化就完成了。
下节我们讲UART。