嵌入式尤其是MCU开发和其他编程最大的区别在于你的代码是要和硬件直接沟通的,比如说你开发一个桌面应用通常是运行在操作系统内部,操作系统帮你屏蔽了大部分底层硬件,访问内存也不是真实物理内存而是mmu映射出来的逻辑内存,这当然极大的方便了开发者。但是有时候你的每一行代码都直接控制硬件,得到物理世界的反馈会带来一些奇妙的感觉,这也是我觉得嵌入式开发最有意思的地方。

这是一个经典的ARM cortex-m内核芯片——stm32f103c8t6,相信很多初学者都使用过这个芯片,通过图片你就可以看出来这个芯片和外界进行连接的地方无非就是一个个金属引脚,那芯片想要对外部进行控制或者和外部通信也就只能通过控制金属引脚达到目的。
查看数据手册知道这些金属引脚术语就是GPIO,里面也说了每个引脚是和ODR(输出数据寄存器)的一个位进行对应,即引脚对应寄存器的位是0引脚输出低电平(GND),是1则输出高电平(VCC-3.3V)。

那么控制GPIO输出的问题就变成了控制ODR每一个位的问题,但是这种寄存器不像一般变量有变量名可以直接通过变量名进行加减乘除等操作,而且操作寄存器的一个位也不能通过加减乘除实现,好在C语言语法在这方面有一些便捷的操作,位运算和指针。
| p | q | p & q | p\ | q | p ^ q |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | |
| 0 | 1 | 0 | 1 | 1 | |
| 1 | 1 | 1 | 1 | 0 | |
| 1 | 0 | 0 | 1 | 1 |
上面我附上了除取反以外的位运算真值表,如果你学过数电那这个东西你并不陌生,事实上在芯片内部再高级的操作也都是通过位运算最终实现的,没学过也没关系也不必深究。计算机体系最大的精髓就是不是任何一层而在于层层抽象,你在其中一层进行工作的时候并不需要考虑底层到底做了什么(当然必须要说如果你知道对你理解更有帮助),这里你只需要在C语言这个层次上知道可以通过位运算改变单个位就可以了。
指针这里我想先操作一个变量进行说明,声明变量时加上“*”表示这是一个指针,“&”在这里是取地址符把counter的地址给p_int,指针的大小之和处理器架构有关,32位的处理器就是3bit,64位就是64个bit。
int32_t counter = 0;
int main(void) {
int32_t *p_int;
p_int = &counter;
while (*p_int < 21) {
++(*p_int);
}
return 0;
}
在监视窗口可以看到p_int和&counter都是0x2000 0020,其实这个就是counter的地址,*p_int即访问p_int地址处的数值。也就是说有了地址就可以操作,理论存在开始实践,查数据手册得知gpioc的odr寄存器地址为0x40020814,理论存在,让我们开始一个简单的实践。
#define BIT13 (1 << 13)
int main(void) {
uint32_t* gpioc_odr = (uint32_t*)(0x40020814U);
*gpioc_odr &= ~BIT13; // 置零GPIOC13
// *gpioc_odr |= BIT13 ; // 置一GPIOC13
}我这里gpioc13接了一个led,很遗憾不管置0还置1都不能点亮led很显然配置出现了问题,如果你去查看memory视图你会发现如果你进行了置1操作,对应的地址处任然全部是0,也可能看到“- - - - - - - - ”。

并不是你对指针的使用有问题,而是因为MCU的复杂设计,GPIOC这个外设的时钟还没有启用,理论上你访问外设寄存器的行为被认为无效,为什么设计时钟这个机制这里不展开说明。不过看起来要想控制一个GPIO并不是那么简单,但想要解决问题还是要去看数据手册。
你会发现一个叫时钟树的的东西,GPIOC13属于GPIOC这一组,GPIOC又挂在AHB1总线上,当然你使用的MCU可能时钟树更复杂,使用时你必须一级一级的启用时钟,然后配置对应GPIO,每一步都可能涉及好几个寄存器。
说真的我已经不想去数据手册查地址了,太多了搞不好就会有疏漏,使用一个GPIO尚且如此复杂,如果要使用更复杂的功能怎么做,芯片厂家其实早就考虑到了这个问题,一般会有一个设备外设访问层头文件,如果没有那么毫无疑问这个芯片厂家可以倒闭了,反正我不会用的。

