嵌入式学习笔记——寄存器实现控制LED小灯

文章详细介绍了如何使用STM32的GPIO通用输出模式初始化LED灯,包括时钟使能、模式寄存器、输出类型、速度和上下拉配置。同时,讨论了通过宏定义和条件运算符进行功能封装的方法,并给出了流水灯功能的实现。此外,还分享了KEILMDK使用中的一些技巧,如代码错误定位、编译器版本选择和ST-LINK烧录设置。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

上一篇中,介绍了GPIO相关的所有寄存器,并在最后简单实现了一个LED灯的控制,由于那个篇幅实在是太长了,编程那部分写的有些许潦草,本文再借着点亮剩下的LED小灯来做个稍微详细点的描述,会涉及一些开发环境使用中的常见BUG、以及部分位操作相关的C语言知识。文中如有不足之处欢迎大家批评指正,创作不易,需要转载或者引用的请注明出处。

GPIO通用输出模式

常见的使用通用输出模式的片外外设就是LED灯、有源蜂鸣器、继电器等等,在我们板子上最常见的就是LED灯,上一篇中,配置了GPIO的端口A的4号管脚为推挽输出模式,并输出了低电平点亮了LED灯,我们先回顾一下配置流程,

初始化LED小灯的GPIO

首先是初始化部分,这里还是借用一个伪代码,这样看起来更加直观:

//注意前面这一段注释一定要记得写上,一方面是为了方便自己能够看懂自己的代码,另一方面,当别人看你的代码的时候也能一目了然。
/*******************************
函数名:Led_Init
函数功能:LED灯IO口初始化配置
函数形参:void
函数返回值:void
备注:
LED1----->PA6 通用输出
LED2----->PA7 通用输出
********************************/
void Led_Init(void)//函数命名,一般是'模块——功能'其中模块名与功能名的首字母大写
{
	①打开PA的时钟
	②端口模式寄存器
	③输出类型寄存器
	④输出速度寄存器
	⑤上下拉寄存器
}

根据这个伪代码我们即可配置出对应端口对应管脚的模式为通用输出模式。
下面我们在昨天的基础上再来配置一次,这次将两个LED都一并初始化。

原理图

还是昨天的原理图,从这可以知道,需要配置的是PA6和PA7两个口。
在这里插入图片描述

初始化代码

/*******************************
函数名:Led_Init
函数功能:LED灯IO口初始化配置
函数形参:void
函数返回值:void
备注:
LED1----->PA6 通用输出
LED2----->PA7 通用输出
********************************/
void Led_Init(void)//函数命名,一般是'模块——功能'其中模块名与功能名的首字母大写
{
/*---------------------①打开PA的时钟------------------------------------------------------------------------*/
	RCC->AHB1ENR |= (1<<0);
	/*对应在编程手册的6.3.12节,有关于此寄存器的配置;
	GPIOA端口的时钟使能是有该寄存器的第0位进行控制的,要使能GPIOA的时钟就是对其第0位进行写1,
	根据前面提到的位操作,就是将1直接赋值给该寄存器即可。*/
/*----------------------②端口模式寄存器---------------------------------------------------------------------*/
			/*端口模式清零*/
	GPIOA->MODER &= ~(0xf<<12);
	/*这一步是保证我们操作的寄存器在我们写入数据之前一定是00.
	避免出现被覆盖,而变成其他模式的问题。清零的思路:
	1.我们需要写入的是5和6两个端口,也就是这个寄存器的15 14 13 12 这四位
	为了将这四位清零,首先使用‘|’运算肯定是不行的,只能选择‘&0’的操作才能确保对应位清零。
	2.0是不能移位操作的,所以只能借助1移位后再进行取反来实现;
	3.于是得出清零语句GPIOA->MODER &= ~(0xf<<12);也就是0xf也就是二进制的1111
	向前移了12位变成了1111 0000 0000 0000然后取反变成0000 1111 1111 1111
	然后与原来寄存器内的数据相与,和0相与的位都被清成0了,和1相与的位保持不变,
	0000 XXXX XXXX XXXX
	这样既保证了对应位写入0又保证了其他位不被干扰。*/
	/*端口通用输出模式*/
	GPIOA->MODER |= (0x5<<12);
	/*上一步已经清零了需要配置的位,接下来直接写入即可,写入过程:
	1.查询寄存器可以知道,要配置为通用输出模式,需要对这四位写入:01 01;
	2.具体操作可以通过|=来实现,GPIOA->MODER |= (0x5<<12);
	3.0101是十六进制的0x5,需要前移12位来到我们需要操作的数据位,
	由于里面的数据位都是0,所以|操作后这四位被覆盖成了0101*/
/*----------------------③输出类型寄存器-----------------------------------------------------------------------*/
		/*端口输出推挽模式*/
	GPIOA->OTYPER &= ~(3<<6);
	/*由于我们需要控制小灯的亮灭,这就要求GPIO口具有独立输出高低电平的能力,
	所以我们需要配置为推挽模式,也就是需要将第六位以及第七位进行写零操作,
	参考上面的写零思路:只需要将二进制的11也就是十进制的3左移6位即可实现。*/
/*----------------------④输出速度寄存器-----------------------------------------------------------------------*/
	/*端口输出速度25MHz就只是控制一个LED灯,对于引脚的高低电平翻转速度没有啥要求,
	配置一个中速即可,也就是需要将第15 14 13 12 四位先清零,然后写入0101,与上面操作一致*/
	GPIOA->OSPEEDR &= ~(0xf<<12);//清零OSPEEDR
	GPIOA->OSPEEDR |= (0x5<<12);//25MHZ中速
/*----------------------⑤上下拉寄存器--------------------------------------------------------------------------*/
	GPIOA->PUPDR   &= ~(0xf<<12);//默认无上下拉
	/*由于是输出模式,我们不需要上下拉操作,直接对对应的15 14 13 12 这四位写零即可*/
}

