像STM8一样编程STM32(寄存器级GPIO)

Programing STM32 like STM8 (register-level GPIO)

我像PD_ODR_ODR4 = 1;一样编写了STM8 GPIO,但是stm32f10x.h没有这个功能。是否有 .h 文件定义了位?

抱歉,我不知道如何更好地解释这个问题。

我尝试了多个 GPIO 库。

你在问题中提到了stm32f10x.h,所以我假设它是关于STM32F1系列控制器的。其他系列有一些差异,但大体步骤是一样的。

GPIO 引脚排列成 16 个称为端口的组,每个端口都有自己的一组控制寄存器,名为 GPIOAGPIOB 等。它们被定义为指向 [=16= 的指针] 结构。有3个控制寄存器影响引脚输出。

写入 ODR 一次设置所有 16 个引脚,例如GPIOB->ODR = 0xF00F 将引脚 B0B3B12B15 设置为 1,将 B4B11 设置为 0,无论他们之前的状态。可以写入 GPIOD->ODR |= (1<<4) 将引脚 GPIOD4 设置为 1,或者 GPIOD->ODR &= ~(1<<4) 将其重置。

写入BSRR将写入的值视为两个位掩码。低半字是设置掩码,值为 1 的位将 ODR 中的相应位设置为 1。高半字是复位掩码,值为 1 的位将 ODR 中的相应位设置为 0。 GPIOC->BSRR = 0x000701E0 会将引脚 C5C8 设置为 1,将 C0C2 重置为 0,并保留所有其他端口位。在写入 BSRR 时尝试设置和重置相同的位,然后它将设置为 1。

写入BRR与在BSRR中写入重置位掩码相同,即GPIOx->BRR = x等同于GPIOx->BSRR = (x << 16) .

现在可以编写一些宏,例如

#define GPIOD_OUT(pin, value) GPIOD->BSRR = ((0x100 + value) << pin)
#define GPIOD4_OUT(value) GPIOD_SET(4, value)

改变单个针脚,但它不像它应该的那样灵活,例如。你不能获取单个引脚的地址并在变量中传递它。

位带

Cortex-M 控制器(不是所有控制器,但 STM32F1 系列控制器)具有此功能,可以使内部 RAM 和硬件寄存器中的各个位可寻址。 0x40000000-0x400FFFFF 范围内的每一位都映射到 0x42000000-0x43FFFFFF 范围内的完整 32 位字。它不适用于此地址范围之外的外围设备,例如 USB 或 NVIC。

外围寄存器的位带地址可以用这个宏计算

#define BB(reg) ((uint32_t *)(PERIPH_BB_BASE + ((uint32_t)&(reg) - PERIPH_BASE) * 32U))

并且您可以将生成的指针视为包含 32 个字的数组的基数,每个字对应于外围寄存器中的一个位。现在可以

#define PD_ODR_ODR4 (BB(GPIOD->ODR)[4])

并在作业中使用它。读它会给出 0 或 1 作为它的值,写入它的值将写入值的最低有效位复制到外围寄存器位。您甚至可以获取它的地址,并将其传递给一个函数,该函数对引脚执行某些操作。

PM0056 Cortex®-M3 编程手册中记录了位带。

@berendi 提供的答案和@P__J__ 的评论已经很有帮助,但我想提供更多见解。对于一个STM32F103CB的GPIO读写寄存器的原始(bare-metal)track-down,没有库或header文件,直接跳到最下面。 此答案的目的是*教您*如何自己阅读数据表和文档,以便您可以将这些技术应用于*任何微控制器*(包括 STM32)中的*任何内存地址或寄存器*。

请注意,底部的“原始,no-header 无论如何”示例仅用于教育目的:我建议仅使用 CMSIS 和 STM32 提供的 header 文件,因为适用,而不是自己编写。但是,在某些情况下,您可能需要快速访问寄存器,这就是方法。

快速参考:

将任何地址定义为 readable/writable:

// Be sure to add `UL` after ADDRESS_TO_MY_REGISTER to make it "unsigned long".
#define MY_REGISTER (*(volatile uint32_t *)(ADDRESS_TO_MY_REGISTER)) 

将任何地址定义为只读:

// Be sure to add `UL` after ADDRESS_TO_MY_REGISTER to make it "unsigned long".
#define MY_REGISTER (*(volatile const uint32_t *)(ADDRESS_TO_MY_REGISTER)) 

