@berendi 提供的答案和@P__J__ 的评论已经很有帮助,但我想提供更多见解。对于 STM32F103CB 的 GPIO 读写寄存器的原始(裸机)追踪,没有库或头文件,直接跳到底部。这个答案的目的是*教你*如何自己阅读数据表和文档,以便你可以将这些技术应用于*任何微控制器*中的*任何内存地址或寄存器*,包括STM32。
请注意,底部的“原始的、无头文件”示例仅用于教育目的:我建议仅使用 CMSIS 和 STM32 提供的头文件(如果适用),而不是编写自己的头文件。但是,在某些情况下,您可能需要快速访问寄存器,这就是方法。
#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".
Run Code Online (Sandbox Code Playgroud)
#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".
Run Code Online (Sandbox Code Playgroud)
在 C 中访问任何内存地址位置的标准(也是唯一真正的方法)方法是使用以下#define基于 volatile 指针结构:
#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".
Run Code Online (Sandbox Code Playgroud)
如何阅读:
(本质上是从右到左阅读):“将 ADDRESS_TO_MY_REGISTER 转换为指向 4 个字节的易失性组(即:4 个易失性字节的组)的指针,然后获取该4 个字节组的内容,并使其成为什么MY_REGISTER意思。” 即: MY_REGISTER 现在修改此地址位置的内存内容。
需要转换为指针才能将地址位置转换为实际的内存地址(指针),而*最左边的解引用()是为了让我们修改该地址处的寄存器或内存的内容,而不仅仅是修改地址本身。volatile需要关键字来防止编译器优化,否则编译器优化可能会尝试假设该寄存器中的内容并优化从该寄存器读取或写入该寄存器的代码。volatile访问寄存器时总是需要的,因为必须假设它们可以从其他进程、外部事件或引脚更改,或 MCU 本身的硬件和/或外设中更改。
尽管这种结构适用于所有的设备在C(不只是STM32),注意,你投的类型(大小uint8_t,uint32_t等等)是你的架构非常重要。即:不要尝试uint32_t在 8 位微控制器上使用类型,因为即使它看起来可行,也不能保证原子访问 8 位微控制器上的 32 位内存块。然而,8 位 AVR 微控制器上的 8 位访问实际上保证是自动原子的(相关阅读:C++ 递减单字节(易失性)数组的元素不是原子的!为什么?(另外:怎么做?我在 Atmel AVR mcus/Arduino 中强制原子性)。但是,对于 STM32 mcu,32 位或更小的内存访问是自动原子的,正如我在此处研究和描述的:/sf/answers/3695010511/。
上面这种类型的#define基于构造的所有微控制器都在使用它,您可以使用它来任意访问您认为合适的任何内存位置,字面上,在任何微控制器上,除非数据表和/或参考手册另有说明(例如:某些寄存器首先需要特殊的解锁说明)。例如,如果您跟踪 AVRLibc 上的寄存器(由 Arduino 使用--在此处下载:https ://www.nongnu.org/avr-libc/-- >“下载”部分),并进行所有宏扩展,你会看到所有寄存器都是 8 位的,归结为:
#define TCCR2A (*(volatile uint8_t *)(0xB0))
Run Code Online (Sandbox Code Playgroud)
此处,寄存器TCCR2A或“定时器 2 的定时器计数器控制寄存器 A”在地址 0xB0 处设置为 1 字节。
STM32 也是如此,除了寄存器通常是 32 位的,所以你可以使用uint32_t代替uint8_t(虽然uint8_t也适用于 STM32),并且它们经常使用基于结构的构造来代替。例如来自“stm32f767xx.h”:
#define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)
Run Code Online (Sandbox Code Playgroud)
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;
Run Code Online (Sandbox Code Playgroud)
并且__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!
Run Code Online (Sandbox Code Playgroud)
如果你想让寄存器只读怎么办?只需在 cast-to-a-pointer左侧的const任意位置添加:*
#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".
Run Code Online (Sandbox Code Playgroud)
现在,您可以使用位移位、位掩码和位操作或使用一些您可能定义的宏来设置或获取寄存器中的任何位。
前任:
// 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);
Run Code Online (Sandbox Code Playgroud)
或:(例如,正如 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))
Run Code Online (Sandbox Code Playgroud)
然后:
// 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);
Run Code Online (Sandbox Code Playgroud)
我们会需要:
我不会详细介绍(阅读上文),但要读取引脚,您需要GPIOx_IDR(GPIO 输入数据寄存器)。要将引脚写入 0 或 1,您需要GPIOx_ODR(GPIO 输出数据寄存器)。显然(基于上面显示的 RM0008 中的措辞)写入GPIOx_ODR不是作为一个组原子的,因此如果您希望以原子方式(全部在同一时刻)写入端口上的一堆引脚,您需要使用GPIOx_BSRR( GPIO 位设置/复位寄存器)或GPIOx_BRR(GPIO 位复位寄存器——只能将位清零)。
假设我们只要做端口 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)
Run Code Online (Sandbox Code Playgroud)
让我们去找这些寄存器的地址吧!
请参阅 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)
Run Code Online (Sandbox Code Playgroud)
现在我们只需要端口 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))
Run Code Online (Sandbox Code Playgroud)
// 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
Run Code Online (Sandbox Code Playgroud)
例如:来自“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;
}
Run Code Online (Sandbox Code Playgroud)
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;
}
}
Run Code Online (Sandbox Code Playgroud)
结尾
您stm32f10x.h在问题中提到了,所以我假设它是关于STM32F1系列控制器的。其他系列有一些差异,但是一般过程是相同的。
GPIO引脚被安排在16个称为端口组,每个有它自己的一套控制寄存器,命名GPIOA,GPIOB等,他们被定义为指针GPIO_TypeDef结构。有3个控制寄存器会影响引脚输出。
编写ODR组同时在所有16个引脚,例如GPIOB->ODR = 0xF00F套针B0通过B3,并B12通过B15为1,并B4通过B11为0,而不管其以前的状态的。可以写GPIOD->ODR |= (1<<4)将引脚设置GPIOD4为1或GPIOD->ODR &= ~(1<<4)将其复位。
写入BSRR将写入的值视为两个位掩码。低半字是一组掩模,具有值1套在相应的位位ODR为1。半字高是复位面膜,具有值1套在对应的比特位ODR为0。GPIOC->BSRR = 0x000701E0将设置销C5虽然C8为1,复位C0通过C2对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)
Run Code Online (Sandbox Code Playgroud)
来更改单个引脚,但是它不那么灵活,例如,您不能使用单个引脚的地址并将其传递给变量。
位带
Cortex-M控制器(不是全部,但所有STM32F1系列都具有)具有此功能,以使内部RAM和硬件寄存器中的单个位可寻址。0x40000000-0x400FFFFF范围中的每个位都映射到范围中的完整32位字0x42000000-0x43FFFFFF。它不适用于此地址范围以外的外围设备,例如USB或NVIC。
可以使用该宏计算外设寄存器的位带地址
#define BB(reg) ((uint32_t *)(PERIPH_BB_BASE + ((uint32_t)&(reg) - PERIPH_BASE) * 32U))
Run Code Online (Sandbox Code Playgroud)
您可以将结果指针视为包含32个字的数组的基础,每个字对应于外设寄存器中的一位。现在可以
#define PD_ODR_ODR4 (BB(GPIOD->ODR)[4])
Run Code Online (Sandbox Code Playgroud)
并在作业中使用它。读取它会得到0或1作为其值,写入它的值会将写入值的最低有效位复制到外设寄存器位。您甚至可以获取其地址,并将其传递给使用该引脚执行某些操作的函数。
PM0056Cortex®-M3编程手册中记录了位带。
| 归档时间: |
|
| 查看次数: |
1388 次 |
| 最近记录: |