1、嵌入式基础与gpio
在STM32中,端口是指一组相同功能的引脚,通常用于连接外设或者控制IO口。
每个端口都有一些寄存器来控制和表示其状态。
而引脚则是指单个的IO口,可以通过设置GPIO相关寄存器来控制它的输入输出状态。
gpio 端口中的寄存器
这些寄存器也就是库函数中gpio_typedef的成员。
寄存器可以分为三类:控制寄存器,数据寄存器,状态寄存器。
注:输入输出方式MODE共有8种,四种输入(2位),四种输出(2位)。所以,每四位可以控制一个io口,其中有一个寄存器的两位决定输入输出。
注:对于每个GPIO引脚,我们都有一对GPIO位(0和1),一个用于输入模式下的状态读取,另一个用于输出模式下的状态控制, 如在输入模式下,GPIO引脚可以读取其连接的设备的状态。
例如,如果一个设备被连接到一个GPIO引脚,并且该设备是开启的,那么读取该引脚的状态将返回1;如果该设备是关闭的,那么读取该引脚的状态将返回0。所以对于每个引脚,我们都有一对GPIO位(0和1)来表示其状态。
控制芯片的寄存器
直接地址法:
顾名思义,直接操作stm32寄存器的地址,进行编程。
读写一个地址的方法: *(地址) = ...;
#define vuint8 volatile uint8_t #define vuint32 volatile uint32_t #define uint32 uint32_t //端口B使能寄存器地址 vuint32* enB = (uint32*) 0x4002104c; //端口B的基地址 vuint32* ptrB = (uint32*) 0x48000400; //引脚模式寄存器地址 vuint32* modeB = ptrB; //置位复位 vuint32* bsrr = ptrB + 6; //gpio位复位 vuint32* brr = ptrB + 10; (*enB) |= (1 << 1); (* modeB) &= ~(3 << 14); //清零 (*modeB) |= (1 << 14); //设置为输出模式 (*brr) |= (1 << 7); //将第7个引脚复位为0,表示灯亮
偏移地址:
用基地址+偏移量来进行寄存器和引脚的定位。
实现方式:将各个寄存器的基地址记录在数组中,然后通过偏移量进行寻址。
gpio.c 源码
为了编程方便,我们使用一个16位寄存器装入端口和引脚,并通过位运算的方式进行取出。
void gpio_get_port_pin(uint16_t port_pin,uint8_t* port,uint8_t* pin) { *port = (port_pin>>8); //右移八位,留下高八位的端口。 *pin = port_pin; //将其赋值为一个低位寄存器,直接获得其低八位的引脚。 }
void gpio_init(uint16_t port_pin, uint8_t dir, uint8_t state) //port_pin高八位是port的偏移量,低八位是引脚位置,dir是输入输出模式,state是dir对应的状态 { GPIO_TypeDef *gpio_ptr; //声明gpio_ptr为GPIO结构体类型指针 uint8_t port,pin; //声明端口port、引脚pin变量 uint32_t temp; //临时存放寄存器里的值 //根据带入参数port_pin,解析出端口与引脚分别赋给port,pin gpio_get_port_pin(port_pin,&port,&pin); //根据port,给局部变量gpio_ptr赋值(GPIO基地址) if(7 == port) //GPIOH gpio_ptr = GPIO_ARR[port-2]; else gpio_ptr = GPIO_ARR[port];
/* ***********到这里获取了端口位置************* */ //使能相应GPIO时钟 RCC->AHB2ENR |= (RCC_AHB2ENR_GPIOAEN<<(port * 1u)); //给端口设置使能信号 //清GPIO模式寄存器对应引脚位 temp = gpio_ptr->MODER; //获取模式寄存器的值 temp &= ~(GPIO_MODER_MODE0 << (pin * 2u)); //清空模式, 这里 pin*2是因为输入输出由一对引脚控制。 if(dir == 1) //定义为输出引脚 { temp |= (GPIO_OUTPUT << (pin * 2u)); //第pin * 2位表示输入输出状态。 gpio_ptr->MODER = temp; //把暂存值写回 gpio_set(port_pin,state); //调用gpio_set函数,设定引脚初始状态 } else //定义为输入引脚 { temp |= (GPIO_INPUT << (pin * 2u)); gpio_ptr->MODER = temp; } }
void gpio_set(uint16_t port_pin, uint8_t state) { //局部变量声明 GPIO_TypeDef *gpio_ptr; //声明port_ptr为GPIO结构体类型指针(首地址) uint8_t port,pin; //声明端口port、引脚pin变量 //根据带入参数port_pin,解析出端口与引脚分别赋给port,pin gpio_get_port_pin(port_pin,&port,&pin); //根据port,给局部变量gpio_ptr赋值(GPIO基地址) if(7 == port) //GPIOH gpio_ptr = GPIO_ARR[port-2]; else gpio_ptr = GPIO_ARR[port];
/* ***** 到这里获取了端口 ***** */ //根据state,设置对应引脚状态 if(1 == state) //高电平(该引脚对应置位寄存器置1) gpio_ptr->BSRR = (uint32_t)(1u<<pin); //bsrr为设置寄存器,可以将 1 << pin 这一位设置为1 else //低电平(该引脚对应重置寄存器置1) gpio_ptr->BRR = (uint32_t)(1u<<pin); //brr为重置寄存器,可以将 1 << pin 这一位重置为0 }
uint8_t gpio_get(uint16_t port_pin) { //局部变量声明 GPIO_TypeDef *gpio_ptr; //声明port_ptr为GPIO结构体类型指针(首地址) uint8_t port,pin; //声明端口port、引脚pin变量 uint32_t temp; uint8_t value; //根据带入参数port_pin,解析出端口与引脚分别赋给port,pin gpio_get_port_pin(port_pin,&port,&pin); //根据port,给局部变量gpio_ptr赋值(GPIO基地址) if(7 == port) //GPIOH gpio_ptr = GPIO_ARR[port-2]; else gpio_ptr = GPIO_ARR[port]; //获得状态寄存器的值 temp = gpio_ptr->MODER; if( (temp >> pin * 2) & 1 )//GPIO输出 检查pin*2位置的那一位是否为1, 为1就是输出 { //读取Output data寄存器对应引脚的值 temp = gpio_ptr->ODR; if((temp & (1u<<pin)) != 0x00u) //查看输出寄存器中的数据是否为0,不为0返回1, 输入同理 value = 1; else value = 0; } else //GPIO输入 { //读取Input data寄存器对应引脚的值 temp = gpio_ptr->IDR; if((temp & (1u<<pin)) != 0x00u) value = 1; else value = 0; } return value; }
延时 delay.h
delay函数的作用是在程序中引入一个时间延迟。当调用delay函数时,程序会停止执行指定的时间段,然后再继续执行后面的代码。常见的情况是在需要控制程序执行速度或者需要等待一段时间后再执行下一步操作时使用delay函数。
计算机的延时操作可以通过以下几种方式实现:
- 使用sleep函数,在指定时间内让程序暂停执行。
- 使用计时器,设置一个定时器,当时间到达时触发延时操作。
- 循环等待,不断地进行空操作,直到时间到达。
伪代码如下:
function delay(seconds) { // 获取当前时间 start = 当前时间 // 循环检查时间,直到指定的时间间隔过去 while (当前时间 - start < seconds) { 继续执行循环 } }
//或者,可以通过cpu频率,首先要知道执行一条加法指令的时间,然后放到while中去,进行累计。
function delay(seconds) {
ull cnt = seconds * 1000 / 单条指令执行时间.
ull i = 0;
while(i < cnt) i ++ ;
}
实验中用到的代码:
void Delay_ms(uint16_t u16ms) { for(volatile uint32_t i = 0; i < 8000*u16ms; i++) __asm("NOP"); }