详情:如何在C中定义任何地址位置或内存中的寄存器,使其为readable/writable:

在 C 中访问任何内存地址位置的标准(也是唯一的方法)是使用以下基于 #define 的易失性指针构造:

#define MY_REGISTER (*(volatile uint32_t *)(ADDRESS_TO_MY_REGISTER)) // Be sure to add `UL` after ADDRESS_TO_MY_REGISTER to make it "unsigned long".

如何阅读:

(本质上是从右到左阅读):“获取 ADDRESS_TO_MY_REGISTER 并将其转换为指向 4 个字节的易失性组(即:一组 4 个易失性字节)的指针,然后获取 那组 4 个字节的内容,并使其成为 MY_REGISTER 的意思。”即:MY_REGISTER 现在修改此地址位置的内存的 内容

需要强制转换为指针,将地址位置转换为实际内存地址(指针),最左边的解引用(*)是为了让我们修改 该地址的寄存器或内存的内容,而不仅仅是修改地址本身。需要关键字 volatile 来防止编译器优化,否则编译器优化可能会尝试假定该寄存器中的内容并优化从该寄存器读取或写入该寄存器的代码。访问寄存器时总是需要 volatile,因为必须假设它们可以从其他进程、外部事件或引脚更改或 mcu 本身的硬件 and/or 外围设备进行更改。

尽管此构造适用于 C 中的所有设备(不仅仅是 STM32),但请注意,您转换为的类型的大小(uint8_tuint32_t 等)对您的建筑学。即:不要尝试在 8 位微控制器上使用 uint32_t 类型,因为即使它看起来可以工作,也不能保证对 8 位微控制器上的 32 位内存块进行原子访问。然而,8 位 AVR 微控制器上的 8 位访问实际上保证是自动原子的(相关阅读:C++ decrementing an element of a single-byte (volatile) array is not atomic! WHY? (Also: how do I force atomicity in Atmel AVR mcus/Arduino)). For an STM32 mcu, however, 32-bit or smaller memory accesses are automatically atomic, as I've researched and described here: https://whosebug.com/a/52785864/4561887.

上面这种基于 #define 的结构被所有微控制器使用,你可以用它来任意访问你认为合适的任何内存位置,从字面上看,在任何微控制器上,除非数据表 and/or 参考手册另有说明(例如:某些寄存器首先需要特殊的解锁指令)。例如,如果您追踪 AVRLibc 上的寄存器(由 Arduino 使用——在此处下载:https://www.nongnu.org/avr-libc/ -->“下载”部分),并进行所有宏扩展,您会看到所有寄存器都是 8 -位并归结为:

#define TCCR2A (*(volatile uint8_t *)(0xB0))

此处,寄存器 TCCR2A 或“定时器 2 的定时器计数器控制寄存器 A”在地址 0xB0 处设置为 1 字节。

STM32也是一样,只是寄存器一般都是32位的,所以可以用uint32_t代替uint8_t(虽然uint8_t在STM32上也可以), 他们经常使用 struct-based 结构来代替。例如来自 "stm32f767xx.h":

#define GPIOD               ((GPIO_TypeDef *) GPIOD_BASE)

其中 GPIO_TypeDef 是一个结构:

typedef struct
{
  __IO uint32_t MODER;    /*!< GPIO port mode register,               Address offset: 0x00      */
  __IO uint32_t OTYPER;   /*!< GPIO port output type register,        Address offset: 0x04      */
  __IO uint32_t OSPEEDR;  /*!< GPIO port output speed register,       Address offset: 0x08      */
  __IO uint32_t PUPDR;    /*!< GPIO port pull-up/pull-down register,  Address offset: 0x0C      */
  __IO uint32_t IDR;      /*!< GPIO port input data register,         Address offset: 0x10      */
  __IO uint32_t ODR;      /*!< GPIO port output data register,        Address offset: 0x14      */
  __IO uint32_t BSRR;     /*!< GPIO port bit set/reset register,      Address offset: 0x18      */
  __IO uint32_t LCKR;     /*!< GPIO port configuration lock register, Address offset: 0x1C      */
  __IO uint32_t AFR[2];   /*!< GPIO alternate function registers,     Address offset: 0x20-0x24 */
} GPIO_TypeDef;

__IO 被简单地定义为 volatile。由于此结构的每个成员都是 4 字节,并且您有 4 字节对齐,因此该结构会自动打包,因此您最终结构的每个新元素都简单地指向地址位置“地址偏移量”(如中所示右边的评论)离基地址更远,所以一切正常!

