while(1)逻辑块中的代码是例程Led_Blink.c的功能主体:
先给PC13脚输出高电平,由赋值语句GPIO_SetBits(GPIOC,GPIO_Pin_13)完成,然后调用延时函数delay_nms(500)等待500ms,再给PC13脚输出低电平,即GPIO_ResetBits(GPIOC,GPIO_Pin_13),然后再次调用延时500ms函数delay_nms(500)。这样就完成了一次闪烁。
在程序中,你没有看到PC13:GPIOC和GPIO_Pin_13的定义,它们已经在固件函数标准库(stm32f10x_map.h和stm32f10x_gpio.h)中定义好了,由头文件stm32f10x_heads.h包括进来。回想一下,用Keil开发51单片机程序时,也是一样的。后续章节中将要用到的其他引脚名称和定义都是如此。
GPIO_SetBits和GPIO_ResetBits这两个函数在stm32f10x_gpio.c中实现,后面将作介绍。
时序图简介
时序图反应的是高、低电压信号与时间的关系图。在图2.6中,时间从左到右增长,高、低电压信号随着时间在低电平或高电平之间变化。这个时序图显示的是刚才实验中的1000ms的高、低电压信号片段。右边的省略号表示的是这些信号是重复出现的。
微控制器的最大优点之一就是它们从来不会抱怨不停地重复做同样的事情。为了让单片机不断闪烁,你需要把让LED闪烁一次的几个语句放在while(1){…}循环里。这里用到了C语言实现循环结构的一种形式:
while(表达式)循环体语句
当表达式为非0值时,执行while语句中的内嵌语句,其特点是先判断表达式,后执行语句。例程中直接用1代替了表达式,因此总是非0值,所以循环永不结束,也就可以一直让LED灯闪烁。
注意: 循环体语句如果包含一个以上的语句,就必须用花括号(“{}”)括起来,以复合语句的形式出现。如果不加花括号,则while语句的范围只到while后面的第一个分号处。例如,本例中while语句中如果没有花括号,则while语句范只到“GPIO_SetBits(GPIOC,GPIO_Pin_13);”。
也可以不要循环体语句,如第一章例程中就直接用while(1);程序将一直停在此处。
图2.6 程序Led_Blink.c的时序图
STM32系列单片机的I/O端口模式
STM32系列单片机的输入/输出引脚可配置成以下8种(4输入+2输出+2复用输出):
① 浮空输入:In_Floating。
② 带上拉输入:IPU(In Push-Up)。
③ 带下拉输入:IPD(In Push-Down)。
④ 模拟输入:AIN(Analog In)。
⑤ 开漏输出:OUT_OD。OD代表开漏:Open-Drain。(OC代表开集:Open-Collector)。⑥ 推挽输出:OUT_PP。PP代表推挽式:Push-Pull。
⑦ 复用功能的推挽输出:AF_PP。AF代表复用功能:Alternate-Function。
⑧ 复用功能的开漏输出:AF_OD。
开漏输出与推挽输出
开漏输出:MOS管漏极开路。要得到高电平状态需要上拉电阻才行。一般用于线或、线与,适合做电流型的驱动,其吸收电流的能力相对强(一般20mA以内)。开漏是对MOS管而言,开集是对双极型管而言,在用法上没区别,开漏输出端相当于三极管的集电极。如果开漏引脚不连接外部的上拉电阻,则只能输出低电平。因此,对于经典的51单片机的P0口,要想做输入/输出功能必须加外部上拉电阻,否则无法输出高电平逻辑。一般来说,可以利用上拉电阻接不同的电压,改变传输电平,以连接不同电平(3.3V或5V)的器件或系统,这样你就可以进行任意电平的转换了。
推挽输出:如果输出级的两个参数相同MOS管(或三极管)受两互补信号的控制,始终处于一个导通、一个截止的状态,就是推挽相连,这种结构称为推拉式电路。推挽输出电路输出高电平或低电平时,两个MOS管交替工作,可以减低功耗,并提高每个管的承受能力。又由于不论走哪一路,管子导通电阻都很小,使RC常数很小,逻辑电平转变速度很快,因此,推拉式输出既可以提高电路的负载能力,又能提高开关速度,且导通损耗小效率高。输出既可以向负载灌电流(作为输出),也可以从负载抽取电流(作为输入)。
下面我们来看看通用GPIO(General Purpose Input Output)端口的初始化配置函数:GPIO_Configuration。在文件“stm32f10x_gpio.h”中定义了:
其中,函数GPIO_Init的具体实现在库文件“stm32f10x_gpio.c”中(\library\src目录下)。其作用是定义各个通用I/O端口的模式。
从上面的程序代码可以看出,对应到外设的输入/输出功能有以下三种情况:
(1)外设对应的引脚为输入:则根据外围电路的配置可以选择浮空输入、带上拉输入或带下拉输入。
(2)ADC对应的引脚:配置引脚为模拟输入。
(3)外设对应的引脚为输出:需要根据外围电路的配置选择对应的引脚为复用功能的推挽输出或复用功能的开漏输出。如果把端口配置成复用输出功能,则引脚和输出寄存器断开,并和片上外设的输出信号连接。将引脚配置成复用输出功能后,如果外设没有被激活,那么它的输出将不确定。
当GPIO口设为输入模式时,输出驱动电路与端口是断开,此时输出速度配置无意义,不用配置。在复位期间和刚复位后,复用功能未开启,I/O端口被配置成浮空输入模式。所有端口都有外部中断能力。为了使用外部中断线,端口必须配置成输入模式。
当GPIO口设为输出模式时,有3种输出速度可选(2MHz、10MHz和50MHz),这个速度是指I/O口驱动电路的响应速度而不是输出信号的速度,输出信号的速度与程序有关(芯片内部在I/O口的输出部分安排了多个响应速度不同的输出驱动电路,可以根据需要选择合适的驱动电路)。通过选择速度来选择不同的输出驱动模块,达到最佳的噪声控制和降低功耗的目的。高频的驱动电路,噪声也高,当不需要高的输出频率时,请选用低频驱动电路,这样非常有利于提高系统的电磁干扰(EMI)性能。当然如果要输出较高频率的信号,但却选用了较低频率的驱动模块,很可能会得到失真的输出信号。关键是GPIO的引脚速度跟应用匹配(推荐10倍以上)。
对于串口,假如最大波特率只需115.2K,那么用2M的GPIO的引脚速度就够了,既省电也噪声小;对于I 2 C接口,假如使用400K传输速率,若想把余量留大些,那么用2M的GPIO的引脚速度或许不够,这时可以选用10M的GPIO引脚速度;对于SPI接口,假如使用18M或9M传输速率,用10M的GPIO的引脚速度显然不够了,需要选用50M的GPIO的引脚速度。
电磁干扰
EMI:电磁干扰(Electromagnetic Interference)是指电磁波与电子元件作用后产生的干扰现象,分为传导干扰和辐射干扰。传导干扰是指通过导电介质把一个电网络上的信号耦合(干扰)到另一个电网络。辐射干扰是指干扰源通过空间把其信号耦合(干扰)到另一个电网络。在高速系统中,高频信号线、集成电路的引脚、各类接插件等都可能成为具有天线特性的辐射干扰源,能发射电磁波并影响其他系统或本系统内其他子系统的正常工作。
由此可见,STM32系列单片机的GPIO功能很强大,具有以下功能:
(1)最基本的功能是可以驱动LED、产生PWM、驱动蜂鸣器等;
(2)具有单独的位设置或位清除,编程简单。端口配置好以后只需GPIO_SetBits(GPIOx,GPIO_Pin_x)就可以实现对GPIOx的pinx位为高电平,GPIO_ResetBits(GPIOx,GPIO_Pin_x)就可以实现对GPIOx的pinx位为低电平;
(3)具有外部中断/唤醒能力,端口配置成输入模式时,具有外部中断能力;
(4)具有复用功能,复用功能的端口兼有I/O功能等;
(5)软件重新映射I/O复用功能:为了使不同器件封装的外设I/O功能的数量达到最优,可以把一些复用功能重新映射到其他一些脚上。这可以通过软件配置相应的寄存器来完成。这时,复用功能就不再映射到它们的原始引脚上了;
(6)GPIO口的配置具有锁定机制,当配置好GPIO口后,在一个端口位上执行了锁定(LOCK),可以通过程序锁住配置组合,在下一次复位之前,将不能再更改端口位的配置。
STM32系列单片机的每个GPIO端口有两个32位配置寄存器(GPIOx_CRL,GPIOx_CRH),两个32位数据寄存器(GPIOx_IDR,GPIOx_ODR),一个32位置位/复位寄存器(GPIOx_BSRR),一个16位复位寄存器(GPIOx_BRR)和一个32位锁定寄存器(GPIOx_LCKR)。GPIO 端口的每个位可以由软件分别配置成多种模式。每个I/O 端口位可以自由编程。
注意: I/O 端口寄存器必须按32位字被访(不允许半字或字节访问)。GPIOx_BSRR和GPIOx_BRR寄存器允许对任何GPIO寄存器的读/写的独立访问。定义这些GPIO寄存器组的结构体是GPIO_TypeDef,在库文件“stm32f10x_map.h”中:
函数GPIO_Init的第一个参数是这些GPIOx(x=A,B,C,D,E)寄存器的存储映射首地址,第二个参数是用户对GPIO端口设置的参数所在首地址,这些数据存放在结构体GPIO_InitTypeDef中,包括所要设置GPIO的端口号,类型和速度。STM32单片机使用固件库函数完成外设(如GPIO、TIM、USART、ADC、DMA、RTC等)初始化都采用这种规范,如图2.7所示,这种固件库结构大大提高了程序的开发效率。
图2.7 使用固件库函数完成外设初始化示意图
从上面的宏定义可以看出,GPIOx(x=A,B,C,D,E)寄存器的存储映射首地址分别是0x40010800,0x40010C00,0x40011000,0x40011400,0x40011800;AFIO寄存器的存储映射首地址是0x40010000。GPIO和AFIO寄存器映像和复位值如表2.2和2.3所示。
表2.2 GPIO寄存器映像和复位值
表2.3 AFIO寄存器映像和复位值
下面我们来看看“stm32f10x_gpio.c”文件中的GPIO_SetBits和GPIO_ResetBits等对STM32单片机I/O口操作的几个函数:
从上面的程序,我们可以看出GPIO_SetBits和GPIO_ResetBits函数实际上是直接访问了GPIO的相关寄存器,对I/O端口的对应位置“1”或清“0”,使引脚输出高电平或低电平。我们可以利用这些STM32固件库的函数来开发自己的应用程序,当然也可以不使用固件库,而直接对寄存器访问来编写应用程序,这需要你对STM32单片机寄存器的各个位的含义和使用十分熟悉。不使用固件库的发光二极管闪烁程序见本书配套例程。你可以对比一下代码尺寸的变化和程序运行的结果。
对于开发一个系统级的产品而言,建议使用STM32固件库。它具有以下特性:
● 兼容性好:使用宏定义能够灵活地兼容各个型号和不同功能;
● 命名规范:不用注释就能看懂变量或函数,可读性好,而且不会重名;
● 通用性强:多数库文件都是只读类型,不用修改便可实现不同功能间的调用。
使用固件库也有缺点,如运行性能有所损失,速度会变慢些。对于越来越复杂的嵌入式应用而言,随着处理器存储容量和频率的提高,笔者建议应该更关注项目的整体开发效率,而不是具体的代码尺寸。对于时序要求严格的地方,完全可以直接访问寄存器减小代码尺寸,根据实际开发设计的要求来确定。同时,使用固件库进行程序开发,借鉴ST固件库函数的命名规范,有助于养成良好的编码风格,学以致用,提高编程水平,正如本书前言所述。关于STM32单片机固件库的介绍参见附录。
什么是assert(断言)
编写代码时,我们总是会做出一些假设,断言就是用于在代码中检测这些假设是否成立,比如,向一个端口写数据,如果这个端口不存在,则不能向一个端口写数据。例如,向GPIO_Pin_0端口写是合法的,而向GPIO_Pin_20端口写就是非法的,因为STM32单片机根本不存在这个端口。
可以将断言看做是程序异常处理的一种高级形式。断言表示为一些布尔表达式,如果在程序的某个特定点要判断某个表达式值为真,则可以进行断言验证。可以在任何时候启用和禁用断言验证。一般我们会让断言语句在编译Debug版本的程序时生效,而在编译Release版本的程序时禁止。同样,最终用户在运行程序遇到问题时可以重新启用断言。使用断言可以创建更稳定,优秀且不易于出错的代码。当需要在一个值为FALSE时中断当前操作的话,可以使用断言。单元测试必须使用断言(Junit/JunitX)。除了类型检查和单元测试外,断言还提供了一种确定各种特性是否在程序中得到维护的极好的方法。
assert宏的原型定义在<assert.h>中,其作用是如果它的条件返回错误,则终止程序执行,原型定义:void assert(int expression);
assert的作用是现计算表达式expression,如果其值为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用 abort 来终止程序运行。
使用assert的缺点是:频繁的调用会极大的影响程序的性能,增加额外的开销。在调试结束后,可以通过如下代码来禁用assert调用:
一般地,STM32系列单片机中配置片内外设使用的通用I/O端口需经过一下几步设置:
(1)配置输入的时钟。
使能APB2总线外设时钟:RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOC,ENABLE)。释放GPIO复位:RCC_APB2PeriphResetCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOC,DISABLE)。
(2)初始化后即被激活(开启)。
(3)如果使用该外设的输入/输出引脚,则需要配置相应的GPIO端口(否则该外设对应的输入/输出引脚可以做普通GPIO引脚使用)。
(4)配置各个PIN端口的模式和速度。
(5)GPIO初始化。
本书所用STM32单片机教学开发板的各个I/O口配置如下:
(1)PA9和PA10是串口1的发送和接收引脚(电路板设计)。
(2)PB8、PB9、PC12、PC13是输出控制发光二极管(电路板设计)。
(3)PD7、PD8、PD9、PD10是输出控制电机(电路板设计)。
(4)PA0、PB0是AD输入引脚,PA4是AD输入引脚或DA输出引脚(电路板设计)。
(5)PC8、PC9、PC10、PC11是按键输入引脚(电路板设计)。
(6)PC0~PC7、PD4、PD5、PD6是输出控制1602液晶(电路板设计)。
(7)PE0、PE1定义成输出引脚。
(8)PE2、PE3定义成输入引脚。
(9)PE4、PE5定义成是输入引脚。
其中,PE口的16个I/O端口并未设计具体电路,开放出来了,你可在面包板上自行搭建电路或制作一个扩展板。
注意: PE4、PE5在函数NVIC_Configuration中,定义成了外部中断输入。那些还没有设置的引脚,你可以参照上述方法设置。
串口初始化函数USART_Configuration,在头文件HelloRobot.h中实现,具体内容将在后面章节讲解。调用printf是为了在程序执行前给调试终端发送一条提示信息,告诉你现在程序开始执行了,并告诉你随后程序将开始干什么。这在你以后的编程开发过程中是一个良好的习惯,将非常有助于你提高程序的调试效率。