#define RCC_AHB1ENR_GPIOCEN_Pos (2U)
#define RCC_AHB1ENR_GPIOCEN_Msk (0x1UL << RCC_AHB1ENR_GPIOCEN_Pos)
#define RCC_AHB1ENR_GPIOCEN RCC_AHB1ENR_GPIOCEN_Msk
/**
* @brief 通用 I/O
*/
typedef struct
{
__IO uint32_t MODER; /*!< GPIO 端口模式寄存器,地址偏移:0x00 */
__IO uint32_t OTYPER; /*!< GPIO 口输出类型寄存器,地址偏移:0x04 */
__IO uint32_t OSPEEDR; /*!< GPIO 口输出速度寄存器,地址偏移:0x08 */
__IO uint32_t PUPDR; /*!< GPIO 口上拉/下拉寄存器,地址偏移:0x0C */
__IO uint32_t IDR; /*!< GPIO 口输入数据寄存器,地址偏移:0x10 */
__IO uint32_t ODR; /*!< GPIO 口输出数据寄存器,地址偏移:0x14 */
__IO uint32_t BSRR; /*!< GPIO 端口位设置/重置寄存器,地址偏移:0x18 */
__IO uint32_t LCKR; /*!< GPIO 端口配置锁寄存器,地址偏移:0x1C */
__IO uint32_t AFR[2]; /*!< GPIO 备用功能寄存器,地址偏移:0x20-0x24 */
} GPIO_TypeDef;
#define PERIPH_BASE 0x40000000UL
#define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000UL)
#define GPIOC_BASE (AHB1PERIPH_BASE + 0x0800UL)
#define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)有了这个头文件你就可以通过GPIOC这个宏定义访问GPIOC各个寄存器,使用RCC_AHB1ENR_GPIOCEN可以更方便地启用GPIOC的一系列时钟。
int main(void) {
uint32_t counter = 0;
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOCEN; // 使能GPIOC时钟
GPIOC->MODER |= (0b01 << (13 * 2)); // 设置为通用输出模式
GPIOC->OTYPER |= (0b0 << 13); // 设置为推挽模式
GPIOC->OSPEEDR |= (0b00 << (13 * 2)); // 设置为低速模式
GPIOC->PUPDR |= (0b01 << (13 * 2)); // 设置为上拉模式
while (1) {
GPIOC->ODR &= ~(1 << 13); // 置零GPIOC13
while (counter < 300000) {
counter++;
}
counter = 0;
GPIOC->ODR |= (1 << 13); // 置一GPIOC13
while (counter < 300000) {
counter++;
}
counter = 0;
}
}
到这里已经实现了嵌入式开发的"hello world",成功点亮一颗LED,你可能会说我都使用CUBEMX生成工程了有必要费劲巴拉的去翻数据手册控制寄存器嘛,而且现在的芯片厂家都出了一些方便配置的工具,比如CUBEMX,甚至51单片机都有厂家出一些工具可以生成一些初始化代码
确实没有必要,但绝不是毫无意义,在这里我直接操作寄存器也是为了带你了解底层到底是如何操作的,使用一个外设的流程,展示官方的文件是怎么通过地址映射,结构体,访问每一个外设的寄存器。如果你去看HAL库函数内部也不过是在操作这些结构体基础上加一些其他操作防止一些参数错误。
另外如果你之后做自己的项目有一些时间关键性操作你也可以替换成直接对寄存器的操作避免调用HAL库函数调用的各种开销。
评论已关闭