例如,使用 STM32 定义的 GPIOD->BSRR 类型构造的替代方法是像这样自己手动完成:

#define GPIOD_BSRR (*(volatile uint32_t *)(GPIOD_BASE + 0x18UL)) // Don't forget the `U` or `UL` at the end of 0x18 here!

如果要注册怎么办read-only?只需将 const 添加到 cast-to-a-pointer 中 * 左侧 的任意位置:

#define MY_REGISTER (*(volatile const uint32_t *)(ADDRESS_TO_MY_REGISTER)) // Be sure to add `UL` after ADDRESS_TO_MY_REGISTER to make it "unsigned long".

获取、设置和清除位:

现在,您可以使用 bit-shifting 和位掩码和位操作,或使用您可能定义的一些宏来设置或获取寄存器中的任何位。

例如:

// get bit30 from the address location you just described above
bool bit30 = (MY_REGISTER >> 30UL) & 0x1UL;
// or (relies on the fact that anything NOT 0 in C is automatically `true`):
bool bit30 = MY_REGISTER & (1UL << 30UL);

// set bit 30 to 1
MY_REGISTER |= (1UL << 30UL);

// clear bit 30 to 0
MY_REGISTER &= ~(1UL << 30UL);

或: (例如,正如 Arduino 在这里所做的那样:https://github.com/arduino/ArduinoCore-avr/blob/master/cores/arduino/Arduino.h

#define bitRead(value, bit) (((value) >> (bit)) & 0x01)
#define bitSet(value, bit) ((value) |= (1UL << (bit)))
#define bitClear(value, bit) ((value) &= ~(1UL << (bit)))
#define bitWrite(value, bit, bitvalue) (bitvalue ? bitSet(value, bit) : bitClear(value, bit))

然后:

// get bit 30
bool bit30 = bitRead(MY_REGISTER, 30);

// set bit 30 to 1
bitSet(MY_REGISTER, 30);
// or
bitWrite(MY_REGISTER, 30, 1);

// clear bit 30 to 0
bitClear(MY_REGISTER, 30);
// or
bitWrite(MY_REGISTER, 30, 0);

一个STM32F103CB的GPIO读写寄存器的Raw (bare-metal) track-down,wit没有库或 header 个文件。

我们需要:

  1. 该芯片的主要参考页:https://www.st.com/content/st_com/en/products/microcontrollers-microprocessors/stm32-32-bit-arm-cortex-mcus/stm32-mainstream-mcus/stm32f1-series/stm32f103/stm32f103cb.html#design-scroll
  2. STM32参考手册(包含寄存器定义):RM0008, Reference Manual for STM32F101xx, 101xx, 103xx, etc
  3. STM32数据表(包含基地址):DS5319

读取 RM0008 p161-162。

我不会详述所有细节(阅读上面),但要读取 一个引脚,您需要 GPIOx_IDR(GPIO 输入数据寄存器)。要将引脚写入 0 或 1,您需要 GPIOx_ODR(GPIO 输出数据寄存器)。显然(基于 RM0008 中的措辞,如上所示)对 GPIOx_ODR 的写入不是作为一个组的原子,所以如果你想要原子地写入端口上的一堆引脚(全部在同一瞬间)你需要使用 GPIOx_BSRR(GPIO 位 Set/Reset 寄存器)或 GPIOx_BRR(GPIO 位复位寄存器——只能将位清除为 0)。

假设我们只做端口 A,这意味着我们需要定义以下寄存器:

GPIOA_IDR   // Input Data Register (for reading pins on Port A)
GPIOA_ODR   // Output Data Register (for *nonatomically* writing 0 or 1 to pins on Port A)
GPIOA_BSRR  // Bit Set/Reset Register (for *atomically* setting (writing 1) or resetting (writing 0) pins on Port A)
GPIOA_BRR   // Bit Reset Register (for *atomically* resetting (writing 0) pins on Port A)

我们去找这些寄存器的地址吧!

参见 RM0008 p172 至 174。

我们可以看到偏移量和数据方向如下:

| Register   | "Address offset"| direction
|------------|-----------------|---------------
| GPIOA_IDR  | 0x08            | r (read only)   
| GPIOA_ODR  | 0x0C            | rw (read/write) 
| GPIOA_BSRR | 0x10            | w (write only)
| GPIOA_BRR  | 0x14            | w (write only)

现在我们只需要端口 A 的基地址。转到 DS5319 第 4 章:内存映射,图 11. 内存映射,您会看到基地址“端口 A”是 0x40010800,如此处所示和突出显示:

现在,让我们手动定义寄存器:

#define GPIOA_IDR  (*(volatile const uint32_t *)(0x40010800UL + 0x08UL)) // use `const` since this register is read-only
#define GPIOA_ODR  (*(volatile uint32_t *)(0x40010800UL + 0x0CUL))
#define GPIOA_BSRR (*(volatile uint32_t *)(0x40010800UL + 0x10UL))
#define GPIOA_BRR  (*(volatile uint32_t *)(0x40010800UL + 0x14UL))

现在我们来读写一个pin:

// Choose a pin number from 0 to 15
uint8_t pin_i = 0; // pin index

// Read it
bool pin_state = (GPIOA_IDR >> (uint32_t)pin_i) & 0x1;

// Write it to 1
GPIOA_ODR |= 1UL << (uint32_t)pin_i; // not to be used for writing to more than 1 pin at a time since apparently its operation is not atomic?
// OR
GPIOA_BSRR |= 1UL << (uint32_t)pin_i; // GOOD! RECOMMENDED approach

// Write it to 0
GPIOA_ODR &= ~(1UL << (uint32_t)pin_i); // not to be used for writing to more than 1 pin at a time since apparently its operation is not atomic?
// OR
GPIOA_BSRR |= (1UL << (uint32_t)pin_i) << 16UL; // GOOD! RECOMMENDED approach, but a bit confusing obviously
// OR
GPIOA_BRR |= 1UL << (uint32_t)pin_i; // GOOD! RECOMMENDED approach

或者:只需使用 HAL 库即可完成。

例如:来自“STM32Cube_FW_F1_V1.6.0/Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_gpio.c”:

HAL_GPIO_ReadPin()(注意他们使用 GPIOx->IDR 寄存器进行读取):

/**
  * @brief  Reads the specified input port pin.
  * @param  GPIOx: where x can be (A..G depending on device used) to select the GPIO peripheral
  * @param  GPIO_Pin: specifies the port bit to read.
  *         This parameter can be GPIO_PIN_x where x can be (0..15).
  * @retval The input port pin value.
  */
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
  GPIO_PinState bitstatus;

  /* Check the parameters */
  assert_param(IS_GPIO_PIN(GPIO_Pin));

  if ((GPIOx->IDR & GPIO_Pin) != (uint32_t)GPIO_PIN_RESET)
  {
    bitstatus = GPIO_PIN_SET;
  }
  else
  {
    bitstatus = GPIO_PIN_RESET;
  }
  return bitstatus;
}