初始化的效果

到这里,我们已经初始化完成了两个LED的GPIO口,此时不管我们先抛开ODR寄存器不管,直接编译烧录,就会发现,两个LED灯已经点亮了,这是因为ODR寄存器默认状态就是低电平输出,所以他会亮。
在这里插入图片描述

功能函数封装

实际做产品的过程中,很少有说初始化后就直接点亮或者开启的,都是需要有后续逻辑控制了后再开启的,所以我们需要对上面的代码进行加工,
按照之前的思路,应该是直接操作对应的·ODR寄存器,来实现开灯与关灯,但是这样不利于后期维护代码,可能过个一两周,你回来看自己的代码都看不明白了,所以我们采用宏定义来对这个开灯关灯操作做一个封装,我这里有两种方式,两个灯用了不同的方式,大家根据自己的喜好来就行。

直接分开宏定义两个

第一种方式就是直接分别宏定义一个LEDn_ON,与一个LEDn_OFF,具体写法如下:

// An highlighted block
#define LED_1_ON GPIOA->ODR &= ~(1<<6)//置零拉低对应端口,LED1灯亮
#define LED_1_OFF GPIOA->ODR |= (1<<6)//置1拉高对应端口,LED1灯灭
//这个比较好理解,直接对对应端口的控制位写零写一。

使用条件运算符

