预备知识:
(1)volatile关键字:
volatile定义的变量一般为无需开发者自己赋值,会自动改变的变量。
在普通的程序中,编译器都具有优化功能,为了避免浪费计算机资源,会将重复或无用的语句删除。
大家观察下面一段代码。
//code1:
double gpio_input = 0;
printf("The GPIO Input1 is %f", gpio_input);
/*
光照传感器开始读取光照数据,并赋值给gpio_input
*/
printf("The GPIO Input2 is %f", gpio_input);
//code2:
volatile double gpio_input = 0;
printf("The GPIO Input1 is %f", gpio_input);
/*
光照传感器开始读取光照数据,并赋值给gpio_input
*/
printf("The GPIO Input2 is %f", gpio_input);
我们将一个GPIO INPUT的值赋值给gpio_input,第一段代码使用double类型来定义变量,并且两次输出的结果均为0;而第二次使用volatile类型来定义变量,它的两次结果分别为0和真实的光照数值。
第一段代码:是因为编译器优化功能。编译器作为计算机的节耗小能手,不允许一个变量没有赋值而反复读取,因此它会自动过滤掉再次读取变量地址的数值的那段代码,直接从缓冲区中找到上次的值,而不是再一次根据变量地址访问主存器,进而找到主存中的变量值。而外设的数值改变一般都是存储器的数值变化,由于优化功能,光照传感器读取的数值无法被读取到,造成两次结果都是0。
[缓冲区的访问速率高于主存,但容量小于主存。因此,一般都是先从缓存中先查找变量,如果找不到,就一边从主存中查找,一边将查找的数据调至缓存,供下一次使用]
第二段代码:使用volatile关键字就可以告诉编译器gpio_input这个变量他自己会变,不要把它当成坏人,直接抹杀掉,因此下一次的读取是从主存中读取的值,即真实的外设读取的数值。
(2)HWREG(x)函数:
把x转为一个指向x的volatile unsigned long指针,然后从中取值
(3)当GPIO引脚输出1时,需要设置引脚值为0;反之,为1。
原因:引脚输出的电压都来自开发板的板供电压,点亮LED所需的电压最大为120mA,电流均来自芯片内部,如果很多LED灯,那么就等同于电压的叠加,此时通过芯片的电流很大,对芯片的伤害大,因此反过来,更有利于保护芯片。
切入主题
CC3200的GPIO引脚和其他arm核的处理器外设使用是一样的步骤。
step1:设置时钟
CC3200简单的函数调用对想要了解开发板的初学者来说是极大地便利了,但是作为想要进一步了解的“油头”青年,只能硬着头皮去分析功能的具体实现
MAP_PRCMPeripheralClkEnable(PRCM_GPIOA1, PRCM_RUN_MODE_CLK);
这句话的含义是。开启GPIOA第二组时钟(下标从0开始)。
翻开这个函数的具体定义:
#define HWREG(x) (*((volatile unsigned long *)(x)))
//外设寄存器地址放入内存,x为外设寄存器地址
void
PRCMPeripheralClkEnable(unsigned long ulPeripheral, unsigned long ulClkFlags)
{
if(ulPeripheral != PRCM_ADC)
{
HWREG(ARCM_BASE + PRCM_PeriphRegsList[ulPeripheral].ulClkReg) |= ulClkFlags;
//启用指定的外设时钟,对PRCM_ADC不做任何操作,同时启动指定的外设寄存器
//等同于 (*((volatile unsigned long *)(0x44025000 + PRCM_PeriphRegsList[ulPeripheral].ulClkReg)))
}
//如果是camera,为其重新定义时钟
if(ulPeripheral == PRCM_CAMERA)
{
HWREG(ARCM_BASE + APPS_RCM_O_CAMERA_CLK_GEN) = 0x0404;
//(*((volatile unsigned long *)(0x44025000 + 0x00000000)));
}
}
step2:设置引脚
上一步我们对GPIOA第二组的时钟进行了设置,由于GPIO引脚具有两个方向,同时每组内包含8个引脚,因此,我们需要
设置组内引脚及方向
MAP_PinTypeGPIO(PIN_64, PIN_MODE_0, false);
//引脚,引脚模式,开漏输出/推挽输出
MAP_GPIODirModeSet(GPIOA1_BASE, 0x2, GPIO_DIR_MODE_OUT);
//基地址,组内号,方向
以上的code不难看出这是设置输出引脚PIN_64,由于引脚复用,因此当对同一引脚使用不同功能时,需要设置不同的引脚模式值,具体参考引脚模式对应表。
同样,我们看函数的定义部分
void PinTypeGPIO(unsigned long ulPin,unsigned long ulPinMode,tBoolean bOpenDrain)
{
//设置标准开漏输出引脚
if(bOpenDrain)
{
PinConfigSet(ulPin, PIN_STRENGTH_2MA, PIN_TYPE_OD);
}
else
{
PinConfigSet(ulPin, PIN_STRENGTH_2MA, PIN_TYPE_STD);
}
PinModeSet(ulPin, ulPinMode);
//设置引脚模式
}
设置GPIO引脚类型设置
void PinConfigSet(unsigned long ulPin,unsigned long ulPinStrength,unsigned long ulPinType)
{
unsigned long ulPad;
ulPad = g_ulPinToPadMap[ulPin & 0x3F];
//将0x4402E144置1,允许输入。
//但是这个寄存器0x4402E144是干什么的不太清楚
HWREG(0x4402E144) &= ~((0x80 << ulPad) & (0x1E << 8));
//计算寄存器地址,参考寄存器地址手册
ulPad = ((ulPad << 2) + PAD_CONFIG_BASE);
//找到寄存器地址,并向寄存器中写入设置值,参考每个引脚不同的配置值对应不同的功能
HWREG(ulPad) = ((HWREG(ulPad) & ~(PAD_STRENGTH_MASK | PAD_TYPE_MASK)) |(ulPinStrength | ulPinType ));
}
}
GPIO方向设置
void
GPIODirModeSet(unsigned long ulPort, unsigned char ucPins,
unsigned long ulPinIO)
{
ASSERT(GPIOBaseValid(ulPort)); //判断GPIO基地址是否合法
ASSERT((ulPinIO == GPIO_DIR_MODE_IN) || (ulPinIO == GPIO_DIR_MODE_OUT)); //判断GPIO方向参数是否合法,输入为0,输出为1
HWREG(ulPort + GPIO_O_GPIO_DIR) = ((ulPinIO & 1) ?
(HWREG(ulPort + GPIO_O_GPIO_DIR) | ucPins) :
(HWREG(ulPort + GPIO_O_GPIO_DIR) & ~(ucPins)));
//HWREG(ulPort + GPIO_O_GPIO_DIR)-》拿到GPIO组地址,ucPins拿到组内地址
}
至此,我们完成了配置。
step3:寄存器编程
CC3200官方自带的GPIO使用例程为流水灯
首先是函数GPIO_IF_LedConfigure,此函数中的GPIO_IF_GetPortNPin函数为主要的设置函数,此函数是
根据给定的GPIO值计算出引脚的GPIO基地址值和组内引脚值
GPIO_IF_LedConfigure(LED1|LED2|LED3);//同时配置3个GPIO引脚
void GPIO_IF_GetPortNPin(unsigned char ucPin, unsigned int *puiGPIOPort, unsigned char *pucGPIOPin)
{
*pucGPIOPin = 1 << (ucPin % 8); //组内序号从0开始,按此计算得到组内位权
*puiGPIOPort = (ucPin / 8); //每组有8个引脚,计算得出所属的GPIO组
*puiGPIOPort = ulReg[*puiGPIOPort]; //取GPIO基地址值
}
拿到了引脚的GPIO基地址值和组内引脚值,接下来我们就需要针对具体GPIO引脚进行0/1的设置了,针对
流水灯中的一个灯的具体设置进行分析
其他设置是相同的步骤
while(1)
{
MAP_UtilsDelay(8000000);
GPIO_IF_LedOn(MCU_RED_LED_GPIO); //gpio9引脚置1,点亮LED
MAP_UtilsDelay(8000000);
GPIO_IF_LedOff(MCU_RED_LED_GPIO);//gpio9引脚置0,关闭LED
}
进入GPIO_IF_LedOn函数体内部,其主要作用的是MAP_GPIOPinWrite函数,其主要功能为
设置GPIO引脚值,点灯
GPIO_IF_Set(GPIO_LED1, g_uiLED1Port, g_ucLED1Pin, 1);
void GPIO_IF_Set(unsigned char ucPin, unsigned char ucGPIOValue)
{
...........
ucGPIOValue = ucGPIOValue << (ucPin % 8);//gpio值为1时,ucGPIOValue 值为0;反之,值为1
...........
}
void GPIOPinWrite(unsigned long ulPort, unsigned char ucPins, unsigned char ucVal)
{
ASSERT(GPIOBaseValid(ulPort));
HWREG(ulPort + (GPIO_O_GPIO_DATA + (ucPins << 2))) = ucVal; //ucVal表示要配置gpio组内的第几个引脚的位值0/1,将对应的位值写入ucPins指定的输出引脚
}
GPIO_IF_LedOff函数也是相同的配置
延时函数UtilsDelay
__asm( //_asm表示内联汇编,即在C/C++里使用汇编指令
" .sect \".text:UtilsDelay\"\n"
" .clink\n"
" .thumbfunc UtilsDelay\n"
" .thumb\n"
" .global UtilsDelay\n"
"UtilsDelay:\n"
" subs r0, #1\n" //r0-1;注意sub r0,r0,#1<——>r0=r0-1放到r0
" bne.n UtilsDelay\n" //16位的BNE指令,“不相等(或不为0)跳转指令”,如果r0不为0就跳转到后面指定的地址,继续执行;为0,则继续执行下面语句
" bx lr\n"); //转到lr中存放的地址处
lr:连接寄存器(Link Register, LR),在ARM体系结构中LR的特殊用途有两种:一、保存子程序返回地址;二、当异常发生时,LR中保存的值等于异常发生时PC的值减4(或者减2),因此在各种异常模式下可以根据LR的值返回到异常发生前的相应位置继续执行。
这段汇编还有点不太懂