HAL_GPIO_WritePin()(注意他们使用 GPIOx->BSRR 寄存器将引脚写入 0 和 1):

/**
  * @brief  Sets or clears the selected data port bit.
  * 
  * @note   This function uses GPIOx_BSRR register to allow atomic read/modify 
  *         accesses. In this way, there is no risk of an IRQ occurring between
  *         the read and the modify access.
  *               
  * @param  GPIOx: where x can be (A..G depending on device used) to select the GPIO peripheral
  * @param  GPIO_Pin: specifies the port bit to be written.
  *          This parameter can be one of GPIO_PIN_x where x can be (0..15).
  * @param  PinState: specifies the value to be written to the selected bit.
  *          This parameter can be one of the GPIO_PinState enum values:
  *            @arg GPIO_BIT_RESET: to clear the port pin
  *            @arg GPIO_BIT_SET: to set the port pin
  * @retval None
  */
void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
{
  /* Check the parameters */
  assert_param(IS_GPIO_PIN(GPIO_Pin));
  assert_param(IS_GPIO_PIN_ACTION(PinState));

  if(PinState != GPIO_PIN_RESET)
  {
    GPIOx->BSRR = GPIO_Pin;
  }
  else
  {
    GPIOx->BSRR = (uint32_t)GPIO_Pin << 16U;
  }
}

更进一步:原子变量和寄存器访问

如果您需要对寄存器进行原子访问以确保在主循环中读取或写入时它们不会被另一个线程或上下文(例如:被 ISR)修改,这里是我所知道的所有方法保证 bare-metal(无操作系统)STM32 上的原子访问:Multiple ways to enable/disable interrupts in STM32 mcus:

结束