第二种方式,使用条件运算符来实现,宏定义的时候定义为LED_n(x) ;当x非0的时候执行(GPIOA->ODR &=~(1<<7);将GPIO对应的控制位置0,拉低IO口,小灯点亮;当x=0时,执行(GPIOA->ODR |=1<<7)将对应位拉高,小灯熄灭。

#define  LED_2(x) (x)?(GPIOA->ODR &=~(1<<7):(GPIOA->ODR |=1<<7)) 

宏定义应该放在那个位置呢,这个我们上一篇中提到过哈,在我们写头文件的时候还专门区分了一个区域用来存放宏定义的。此时的宏定义就放到这。
在这里插入图片描述

然后将上面的两个封装好的宏进行调用进初始化,使LED默认熄灭。
在这里插入图片描述
编译烧录后,可以发现LED上电后默认是熄灭的。
在这里插入图片描述

封装函数实现简单的功能

经过上面的操作,已经将LED的开启与关闭做了封装,接下来,就是做对应功能的封装,类似跑马灯、流水灯,闪烁之类的操作。这类操作,一般都是采用一个功能函数进行封装而不是直接码在while(1)里面的。
这里实现一个流水灯吧。查一个小技巧,如果我们有一个函数代码写的很长了,往后翻比较麻烦,可以在代码任意位置右键------>选择1的位置,---------->点击一下2的位置。
在这里插入图片描述
就会出现左侧的折叠符号,点击就可以将整个函数进行折叠。
在这里插入图片描述

//第一步,写注释
/***********************************************
*函数名    :Led_Flow
*函数功能  :实现一个简单的流水灯(非阻塞)
*函数参数  :u8 delay
*函数返回值:void
*函数描述  :灯1亮 灯1灭 灯2亮  灯2灭灯1亮(非阻塞)
delay 用来控制流水灯的速度。
*********************************************/
//第二步写函数,由于是非阻塞的流水灯,所以是不能使用while(i--)的那种死等的延时的。
void Led_Flow(u8 delay)
{
	static u8  n=1;
	static u32 cnt=0;
	if(n==1){LED_1_ON;}
	if(n==2){LED_2(1);}

	cnt++; 
	//延时切换灯
	if(cnt>50000*delay)  //不精准延时
	{
		if(n==1)
		{LED_1_OFF; }
		if(n==2)
		{
			LED_2(0);
		}
		n++;    //往后切灯
		if(n>2)
		{
		  n=1;	
		}
		cnt=0;
	}
}

然后函数声明,在主函数调用;
在这里插入图片描述
主函数调用,由于设置了一个可以调速的delay形参,因此在主函数中需要摄者一个Led_Speed来传递参数。
在这里插入图片描述
然后编译下载,可以实现如下图效果:
在这里插入图片描述

KEIL MDK一些技巧

代码左侧一直出现小红叉

如果出现代码右侧一直报小红叉,但是编译又没有问题的情况,如下图所示,这是因为KEIL默认开启了Dynamic Syntax Checking(动态语法检查的原因)看着十分恼火,这里我们手动关闭一下。
在这里插入图片描述
步骤:
1.点击下图所得扳手
在这里插入图片描述
2.在弹出的框中选择Text Completion ----------->然后把Dynamic Syntax Checking下方的复选框给取消勾选就可以了。
在弹出的框中选择

2.同一个工程,别人能编译通过,自己不行

出现这类问题极有可能是自己的版本和其他人的版本不一致,KEIL的新版有可能连一些官方例程都没法编译通过。遇到这种问题一般双击下面的报错选项都会跳转到一些官方文件显示一些东西未定义
这是由于编译器版本太新造成的。
类似如下的报错:
…/CMSIS/Include\core_cmFunc.h(614): error: unknown register name ‘vfpcc’ in asm
__ASM volatile (“VMSR fpscr, %0” : : “r” (fpscr) : “vfpcc”);
在这里插入图片描述
解决方案:点击1位置的魔法棒,然后选择2的Target,再然后选择3位置的编译器版本为5,然后重新编译即可。
在这里插入图片描述

ST-LINK烧录后总是要手动按复位

这个也是可以设置成让其自动帮我们完成复位操作的,也是点击魔法棒,然后选择Debug,注意要选中ST-LINK再点击settings,在弹出的框上将4号框的复选框选中即可。
在这里插入图片描述

修改字体大小及字体

Keil MDK5修改配色和字体以及字号的方法http://t.csdn.cn/MPqpH

定位代码错误

第一类,…\USER\main.c(6): error: #65: expected a “;”
下方提示是小的errors,这种错误直接点击errors会跳转到对应位置,在跳转的位置仔细寻找自己的语法错误即可,有时候定位和报的错误类型不一定准,但一般都是在定位附近查找即可,如下图,报错是说在main.c的第六行是少了一个分号,实际是第六行的赋值运算符多写了一个=号。
在这里插入图片描述
第二类,.\Objects\TEST1.axf: Error: L6218E: Undefined symbol Led_Flow (referred from main.o).
报错是Errors,E是大写的,这种错误一般都是没有定义,或者重复定义,或者是没有将对应的.c文件添加到工程引起的报错。
在这里插入图片描述
他也会指出地方和报错的内容,下图的报错,说的就是,在main.o中没有找到Led_Flow这个函数,这里的main.o实际就是main.c;整个.C的编译过程如下图所示:
在这里插入图片描述
重新添加Led.c进入工程后即可解决报错。
在这里插入图片描述

M4系列目录

1.嵌入式学习笔记——概述
2.嵌入式学习笔记——基于Cortex-M的单片机介绍
3.嵌入式学习笔记——STM32单片机开发前的准备
4.嵌入式学习笔记——STM32硬件基础知识
5.嵌入式学习笔记——认识STM32的 GPIO口
6.嵌入式学习笔记——使用寄存器编程操作GPIO
7.嵌入式学习笔记——寄存器实现控制LED小灯
8.嵌入式学习笔记——使用寄存器编程实现按键输入功能
9.嵌入式学习笔记——STM32的USART通信概述
10.嵌入式学习笔记——STM32的USART相关寄存器介绍及其配置
11.嵌入式学习笔记——STM32的USART收发字符串及串口中断
12.嵌入式学习笔记——STM32的中断控制体系
13.嵌入式学习笔记——STM32寄存器编程实现外部中断
14.嵌入式学习笔记——STM32的时钟树
15.嵌入式学习笔记——SysTick(系统滴答)
16.嵌入式学习笔记——M4的基本定时器
17.嵌入式学习笔记——通用定时器
18.嵌入式学习笔记——PWM与输入捕获(上)
19.嵌入式学习笔记——PWM与输入捕获(下)
20.嵌入式学习笔记——ADC模数转换器
21.嵌入式学习笔记——DMA
22.嵌入式学习笔记——SPI通信
23.嵌入式学习笔记——SPI通信的应用
24嵌入式学习笔记——IIC通信

实验一 熟悉实验开发环境及GPIO编程 结构合理、条理清晰的程序结构,有助于提高程序的可移植性与可复用性,有利于程序 的维护。学习嵌入式软件编程,从一开始就养成规范编程的习惯,将为未来发展打下踏实基 础。这是第一个实验,目的是以通用输入输出为例,达到熟悉实验开发环境、理解规范编程 结构、掌握基本调试方法等目的。 一、实验目的 本实验通过编程控制 LED 小灯,体会 GPIO 输出作用,可扩展控制蜂鸣器、继电器等; 通过编程获取引脚状态,体会 GPIO 输入作用,可用于获取开关的状态。主要目的如下: (1)了解集成开发环境的安装与基本使用方法。 (2)掌握 GPIO 构件基本应用方法,理解第一个 C 程序框架结构,了解汇编语言与 C 语言的如何相互调用。 (3)掌握硬件系统的软件测试方法,初步理解 printf 输出调试的基本方法。 二、实验准备 (1)硬件部分。PC 机或笔记本电脑一台、开发套件一套。 (2)软件部分。根据网上光盘“..\02-Doc”文件夹下的电子版快速指南,下载合适的电 子资源。 (3)软件环境。按照电子版快速指南中“安装软件开发环境”一节,进行有关软件工 具的安装。 三、参考样例 (1)“.. \04-Soft\CH04-GPIO\GPIO-Output-DirectAddress)”。该程序使用直接地址编程方 式,点亮一个发光二极管。从中可了解到,模块的哪个寄存器的哪一位变化使得发光二极管 亮了,由此理解硬件是如何干预软件的。但这个程序不作为标准应用编程模板,因为要真正 进行规范的嵌入式软件编程,必须封装底层驱动构件,在此基础上进行嵌入式软件开发。 (2)“.. \04-Soft\CH04-GPIO\GPIO-Output-Component”。该程序通过调用 GPIO 驱动构 件方式,使得一个发光二极管闪烁。使用构件方式编程干预硬件是今后编程的基本方式。而 使用直接地址编程方式干预硬件,仅用于底层驱动构件制作过程中的第一阶段(打通硬件), 为构件封装做准备。 四、GPIO知识要素与构件接口 1.GPIO 知识要素 通用输入输出(General Purpose Input/Output,GPIO),是 I/O 最基本形式,是几乎所有 计算机均使用到的部件。通俗地说,GPIO 是开关量输入输出的简化名称。而开关量是指逻 辑上具有 1 和 0 两种状态的物理量。开关量输出,可以是指在电路中控制电器的开和关,也 可以是指控制灯的亮和暗,还可以是指闸门的开和闭等等。开关量输入,开关量输出,可以 是指获取电路中电器开关状态,也可以是指获取灯的亮暗状态,还可以是指获取闸门开关状 态等等。 GPIO 硬件部分的主要知识要素有:GPIO 的含义与作用、输出引脚外部电路的基本接 法及输入引脚外部电路的基本接法等。 1)GPIO 的含义与作用 从物理角度,GPIO 只有高电平与低电平两种状态。从逻辑角度,GPIO 只有“1”和“0” 两种取值。在使用正逻辑情况下,电源(Vcc)代表高电平,对应数字信号“1”;地(GND) 代表低电平,对应数字信号“0”。作为通用输入引脚,计算机内部程序可以获取该引脚状 态,以确定该引脚是“1”(高电平)或“0”(低电平),即开关量输入。作为通用输出引 脚,计算机内部程序可以控制该引脚状态,使得引脚输出“1”(高电平)或“0”(低电平), 即开关量输出。 GPIO 的输出是以计算机内部程序通过单个引脚控制着开关量设备,达到自动控制开关 状态之目的。GPIO 的输入是以计算机内部程序获取单个引脚状态,达到获得外界开关状态 之目的。 特别说明:注意在不同电路中,逻辑“1”对应的物理电平不同。在 5V 供电的系统中, 逻辑“1”的特征物理电平为5V;在3.3V供电的系统中,逻辑“1”的特征物理电平为3.3V。 因此,高电平的实际大小取决于具体电路。 2)输出引脚外部电路的基本接法 作为通用输出引脚,计算机内部程序向该引脚 输出高电平或低电平来驱动器件工作,即开关量输 出。如图 1-1 所示,输出引脚 O1 和 O2 采用了不同 的方式驱动外部器件。一种接法是 O1 直接驱动发光 二极管 LED,当 O1 引脚输出高电平时,LED 不亮; 当 O1 引脚输出低电平时,LED 点亮。这种接法的驱 动电流一般在 2mA~10mA。另一种接法是 O2 通过 一个 NPN 三极管驱动蜂鸣器,当 O2 引脚输出高电 平时,三极管导通,蜂鸣器响;当 O2 引脚输出低电 平时,三极管截止,蜂鸣器不响。这种接法可以用 O2 引脚上的几个 mA 的控制电流驱动高达 100mA 的驱 动电流。若负载需要更大的驱动电流,就必须采用光电隔离外加其他驱动电路,但对计算机 编
最新发布
03-16
<think>好的,我需要回答用户关于嵌入式系统GPIO编程、实验开发环境、C语言与汇编语言交互以及嵌入式软件基础的问题。首先,我应该从GPIO编程的基础开始,解释GPIO的概念和基本操作。然后介绍实验开发环境的搭建,包括必要的工具链和硬件设备。接下来,需要详细说明C语言和汇编语言在嵌入式系统中的交互方式,比如内联汇编和函数调用约定。最后,总结嵌入式软件编程的基础知识,强调可移植性和底层操作的重要性。同时,我要确保所有数学公式和代码块符合格式要求,添加相关引用,并生成相关问题。需要检查引用是否准确,内容是否清晰易懂,避免使用复杂术语,保持口语化。最后,确保生成的问题与主题紧密相关,帮助用户深入学习。</think>### 嵌入式系统GPIO编程基础 #### 1. GPIO编程原理 GPIO(通用输入输出)是嵌入式系统中控制硬件设备的接口,通过寄存器操作实现电平控制。例如,设置GPIO方向寄存器的第3位为1表示配置为输出模式: $$ \text{GPIO\_DIR} |= (1 << 3) $$ 写入数据寄存器控制引脚电平[^2]。 #### 2. 实验开发环境搭建 典型开发环境包含: - **硬件**:开发板(如基于6818芯片的板卡)、调试器(J-Link)、外设模块 - **软件**:交叉编译工具链(如arm-linux-gcc)、IDE(Keil/IAR)、调试工具(OpenOCD) - **示例工具链配置**: ```bash arm-linux-gcc -o led_test led.c # 交叉编译 scp led_test root@192.168.1.100:/app # 部署到开发板 ``` #### 3. C与汇编交互实现嵌入式开发中,两种常用交互方式: **内联汇编**: ```c void delay_ms(uint32_t ms) { __asm__ volatile( "mov r0, %0\n" "1: subs r0, #1\n" "bne 1b" : : "r"(ms*1000) : "r0" ); } ``` **独立汇编模块**: ```armasm @ file: enable_irq.s .global EnableIRQ EnableIRQ: cpsie i bx lr ``` #### 4. 嵌入式软件基础要点 - **内存管理**:直接操作物理地址(如`*(volatile uint32_t *)0x12345678 = 0x1;`) - **中断处理**:通过向量表注册ISR(中断服务例程) - **外设驱动**:遵循"初始化->配置->操作"流程 - **低功耗设计**:合理使用WFI/WFE指令 [^1]: C语言的可移植性使同一代码可适配不同硬件平台,这是嵌入式开发的核心优势 : GPIO编程需直接操作芯片手册中规定的寄存器地址
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值