当前位置:首页 >> 信息与通信 >>

Stm32初学者全攻略


Stm32 学习文档
1 软件安装和 J-LINK 的安装和设置
1.1keil 软件的安装
1)安装KEIL 3.80A. 待安装完成后,在桌面出现如下图标(Keil uVision3):

2)双击该图标,出现MDK 的启动界面,默认打开了一个示例工程.得到如下界面:

3)选择:File->License Management.弹出如下对话框:

复制右边 Computer ID 下面的CID 号. 4)运行:注册.exe Target 选择:ARM 第三个对话框选择:RealView MDK Professional 复制CID 到CID 对话框,点解Generate.得到LIC.如下图所示:

5)复制LIC 到MDK License Management 对话框的New License ID Code(LIC)框下,点击 Add LIC.如下图所示:

可以看到下面的框提示LIC Added Sucessfully.从红色圈里面可以知道,这次注册可以一直用 到2020 年8 月. 6)完成注册

1.2j-link 的安装和设置
1)安装驱动 Setup_JLinkARM_V408l,一路点确认即可 2)keil 的设置 点击图标

选择 debug 选项卡

选择 utilities 选项卡,其设置和 debug 相同。 3)下载程序是点击 download 即可 注意:如果有时出现有时候不出现的问题 rebuilt all 即可。

2 通过简单程序学习各部分内容
2.1 如何新建一个工程
以下简要介绍如何建立一个可以通过编译的工程 新建一个文件夹,名为 GPIOPro 1)文件夹下面再建立三个子文件夹,名为 StartUpCode\User\FWlib

User 文件夹下面再建立两个子文件夹 Lis\Obj

2)从“新建工程相关”目录下拷贝 core_cm3.c\core_cm3.h\startup_stm32f10x_cl.s\stm32f10x.h 文件到新建工程的 StartUpCode 文件夹中

从 “ 新 建 工 程 相 关 ” 拷 贝 stm32f10x_it.c\stm32f10x_it.h\main.c\system_stm32f10x.c\ system_stm32f10x. h \stm32f10x_conf.h 到新建工程的 User 文件夹下面

从“新建工程相关”拷贝 inc\src 两个文件夹到新建工程的 FWlib 文件夹里面

3)打开 keil 软件点击 Project—new Project

打开选择 Project 下拉菜单中的 New uVision Project 创建自己的工程名然后点保存(保存到刚才新建的工程目录下面的 User 文件夹下面)

选择 MINI-STM32 开发板 CPU 型号

是否添加默认的启动文件到工程文件,选择否

4)点击下图的按钮,为使得工程条理清晰,在里面设置好文件层次

参照上图加入文件 5)点击下图所示按钮, 进行工程设置选择创建 16 进制文件,如图所示

选择 ”Select Folder For Objects”,选择之前创建的 Obj 文件夹

同上法设置 Lis 文件夹目录

设置头文件索引目录

把工程里面包含有头文件.h 的目录都添加进来

修改 main.c 函数的相应内容.并且编译. 在 Define 项输入 USE_STDPERIPH_DRIVER, STM32F10X_HD

2.2IO 口使用
步骤 1:配置时钟 配置时钟有三个相关的常用函数

RCC_AHBPeriphClockCmd RCC_APB2PeriphClockCmd
RCC_APB1PeriphClockCmd

这三个函数的区别可以理解为对不同的对象配置时钟
步骤 2: 端口初始化

void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct) 初始化端口 其中, typedef struct { u16 GPIO_Pin; GPIOSpeed_TypeDef GPIO_Speed;
GPIOMode_TypeDef GPIO_Mode; } GPIO_InitTypeDef;

GPIO_Mode_AF_PP 复用推挽输出 GPIO_Mode_Out_PP 推挽输出 GPIO_Mode_IPU 上拉输入 GPIO_Mode_IPD 下拉输入 GPIO_Mode_IN_FLOATING 浮空输入 GPIO_Mode_AIN 模拟输入 步骤 3:调用读取或写入的函数 GPIO_SetBits(GPIOC, GPIO_Pin_6);//把某个端口置位 GPIO_ResetBits(GPIOC, GPIO_Pin_6);//把某个端口置零 GPIO_ReadOutputDataBit(GPIOB,GPIO_Pin_5);//把某个端口输出的状态读进来 GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_5)//把某个端口的值读出来 单片机的内部可靠性高,但外部的可靠性就就比较低了,尤其是输入信号时经常受到外界干扰或机械性能的限 制。要提高按键输入可靠性,我们就必须要解决按键开关的抖动问题。由于按键是机械触点,当机械触点断开、闭 合时,会有抖动,输入端的波形这种抖动对于人来说是感觉不到的,但对单片机来说,则是完全可以感应到的,因 为单片机处理的速度是在微秒级,而机械抖动的时间至少是毫秒级,这已是 一个“漫长”的时间了。这样有可能 导致被执行的部分多次执行。

这段程序是防抖之后的: if(!GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_5)) { Delay(0x15); if(!GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_5)) { GPIO_SetBits(GPIOC, GPIO_Pin_6);// D1 亮 Delay(0x10FFFF); GPIO_ResetBits(GPIOC, GPIO_Pin_6); } } //D1 灭 //S1

2.3 中断的使用 2.3.1 基本概念
STM32 目前支持的中断共 84 个(16 个内部+68 个外部) ,还有 16 级可编程的中断优先级的设置,仅使用中断 优先级设置 8bit 中的高 4 位。

STM32 可支持 68 个中断通道,已经固定分配给相应的外部设备,每个中断通道都具备自己的中断优先级控制 字节 PRI_n(8 位,但是 STM32 中只使用 4 位,高 4 位有效),每 4 个通道的 8 位中断优先级控制字构成一个 32 位的 优先级寄存器。68 个通道的优先级控制字至少构成 17 个 32 位的优先级寄存器。 4bit 的中断优先级可以分成 2 组, 从高位看, 前面定义的是抢占式优先级, 后面是响应优先级。 按照这种分组, 4bit 一共可以分成 5 组 第 0 组:所有 4bit 用于指定响应优先级; 第 1 组:最高 1 位用于指定抢占式优先级,后面 3 位用于指定响应优先级; 第 2 组:最高 2 位用于指定抢占式优先级,后面 2 位用于指定响应优先级; 第 3 组:最高 3 位用于指定抢占式优先级,后面 1 位用于指定响应优先级; 第 4 组:所有 4 位用于指定抢占式优先级。 所谓抢占式优先级和响应优先级,他们之间的关系是:具有高抢占式优先级的中断可以在具有低抢占式优先级 的中断处理过程中被响应,即中断嵌套。 当两个中断源的抢占式优先级相同时,这两个中断将没有嵌套关系,当一个中断到来后,如果正在处理另一个 中断,这个后到来的中断就要等到前一个中断处理完之后才能被处理。如果这两个中断同时到达,则中断控制器根 据他们的响应优先级高低来决定先处理哪一个;如果他们的抢占式优先级和响应优先级都相等,则根据他们在中断 表中的排位顺序决定先处理哪一个。每一个中断源都必须定义 2 个优先级。 有几点需要注意的是: 1)如果指定的抢占式优先级别或响应优先级别超出了选定的优先级分组所限定的范围,将可能得到意想不到的结 果; 2)抢占式优先级别相同的中断源之间没有嵌套关系; 3)如果某个中断源被指定为某个抢占式优先级别,又没有其它中断源处于同一个抢占式优先级别,则可以为这个 中断源指定任意有效的响应优先级别。

2.3.2 GPIO 外部中断
STM32 中,每一个 GPIO 都可以触发一个外部中断,但是,GPIO 的中断是以组位一个单位的,同组间的外部中 断同一时间只能使用一个。比如说,PA0,PB0,PC0,PD0,PE0,PF0,PG0 这些为 1 组,如果我们使用 PA0 作为外 部中断源,那么别的就不能够再使用了,在此情况下,我们智能使用类似于 PB1,PC2 这种末端序号不同的外部中 断源。每一组使用一个中断标志 EXTIx。EXTI0 – EXTI4 这 5 个外部中断有着自己的单独的中断响应函数,EXTI5-9 共 用一个中断响应函数,EXTI10-15 共用一个中断响应函数。 对于中断的控制,STM32 有一个专用的管理机构:NVIC。中断的使能,挂起,优先级,活动等等部都是 NVIC 在管理的。

2.3.3 程序开发
其实上面那些基本概念和知识只是对 STM32 的中断系统有一个大概的认识, 用程序说话将会更能够加深如何使 用中断。使用外部中断的基本步骤如下: 1)设置好相应的时钟; 2)设置相应的中断; 3)IO 口初始化; 4)把相应的 IO 口设置为中断线路(要在设置外部中断之前)并初始化; 5)在选择的中断通道的响应函数中中断函数。 在我们这块板,可以使用按键来触发相应的中断。根据原理图,s1/s2/s3/s4 连接的是 PE5/PE4/PE3/PE2,因此我将 用 EXTI5/EXTI4/EXTI3/EXTI2 四个外部中断。PC6/PC7/PD13/PD6 分别连接了三个 LED 灯。中断的效果是按下按键,相 应的 LED 灯将会被点亮。

1)设置相应的时钟 首先需要打开 GPIOD、GPIOC 和 GPIOE。然后由于是要用于触发中断,所以还需要打开 GPIO 复用的时钟。详细 代码如下: void RCC_cfg() { //打开 PE PD PC 端口时钟,并且打开复用时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE RCC_APB2Periph_AFIO, ENABLE); } 设置相应的时钟所需要的 RCC 函数在 stm32f10x_rcc.c 中,所以要在工程中添加此文件。 2) 设置好相应的中断 设置相应的中断实际上就是设置 NVIC,在 STM32 的固件库中有一个结构体 NVIC_InitTypeDef,里面有相应的标 志位设置,然后再用 NVIC_Init()函数进行初始化。详细代码如下: void NVIC_cfg() { NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitStructure.NVIC_IRQChannel = EXTI3_IRQn; //选择中断分组 2 //选择中断通道 3 | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD |

NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //抢占式中断优先级设置为 0 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //响应式中断优先级设置为 0 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能中断 NVIC_Init(&NVIC_InitStructure);

NVIC_InitStructure.NVIC_IRQChannel = EXTI4_IRQn; //选择中断通道 4 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //抢占式中断优先级设置为 1 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //响应式中断优先级设置为 1 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); //使能中断

NVIC_InitStructure.NVIC_IRQChannel = EXTI9_5_IRQn; //选择中断通道 5 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //抢占式中断优先级设置为 2 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); //响应式中断优先级设置为 2 //使能中断

NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn; //选择中断通道 2 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3; //抢占式中断优先级设置为 0 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); //响应式中断优先级设置为 0 //使能中断

} 注意:这里为 EXTI2_IRQn 和一般的 EXTI2_IRQnChannel 不同。 由于有 4 个中断,因此根据前文所述,需要有 2 个 bit 来指定抢占优先级,所以选择第 2 组。又由于 EXTI5-9 共 用一个中断响应函数,所以 EXTI5 选择的中断通道是 EXTI9_5_IRQChannel,详细信息可以在头文件中查询得到。用

到的 NVIC 相关的库函数在 stm32f10x_nivc.c 中,需要将此文件复制并添加到工程中。具体位置可以查看关于 GPIO 的笔记。这段代码编译起来没有任何问题,但是在链接的时候就会报错,需要把 STM32F10xR.LIB 加入工程中,具体 位置在…\Keil\ARM\RV31\LIB\ST\STM32F10xR.LIB。 3)IO 口初始化 void GPIO_Configuration(void) { GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_13; GPIO_Init(GPIOD, &GPIO_InitStructure); //D3, D4

//D1

D2

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5|GPIO_Pin_4|GPIO_Pin_3|GPIO_Pin_2;//中断引脚 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOE, &GPIO_InitStructure); } 4)把相应的 IO 口设置为中断线路 由于 GPIO 并不是专用的中断引脚,因此在用 GPIO 来触发外部中断的时候需要设置将 GPIO 相应的引脚和中断线连 接起来,具体代码如下: void EXTI_cfg() { EXTI_InitTypeDef EXTI_InitStructure; //清空中断标志 EXTI_ClearITPendingBit(EXTI_Line2); EXTI_ClearITPendingBit(EXTI_Line3); EXTI_ClearITPendingBit(EXTI_Line4); EXTI_ClearITPendingBit(EXTI_Line5); //选择中断管脚 GPIO_EXTILineConfig(GPIO_PortSourceGPIOE, GPIO_PinSource2); GPIO_EXTILineConfig(GPIO_PortSourceGPIOE, GPIO_PinSource3); GPIO_EXTILineConfig(GPIO_PortSourceGPIOE, GPIO_PinSource4); GPIO_EXTILineConfig(GPIO_PortSourceGPIOE, GPIO_PinSource5); EXTI_InitStructure.EXTI_Line = EXTI_Line2 | EXTI_Line3 | EXTI_Line4|EXTI_Line5; //选择中断线路 EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //设置为中断请求,非事件请求 EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling; //设置中断触发方式为上下降沿触发 EXTI_InitStructure.EXTI_LineCmd = ENABLE; //(总开关)外部中断使能 EXTI_Init(&EXTI_InitStructure); } EXTI_cfg 中需要调用到的函数都在 stm32f10x_exti.c。 5)写中断响应函数 STM32 不像 C51 单片机那样, 可以用过 interrupt 关键字来定义中断响应函数, STM32 的中断响应函数接口存在

中断向量表中,是由启动代码给出的。默认的中断响应函数在 stm32f10x_it.c 中。因此我们需要把这个文件加入到 工程中来。 在这个文件中,我们发现,很多函数都是只有一个函数名,并没有函数体。我们找到 EXTI2_IRQHandler()这个函 数,这就是 EXTI2 中断响应的函数。我的目标是将 LED 灯点亮,所以函数体其实很简单: void EXTI9_5_IRQHandler(void) { GPIO_SetBits(GPIOC,GPIO_Pin_6); EXTI_ClearITPendingBit(EXTI_Line5); } 这个函数是 EXTI5~9 的中断函数,进入中断后都到这里。

2.3.4 补充(2011-12-3)
? 中断和事件的区别:"中断 "基于硬件而言,事件是基于软件而言,“中断”就是事件没做完而被迫停止,事件就 是任务,要做的工作。“中断”常由事件引起,只要优先级事件(任务)到来时才被迫放一放手中的事件,必须 “中断”。

2.4 系统时钟介绍 2.4.1 STM32 的时钟系统
在 STM32 中,一共有 5 个时钟源,分别是 HSI、HSE、LSI、LSE、PLL (1)HSI 是高速内部时钟,RC 振荡器,频率为 8MHz; (2) HSE 是高速外部时钟, 可接石英/陶瓷谐振器, 或者接外部时钟源, 频率范围是 4MHz – 16MHz; (3)LSI 是低速内部时钟,RC 振荡器,频率为 40KHz; (4)LSE 是低速外部时钟,接频率为 32.768KHz 的石英晶体; (5) PLL 为锁相环倍频输出,严格的来说并不算一个独立的时钟源,PLL 的输入可以接 HSI/2、HSE 或者 HSE/2。倍频可选择为 2 – 16 倍,但是其输出频率最大不得超过 72MHz。 其中,40kHz 的 LSI 供独立看门狗 IWDG 使用,另外它还可以被选择为实时时钟 RTC 的时钟源。另 外,实时时钟 RTC 的时钟源还可以选择 LSE,或者是 HSE 的 128 分频。 STM32 中有一个全速功能的 USB 模块,其串行接口引擎需要一个频率为 48MHz 的时钟源。该时 钟源只能从 PLL 端获取,可以选择为 1.5 分频或者 1 分频,也就是,当需使用到 USB 模块时,PLL 必 须使能,并且时钟配置为 48MHz 或 72MHz。 另外 STM32 还可以选择一个时钟信号输出到 MCO 脚(PA.8)上, 可以选择为 PLL 输出的 2 分频、 HSI、 HSE 或者系统时钟。 系统时钟 SYSCLK, 它是提供 STM32 中绝大部分部件工作的时钟源。 系统时钟可以选择为 PLL 输出、 HSI、HSE。系统时钟最大频率为 72MHz,它通过 AHB 分频器分频后送给各个模块使用,AHB 分频器 可以选择 1、2、4、8、16、64、128、256、512 分频,其分频器输出的时钟送给 5 大模块使用: (1)送给 AHB 总线、内核、内存和 DMA 使用的 HCLK 时钟; (2)通过 8 分频后送给 Cortex 的系统定时器时钟; (3)直接送给 Cortex 的空闲运行时钟 FCLK; (4)送给 APB1 分频器。APB1 分频器可以选择 1、2、4、8、16 分频,其输出一路供 APB1 外设使 用(PCLK1,最大频率 36MHz),另一路送给定时器(Timer)2、3、4 倍频器使用。该倍频器以选 择 1 或者 2 倍频,时钟输出供定时器 2、3、4 使用。 (5)送给 APB2 分频器。APB2 分频器可以选择 1、2、4、8、16 分频,其输出一路供 APB2 外设使用 (PCLK2,最大频率 72MHz),另外一路送给定时器(Timer)1 倍频使用。该倍频器可以选择 1 或 2 倍频,时钟输出供定时器 1 使用。另外 APB2 分频器还有一路输出供 ADC 分频器使用,分频后

送给 ADC 模块使用。ADC 分频器可选择为 2、4、6、8 分频。 需要注意的是定时器的倍频器, APB 的分频为 1 时, 当 它的倍频值为 1, 否则它的倍频值就为 2。 连接在 APB1(低速外设)上的设备有:电源接口、备份接口、CAN、USB、I2C1、I2C2、UART2、UART3、 SPI2、 窗口看门狗、 Timer2、 Timer3、 Timer4。 注意 USB 模块虽然需要一个单独的 48MHz 的时钟信号, 但是它应该不是供 USB 模块工作的时钟,而只是提供给串行接口引擎(SIE)使用的时钟。USB 模块的工 作时钟应该是由 APB1 提供的。 连接在 APB2(高速外设)上的设备有:UART1、SPI1、Timer1、ADC1、ADC2、GPIOx(PA~PE)、第 二功能 IO 口。

2.4.2 STM32 时钟的初始化
由于我现在所用的开发板已经外接了一个 8MHz 的晶振,因此将采用 HSE 时钟,在 MDK 编译平 台中,程序的时钟设置参数流程如下: (1) 将 RCC 寄存器重新设置为默认值:RCC_DeInit; (2) 打开外部高速时钟晶振 HSE: RCC_HSEConfig(RCC_HSE_ON); (3) 等待外部高速时钟晶振工作: HSEStartUpStatus = RCC_WaitForHSEStartUp(); (4) 设置 AHB 时钟(HCLK): RCC_HCLKConfig; (5) 设置高速 AHB 时钟(APB2): RCC_PCLK2Config; (6) 设置低速 AHB 时钟(APB1): RCC_PCLK1Config; (7) 设置 PLL: RCC_PLLConfig; (8) 打开 PLL: RCC_PLLCmd(ENABLE); (9) 等待 PLL 工作: while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET); (10)设置系统时钟: RCC_SYSCLKConfig; (11)判断 PLL 是否是系统时钟: while(RCC_GetSYSCLKSource() != 0x08); (12)打开要使用的外设时钟: RCC_APB2PerphClockCmd()….

2.4.3 SysTick 定时器
首先,我们要知道系统滴答时钟的作用:其主要可以用来做精确延时。 NVIC 中,捆绑着一个 SysTick 定时器,它是一个 24 位的倒数计数定时器,当计到 0 时,将从 RELOAD 寄存器中 自动重装载定时初值并继续计数,同时内部的 COUNTFLAG 标志会置位,触发中断 (如果中断使能情况下)。只要不 把它在 SysTick 控制及状态寄存器中的使能位清除,就用不停息。Cortex-M3 允许为 SysTick 提供 2 个时钟源以供选 择,第一个是内核的“自由运行时钟”FCLK,“自由”表现在它不是来自系统时钟 HCLK,因此在系统时钟停止时, FCLK 也能继续运行。第 2 个是一个外部的参考时钟,但是使用外部时钟时,因为它在内部是通过 FCLK 来采样的, 因此其周期必须至少是 FCLK 的两倍(采样定理)。 下面介绍一下 STM32 中的 SysTick,它属于 NVIC 控制部分,一共有 4 个寄存器: STK_CSR, STK_LOAD, STK_VAL, 0xE000E010: 0xE000E014: 0xE000E018: 控制寄存器 重载寄存器 当前值寄存器 校准值寄存器

STK_CALRB, 0xE000E01C:

首先看 STK_CSR 控制寄存器,有 4 个 bit 具有意义: 第 0 位:ENABLE,SysTick 使能位(0:关闭 SysTick 功能,1:开启 SysTick 功能); 第 1 位:TICKINT,SysTick 中断使能位(0:关闭 SysTick 中断,1:开启 SysTick 中断); 第 2 位:CLKSOURCE,SysTick 时钟选择(0:使用 HCLK/8 作为时钟源,1:使用 HCLK); 第 3 为:COUNTFLAG,SysTick 计数比较标志,如果在上次读取本寄存器后,SysTick 已经数到 0 了,则该位为 1,如 果读取该位,该位自动清零。 STK_LOAD 重载寄存器:

Systick 是一个递减的定时器, 当定时器递减至 0 时, 重载寄存器中的值就会被重装载, 继续开始递减。 STK_LOAD 载寄存器是个 24 位的寄存器最大计数 0xFFFFFF。



STK_VAL 当前值寄存器: 也是个 24 位的寄存器,读取时返回当前倒计数的值,写它则使之清零,同时还会清除在 SysTick 控制及状态寄存器 中的 COUNTFLAG 标志。 STK_CALRB 校准值寄存器: 其中包含着一个 TENMS 位段,具体信息不详。暂时用不到。 主要用到的函数如下: SysTick_Config(SystemFrequency / 1000)//设置每隔 1ms 进入一次中断函数 void SysTick_Handler(void) 注意:不要随便用网上的程序,因为本身库是不一样的。

2.5 定时器 2.5.1 定时器原理
STM32 系列的 CPU,有多达 8 个定时器,其中 TIM1 和 TIM8 是能够产生三对 PWM 互补输出的高级定时器, 常用于三相电机的驱动,它们的时钟由 APB2 的输出产生。其它 6 个为普通定时器,时钟由 APB1 的输出产生。下 图是 STM32 参考手册上时钟分配图中,有关定时器时钟部分的截图:

实际上 STM32 的 CPU 文档给出的图与这个图略有区别。但是我们还是想研究这个图。原因是这个图对我们思 路的理解比较有帮助。从图中可以看出,定时器的时钟不是直接来自 APB1 或 APB2,而是来自于输入为 APB1 或 APB2 的一个倍频器,图中的蓝色部分。 下面以通用定时器 2 的时钟说明这个倍频器的作用:当 APB1 的预分频系数为 1 时,这个倍频器不起作用, 定时器的时钟频率等于 APB1 的频率;当 APB1 的预分频系数为其它数值(即预分频系数为 2、4、8 或 16)时,这个 倍频器起作用,定时器的时钟频率等于 APB1 的频率两倍。 假定 AHB=36MHz,因为 APB1 允许的最大频率为 36MHz,所以 APB1 的预分频系数可以取任意数值;当预分频 系数=1 时,APB1=36MHz,TIM2~7 的时钟频率=36MHz(倍频器不起作用);当预分频系数=2 时,APB1=18MHz,在 倍频器的作用下,TIM2~7 的时钟频率=36MHz。

有人会问,既然需要 TIM2~7 的时钟频率=36MHz,为什么不直接取 APB1 的预分频系数=1?答案是:APB1 不 但要为 TIM2~7 提供时钟,而且还要为其它外设提供时钟;设置这个倍频器可以在保证其它外设使用较低时钟频率 时,TIM2~7 仍能得到较高的时钟频率。 再举个例子:当 AHB=72MHz 时,APB1 的预分频系数必须大于 2,因为 APB1 的最大频率只能为 36MHz。如果 APB1 的预分频系数=2, 则因为这个倍频器, TIM2~7 仍然能够得到 72MHz 的时钟频率。 能够使用更高的时钟频率, 无疑提高了定时器的分辨率,这也正是设计这个倍频器的初衷。 TIM2-TIM5 可以由向上计数、向下计数、向上向下双向计数。向上计数模式中,计数器从 0 计数到自动加载值 (TIMx_ARR 计数器内容),然后重新从 0 开始计数并且产生一个计数器溢出事件。在向下模式中,计数器从自动装入 的值(TIMx_ARR)开始向下计数到 0,然后从自动装入的值重新开始,并产生一个计数器向下溢出事件。而中央对齐 模式(向上/向下计数)是计数器从 0 开始计数到自动装入的值,产生一个计数器溢出事件,然后向下计数到 1 并 且产生一个计数器溢出事件;然后再从 0 开始重新计数。

2.5.2 定时器编程实例
1)系统配置 SystemInit();,包括时钟 RCC 的配置,倍频到 72MHZ。 void RCC_Configuration(void) { SystemInit(); RCC_APB2PeriphClockCmd( RCC_APB2Periph_USART1 |RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE, ENABLE); } 2)GPIO 的配置,使用函数为 GPIO_Config(),该函数的实现如下: void GPIO_Cfg(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_13; GPIO_Init(GPIOD, &GPIO_InitStructure); //D3, D4 //D1 D2

} 3)嵌套中断控制器的配置,我们使用函数 NVIC_Config();,只是初始化的过程略有不同。 //用来管理那一条中断通道可以用 void NVIC_Cfg(void) { NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0); NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; //通道 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;// NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure);

} 4)定时器的初始化配置,使用 Timer_Config()。 void Timer_Cfg(void) { TIM_TimeBaseInitTypeDef

TIM_TimeBaseStructure;

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2 , ENABLE); TIM_DeInit(TIM2); TIM_TimeBaseStructure.TIM_Period=2000; //自动重装载寄存器的值 TIM_TimeBaseStructure.TIM_Prescaler= (36000 - 1); //时钟预分频数,其频率为 2khz TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1; //采样分频 TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_ClearFlag(TIM2, TIM_FLAG_Update); //清除溢出中断标志 TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE);//开启定时器中断,跟 NVIC 的作用相当于双保险, //便于以后的开启和关闭定时器 TIM_Cmd(TIM2, ENABLE); //开启时钟 } 最后两个步骤,一个是管理时钟的,一个是管理中断的。 用到的函数功能如下: (1) 利用 TIM_DeInit()函数将 Timer 设置为默认缺省值; TIM_InternalClockConfig()选择 TIMx 来设置内部时钟源; (2) (3) (4) (5) (6) (7) (8) TIM_Perscaler 来设置预分频系数; TIM_ClockDivision 来设置时钟分割; TIM_CounterMode 来设置计数器模式; TIM_Period 来设置自动装入的值 TIM_ARRPerloadConfig()来设置是否使用预装载缓冲器 TIM_ITConfig()来开启 TIMx 的中断

其中(3)-(6)步骤中的参数由 TIM_TimerBaseInitTypeDef 结构体给出。步骤(3)中的预分频系数用来确定 TIMx 所使用的时钟频率,具体计算方法为:CK_INT/(TIM_Perscaler+1)。CK_INT 是内部时钟源的频率,是根据描 这里为 72M。 TIM_Perscaler 是用户设定的预分频系数, 其值范围是从 0 – 65535。 述的 APB1 的倍频器送出的时钟, 步骤(4)中的时钟分割定义的是在定时器时钟频率(CK_INT)与数字滤波器(ETR,TIx)使用的采样频率之间的分频比 例。TIM_ClockDivision 的参数如下表:(一般设置为 TIM_CKD_DIV1) TIM_ClockDivision TIM_CKD_DIV1 TIM_CKD_DIV2 TIM_CKD_DIV4 描述 tDTS = Tck_tim tDTS = 2 * Tck_tim tDTS = 4 * Tck_tim 二进制值 0x00 0x01 0x10

数字滤波器(ETR,TIx)是为了将 ETR 进来的分频后的信号滤波,保证通过信号频率不超过某个限定。 步骤(7)中需要禁止使用预装载缓冲器。当预装载缓冲器被禁止时,写入自动装入的值(TIMx_ARR)的数值会 直接传送到对应的影子寄存器;如果使能预加载寄存器,则写入 ARR 的数值会在更新事件时,才会从预加载寄存 器传送到对应的影子寄存器。 ARM 中,有的逻辑寄存器在物理上对应 2 个寄存器,一个是程序员可以写入或读出的寄存器,称为 preload register(预装载寄存器),另一个是程序员看不见的、但在操作中真正起作用的寄存器,称为 shadow register(影子寄 存器);设计 preload register 和 shadow register 的好处是,所有真正需要起作用的寄存器(shadow register)可以在同一 个时间(发生更新事件时)被更新为所对应的 preload register 的内容, 这样可以保证多个通道的操作能够准确地同步。 如果没有 shadow register,或者 preload register 和 shadow register 是直通的,即软件更新 preload register 时,同时更 新了 shadow register, 为 软件不可能在一个相同的时刻同时更新多个寄存器, 因 结果造成多个通道的时序不能同步,

如果再加上其它因素(例如中断),多个通道的时序关系有可能是不可预知的。 5)编写中断服务程序。同样需要注意的,一进入中断服务程序,第一步要做的,就是清除掉中断标志位。由于我 们使用的是向上溢出模式,因此使用 的函数应该是: TIM_ClearITPendingBit(TIM2 , TIM_FLAG_Update);

2.6 定时器 PWM 2.6.1 TIMER 输出 PWM 基本概念
脉冲宽度调制(PWM),是英文“Pulse Width Modulation”的缩写,简称脉宽调制,是利用微处理器的数字输出 来对模拟电路进行控制的一种非常有效的技术。简单一点,就是对脉冲宽度的控制。一般用来控制步进电机的速度 等等。 STM32 的定时器除了 TIM6 和 TIM7 之外,其他的定时器都可以用来产生 PWM 输出,其中高级定时器 TIM1 和 TIM8 可以同时产生 7 路的 PWM 输出,而通用定时器也能同时产生 4 路的 PWM 输出。 1) PWM 输出模式 STM32 的 PWM 输出有两种模式,模式 1 和模式 2,由 TIMx_CCMRx 寄存器中的 OCxM 位确定的(“110”为模 式 1,“111”为模式 2)。模式 1 和模式 2 的区别如下: 110:PWM 模式 1-在向上计数时,一旦 TIMx_CNT<TIMx_CCR1 时通道 1 为有效电平,否则为无效电平;在向下计 数时,一旦 TIMx_CNT>TIMx_CCR1 时通道 1 为无效电平(OC1REF=0),否则为有效电平(OC1REF=1)。 111:PWM 模式 2-在向上计数时,一旦 TIMx_CNT<TIMx_CCR1 时通道 1 为无效电平,否则为有效电平;在向下计 数时,一旦 TIMx_CNT>TIMx_CCR1 时通道 1 为有效电平,否则为无效电平。 由此看来,模式 1 和模式 2 正好互补,互为相反,所以在运用起来差别也并不太大。 而从计数模式上来看, PWM 也和 TIMx 在作定时器时一样, 也有向上计数模式、 向下计数模式和中心对齐模式, 具体说明看 2.5 节定时器 。 2 )PWM 输出管脚 PWM 的输出管脚是确定好的,具体的引脚功能可以查看《STM32 参考手册》的“定时器复用功能重映射”一 节。在此需要强调的是,不同的 TIMx 有分配不同的引脚,但是考虑到管脚复用功能,STM32 提出了一个重映像的 概念,就是说通过设置某一些相关的寄存器,来使得在其他非原始指定的管脚上也能输出 PWM。但是这些重映像 的管脚也是由参考手册给出的。比如说 TIM3 的第 2 个通道,在没有重映像的时候,指定的管脚是 PA.7,如果设置 部分重映像之后,TIM3_CH2 的输出就被映射到 PB.5 上了,如果设置了完全重映像的话,TIM3_CH2 的输出就被映 射到 PC.7 上了。 3) PWM 输出信号 PWM 输出的是一个方波信号,信号的频率是由 TIMx 的时钟频率和 TIMx_ARR 预分频器所决定的,具体设置方 法在前面一个学习笔记中有详细的交代。而输出信号的占空比则是由 TIMx_CRRx 寄存器确定的。其公式为“占空比 =(TIMx_CRRx/TIMx_ARR)*100%”,因此,可以通过向 CRR 中填入适当的数来输出自己所需的频率和占空比的方波信 号。

2.6.2 TIMER 输出 PWM 实现步骤
1. 2. 3. 4. 设置 RCC 时钟; 设置 GPIO 时钟; 设置 TIMx 定时器的相关寄存器; 设置 TIMx 定时器的 PWM 相关寄存器。

第 1 步设置 RCC 时钟已经在前文中给出了详细的代码,在此就不再多说了。需要注意的是通用定时器 TIMx 是

由 APB1 提供时钟,而 GPIO 则是由 APB2 提供时钟。注意,如果需要对 PWM 的输出进行重映像的话,还需要开启 引脚复用时钟 AFIO。 第 2 步设置 GPIO 时钟时,GPIO 模式应该设置为复用推挽输出 GPIO_Mode_AF_PP,如果需要引脚重映像的话, 则需要用 GPIO_PinRemapConfig()函数进行设置。 第 3 步设置 TIMx 定时器的相关寄存器时,和之前介绍的一样。 第 4 步设置 PWM 相关寄存器,首先要设置 PWM 模式(默认情况下 PWM 是冻结的),然后设置占空比(根据 前面所述公式进行计算),再设置输出比较极性:当设置为 High 时,输出信号不反相,当设置为 Low 时,输出信 号反相之后再输出。最重要是是要使能 TIMx 的输出状态和使能 TIMx 的 PWM 输出使能。 相关设置完成之后,就可以通过 TIM_Cmd()来打开 TIMx 定时器,从而得到 PWM 输出了。

2.6.3 代码实现
E:\My_Keil_Projects\timer_pwm 该程序的目的在于通过 pwm 控制 led 的亮度,D1 对应的 PC6,是 timer3 的完全映射。 主函数如下所示 int main(void) { RCC_Configuration(); Timer_Cfg(); GPIO_Cfg(); PWM_Cfg(); TIM_Cmd(TIM3, ENABLE); //开启时钟 while (1); } void RCC_Configuration(void) { SystemInit(); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC | RCC_APB2Periph_AFIO,ENABLE); } void PWM_Cfg(void) { TIM_OCInitTypeDef TimOCInitStructure; //设置缺省值 TIM_OCStructInit(&TimOCInitStructure); //PWM 模式 1 输出 TimOCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //设置占空比,占空比=(CCRx/ARR)*100%或(TIM_Pulse/TIM_Period)*100% TimOCInitStructure.TIM_Pulse = dutyfactor * 7200 / 100; //TIM 输出比较极性高 TimOCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //使能输出状态 TimOCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //TIM3 的 CH1 输出 TIM_OC1Init(TIM3, &TimOCInitStructure); //设置 TIM3 的 PWM 输出为使能

TIM_CtrlPWMOutputs(TIM3,ENABLE); } void Timer_Cfg(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; //重新将 Timer 设置为缺省值 TIM_DeInit(TIM3); //采用内部时钟给 TIM3 提供时钟源 TIM_InternalClockConfig(TIM3); //预分频系数为 0,即不进行预分频,此时 TIMER 的频率为 72MHz TIM_TimeBaseStructure.TIM_Prescaler = 0; //设置时钟分割 TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置计数器模式为向上计数模式 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //设置计数溢出大小,每计 7200 个数就产生一个更新事件,即 PWM 的输出频率为 10kHz TIM_TimeBaseStructure.TIM_Period = 7200 - 1; //将配置应用到 TIM3 中 TIM_TimeBaseInit(TIM3,&TIM_TimeBaseStructure); } void GPIO_Cfg(void) { GPIO_PinRemapConfig(GPIO_FullRemap_TIM3, ENABLE); //设置管脚映射

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; //D1 D2 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出, GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); }

2.7 串口(USART) 2.7.1 串口的基本概念
在 STM32 的参考手册中,串口被描述成通用同步异步收发器(USART),它提供了一种灵活的方法与使用工业标 准 NRZ 异步串行数据格式的外部设备之间进行全双工数据交换。 USART 利用分数波特率发生器提供宽范围的波特率 选择。它支持同步单向通信和半双工单线通信,也支持 LIN(局部互联网),智能卡协议和 IrDA(红外数据组织) SIR ENDEC 规范,以及调制解调器(CTS/RTS)操作。它还允许多处理器通信。还可以使用 DMA 方式,实现高速数据通 信。 USART 通过 3 个引脚与其他设备连接在一起,任何 USART 双向通信至少需要 2 个引脚:接受数据输入(RX)和发 送数据输出(TX)。 RX: 接受数据串行输入。通过过采样技术来区别数据和噪音,从而恢复数据。 TX: 发送数据输出。当发送器被禁止时,输出引脚恢复到它的 I/O 端口配置。当发送器被激活,并且不发送数据时, TX 引脚处处于高电平。在单线和智能卡模式里,此 I/O 口被同时用于数据的发送和接收。

2.7.2 串口的如何工作的
一般有两种方式:查询和中断。 (1)查询:串口程序不断地循环查询,看看当前有没有数据要它传送。如果有,就帮助传送(可以从 PC 到 STM32 板子,也可以从 STM32 板子到 PC)。 (2)中断:平时串口只要打开中断即可。如果发现有一个中断来,则意味着要它帮助传输数据——它就马上进行 数据的传送。同样,可以从 PC 到 STM3 板子,也可以从 STM32 板子到 PC。

2.7.3 串口的硬件连接
我们用的板子有两路串口,USART1 经 PL2303HX 转化成 USB,另外一路 USART2 经 PA2 和 PA3 连接出来。

2.7.4 编程实例
一般对串口的操作,我们需要利用串口调试软件,在光盘的常用软件中可以看到。 串口编程一般按照如下步骤进行: (1) RCC 配置; (2) GPIO 配置; (3) (4) (5) USART 配置; NVIC 配置; 发送/接收数据。

在 RCC 配置中,我们除了常规的时钟设置以外,要记得打开 USART 相对应的 IO 口时钟,USART 时钟,还有管 脚功能复用时钟。 在 GPIO 配置中,将发送端的管脚配置为复用推挽输出,将接收端的管脚配置为浮空输入。 在 USART 的配置中,通过 USART_InitTypeDef 结构体对 USART 进行初始化操作,按照自己所需的功能配置好就 可以了。注意,在串口调试软件的设置中,需要和这个里面的配置相对应。由于我是采用中断接收数据的方式,所 以记得在 USART 的配置中药打开串口的中断,同时最后还要打开串口。 在 NVIC 的配置中,主要是 USART2_IRQn 的配置,和以前讲述的中断配置类似。 全 部 配 置 好 之 后 就 可 以 开 始 发 送 / 接 收 数 据 了 。 发 送 数 据 用 USART_SendData() 函 数 , 接 收 数 据 用 USART_ReceiveData()函数。具体的函数功能可以参考固件库的参考文件。根据 USART 的配置,在发送和接收时,都 是采用的 8bits 一帧来进行的,因此,在发送的时候,先开辟一个缓存区,将需要发送的数据送入缓存区,然后再 将缓存区中的数据发送出去,在接收的时候,同样也是先接收到缓存区中,然后再进行相应的操作。 注意在对数据进行发送和接收的时候,要检查 USART 的状态,只有等到数据发送或接收完毕之后才能进行下 一帧数据的发送或接收。采用 USART_GetFlagStatus()函数。 同时还要注意的是,在发送数据的最开始,需要清除一下 USART 的标志位,否则,第 1 位数据会丢失。因为 在硬件复位之后,USART 的状态位 TC 是置位的。当包含有数据的一帧发送完成之后,由硬件将该位置位。只要当 USART 的状态位 TC 是置位的时候,就可以进行数据的发送。然后 TC 位的置零则是通过软件序列来清除的,具体的 步骤是“先读 USART_SR,然后写入 USART_DR”,只有这样才能够清除标志位 TC,但是在发送第一帧数据的 时候,并没有进行读 USART_SR 的操作,而是直接进行写操作,因此 TC 标志位并没有清空,那么,当发送第一帧 数据,然后用 USART_GetFlagStatus()检测状态时返回的是已经发送完毕(因为 TC 位是置 1 的),所以程序会马上 发送下一帧数据,那么这样,第一帧数据就被第二帧数据给覆盖了,所以看不到第一帧数据的发送。 int main(void) { int i=0;

unsigned char TxBuf1[]="huanyingla!"; RCC_Configuration(); GPIO_Cfg(); USART_Cfg(); NVIC_Cfg(); USART_ClearFlag(USART2,USART_FLAG_TC); for( i=0;TxBuf1[i]!='\0';i++) { USART_SendData(USART2,TxBuf1[i]); GPIO_SetBits(GPIOC,GPIO_Pin_6); //等待数据发送完毕 while(USART_GetFlagStatus(USART2, USART_FLAG_TC)==RESET); GPIO_ResetBits(GPIOC,GPIO_Pin_6); } while(1); }

void RCC_Configuration(void) { SystemInit(); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); } void GPIO_Cfg(void) { GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); //发送数据 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); //接受数据 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); } //可以跟定时器做类比 void USART_Cfg(void) { USART_InitTypeDef USART_InitStructure; //D1 D2

//将结构体设置为缺省状态 USART_StructInit(&USART_InitStructure); //波特率设置为 115200 USART_InitStructure.USART_BaudRate = 115200; //一帧数据的宽度设置为 8bits USART_InitStructure.USART_WordLength = USART_WordLength_8b; //在帧结尾传输 1 个停止位 USART_InitStructure.USART_StopBits = USART_StopBits_1; //奇偶失能模式,无奇偶校验 USART_InitStructure.USART_Parity = USART_Parity_No; //发送/接收使能 USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //硬件流控制失能 USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //设置串口 2 USART_Init(USART2, &USART_InitStructure); //打开串口 2 的中断响应函数 USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); //打开串口 1 USART_Cmd(USART2, ENABLE); } void NVIC_Cfg(void) { NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//选择中断分组 2 NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;//选择串口 1 中断 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;//抢占式中断优先级设置为 0 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;//响应式中断优先级设置为 0 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//使能中断 NVIC_Init(&NVIC_InitStructure); }

存在问题:使用不稳定。特别是在利用终端接收数据之后发送时,不能一下接收多位 数据。

2.7.5 串口补充
1)可以用 printf 进行数据的发送: 步骤一:添加定义 #ifdef __GNUC__ #define PUTCHAR_PROTOTYPE int __io_putchar(int ch) #else #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f) #endif // __GNUC__ 步骤二:重写函数

PUTCHAR_PROTOTYPE { USART_SendData(USART2, (u8) ch); while (USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET) {} return ch; } 步骤三:勾选 Target 选项框里选 Use MicroLib 选项 步骤四:包含头文件#include <stdio.h> 2)#define countof(a) (sizeof(a) / sizeof(*(a))) //取数组 a 的元素个数 sizeof(a)//获得 a 的总的字节数, sizeof(*(a)),//获得 a 单个元素的大小 3)利用串口进行数据的发送 这里建议一般利用查询进行数据的发送,但如果需要用终端也可以,有两种方法: 先说 TC。即 Transmission Complete。发送一个字节后才进入中断,这里称为“发送后中断”。和原来 8051 的 TI 方式 一样,都是发送后才进中断,需要在发送函数中先发送一个字节触发中断。发送函数如下 /******* 功能:中断方式发送字符串.采用判断 TC 的方式.即 判断 发送后中断 位. 输入:字符串的首地址 输出:无 *******/ void USART_SendDataString( u8 *pData ) { pDataByte = pData; USART_ClearFlag(USART1, USART_FLAG_TC);//清除传输完成标志位,否则可能会丢失第 1 个字节的数据.网友提供. USART_SendData(USART1, *(pDataByte++) ); //必须要++,不然会把第一个字符 t 发送两次 } 中断处理函数如下 void USART1_IRQHandler(void) { if( USART_GetITStatus(USART1, USART_IT_TC) == SET ) { if( *pDataByte == '\0' )//TC 需要 读 SR+写 DR 方可清 0,当发送到最后,到'\0'的时候用个 if 判断关掉 USART_ClearFlag(USART1, USART_FLAG_TC);//不然 TC 一直是 set, TCIE 也是打开的,导致会不停进入中断. Clear 掉即可,不用关掉 TCIE else USART_SendData(USART1, *pDataByte++ ); } } 其中 u8 *pDataByte;是一个外部指针变量 在中断处理程序中,发送完该字符串后,不用关闭 TC 的中断使能 TCIE,只需要清掉标志位 TC;这样就能避免 TC == SET 导致反复进入中断了。

串口初始化函数如下 void USART_Config() { USART_InitTypeDef USART_InitStructure;//定义一个包含串口参数的结构体 USART_InitStructure.USART_BaudRate = 9600; //波特率 9600 USART_InitStructure.USART_WordLength = USART_WordLength_8b;//8 位数据位 USART_InitStructure.USART_StopBits = USART_StopBits_1;//1 位停止位 USART_InitStructure.USART_Parity = USART_Parity_No;//无校验 USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件流控制 USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;//输入加输出模式 USART_InitStructure.USART_Clock = USART_Clock_Disable;//时钟关闭 USART_InitStructure.USART_CPOL = USART_CPOL_Low; USART_InitStructure.USART_CPHA = USART_CPHA_2Edge; USART_InitStructure.USART_LastBit = USART_LastBit_Disable; USART_Init(USART1, &USART_InitStructure);//设置到 USART1 USART_ITConfig(USART1, USART_IT_TC, ENABLE);//Tramsimssion Complete 后,才产生中断. 开 TC 中断必须放在这里, 否则还是会丢失第一字节 USART_Cmd(USART1, ENABLE); //使能 USART1 } 再说判断 TXE。即 Tx DR Empty,发送寄存器空。当使能 TXEIE 后,只要 Tx DR 空了,就会产生中断。所以,发送完 字符串后必须关掉,否则会导致重复进入中断。这也是和 TC 不同之处。 发送函数如下: /******* 功能:中断方式发送字符串.采用判断 TC 的方式.即 判断 发送后中断 位. 输入:字符串的首地址 输出:无 *******/ void USART_SendDataString( u8 *pData ) { pDataByte = pData; USART_ITConfig(USART1, USART_IT_TXE, ENABLE);//只要发送寄存器为空,就会一直有中断,因此,要是不发送数 据时,把发送中断关闭,只在开始发送时,才打开。 } 中断处理函数如下: void USART1_IRQHandler(void) { if( USART_GetITStatus(USART1, USART_IT_TXE) == SET ) { if( *pDataByte == '\0' )//待发送的字节发到末尾 NULL 了 USART_ITConfig(USART1, USART_IT_TXE, DISABLE);//因为是发送寄存器空的中断,所以发完字符串后必 须关掉,否则只要空了,就会进中断 else

USART_SendData(USART1, *pDataByte++ ); } }在串口初始化函数中就不用打开 TXE 的中断了(是在发送函数中打开的)如下: /************ 名称: USART_Config 功能: 设置串口参数 输入: 无 输出: 无 返回: 无 ************/ void USART_Config() { USART_InitTypeDef USART_InitStructure;//定义一个包含串口参数的结构体 USART_InitStructure.USART_BaudRate = 9600; //波特率 9600 USART_InitStructure.USART_WordLength = USART_WordLength_8b;//8 位数据位 USART_InitStructure.USART_StopBits = USART_StopBits_1;//1 位停止位 USART_InitStructure.USART_Parity = USART_Parity_No;//无校验 USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件流控制 USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;//输入加输出模式 USART_InitStructure.USART_Clock = USART_Clock_Disable;//时钟关闭 USART_InitStructure.USART_CPOL = USART_CPOL_Low; USART_InitStructure.USART_CPHA = USART_CPHA_2Edge; USART_InitStructure.USART_LastBit = USART_LastBit_Disable; USART_Init(USART1, &USART_InitStructure);//设置到 USART1 USART_Cmd(USART1, ENABLE); //使能 USART1 }

2.8 RTC 实时时钟 2.8.1 基本原理
1)STM32 的 RTC 模块 RTC 模块之所以具有实时时钟功能,是因为它内部维持了一个独立的定时器,通过配置,可以让它准确地每秒 钟中断一次。下面就来看以下它的组成结构。 2)RTC 的组成 RTC 由两个部分组成:APB1 接口部分以及 RTC 核心部分。RTC 核心部分又分为预分频模块和一个 32 位的可 编程计数器。前者可使每个 TR_CLK 周期中 RTC 产生一个秒中断,后者可被初始化为当前系统时间。此后系统时间 会按照 TR_CLK 周期进行累加,实现时钟功能。 3)对 RTC 的操作 我们对 RTC 的访问,是通过 APB1 接口来进行的。注意,APB1 刚被开启的时候(比如刚上电,或刚复位后), 从 APB1 上读出来的 RTC 寄存器的第一个值有可能是被破坏了的(通常读到 0) 。这个不幸,STM32 是如何预防的 呢?我们在程序中,会先等待 RTC_CRL 寄存器中的 RSF 位(寄存器同步标志)被硬件置 1,然后才开始读操作,这 时候读出来的值就是 OK 的。那么对 RTC 寄存器的写操作会不会有类似的情况呢?对于写操作,我们只 要注意,每一次写操作,必须确保在前一次写操作完成后进行。这个“确保” ,是通过查询 RTC_CR 寄存器中

的 RTOFF 状态位,判断 RTC 寄存器是否处于更新中。只有当 RTOFF 状态位是 1,才可以写 RTC 寄存器。

2.8.2 编程实例
C:\Documents and Settings\HP\桌面\建工给的东东\rtc\RTC\完善版 这个程序的功能是在串口调试软件中实时显示现在的时间。 首先看 GPIO 的配置,RCC 配置和 NVIC 的配置: void GPIO_Configuration(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOC, &GPIO_InitStructure); //以下设置串口 usart GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOA , &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); } //系统中断管理 void NVIC_Configuration(void) { NVIC_InitTypeDef NVIC_InitStructure; #ifdef VECT_TAB_RAM /* Set the Vector Table base location at 0x20000000 */ NVIC_SetVectorTable(NVIC_VectTab_RAM, 0x0); #else /* VECT_TAB_FLASH */ /* Set the Vector Table base location at 0x08000000 */ NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x0); #endif /* Configure one bit for preemption priority */ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); /* Enable the RTC Interrupt *///RTC 也有中断? NVIC_InitStructure.NVIC_IRQChannel = RTC_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); } //配置系统时钟,使能各外设时钟

void RCC_Configuration(void) { SystemInit(); RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA |RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC |RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE |RCC_APB2Periph_ADC1 | RCC_APB2Periph_AFIO |RCC_APB2Periph_SPI1, ENABLE ); // RCC_APB2PeriphClockCmd(RCC_APB2Periph_ALL ,ENABLE ); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4|RCC_APB1Periph_USART2 |RCC_APB1Periph_USART3|RCC_APB1Periph_TIM2 , ENABLE ); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE); }

//配置所有外设 void Init_All_Periph(void) { RCC_Configuration(); // InitDis(); GPIO_Configuration(); NVIC_Configuration(); USART_Configuration(); } 主函数如下: int main(void) { Init_All_Periph(); if (BKP_ReadBackupRegister(BKP_DR1) != 0xA1A1)//读取备份寄存器的值是否为默认值 0xA1A1,如果不是,即要重 新设置备份寄存器 { GPIO_SetBits(GPIOD,GPIO_Pin_2); printf("\r\n RTC not yet configured...."); RTC_Configuration(); printf("\r\n RTC configured...."); Time_Adjust();//调整时钟 BKP_WriteBackupRegister(BKP_DR1, 0xA1A1);//做个标志,说明已经设置过时间 } //BKP_WriteBackupRegister 向指定的后备寄存器中写入用户程序数据 else { GPIO_ResetBits(GPIOD,GPIO_Pin_2); GPIO_SetBits(GPIOD,GPIO_Pin_3); if (RCC_GetFlagStatus(RCC_FLAG_PORRST) != RESET) //POR/PDR 复位

{ printf("\r\n\n Power On Reset occurred...."); } else if (RCC_GetFlagStatus(RCC_FLAG_PINRST) != RESET)//RESET=0 { printf("\r\n\n External Reset occurred...."); } printf("\r\n No need to configure RTC...."); RTC_WaitForSynchro(); //等待 RTC 寄存器(RTC_CNT, RTC_ALR and RTC_PRL)与 RTC 的 APB 时钟同步 /* Enable the RTC Second 开中断*/ RTC_ITConfig(RTC_IT_SEC, ENABLE);//使能或者失能指定的 RTC 中断 /* Wait until last write operation on RTC registers has finished */ RTC_WaitForLastTask();//等待最近一次对 RTC 寄存器的写操作完成 #ifdef RTCClockOutput_Enable /* Enable PWR and BKP clocks */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); /* Allow access to BKP Domain */ PWR_BackupAccessCmd(ENABLE); /* Disable the Tamper Pin */ BKP_TamperPinCmd(DISABLE); /* To output RTCCLK/64 on Tamper pin, the tamper 使能或者失能管脚的侵入检测功能 functionality must be disabled */ /* Enable RTC Clock Output on Tamper Pin */ BKP_RTCOutputConfig(BKP_RTCOutputSource_CalibClock); #endif /* Clear reset flags */ RCC_ClearFlag(); /* Display time in infinite loop */ Time_Show();//主函数中调用 } } 关键函数以用红色标出: //对 RTC 时钟的调整 void RTC_Configuration(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);//开 PWR 时钟&BKP 时钟 PWR_BackupAccessCmd(ENABLE); //使能 RTC 和后备寄存器访问 BKP_DeInit();//将外设 BKP 的全部寄存器重设为缺省值 RCC_LSEConfig(RCC_LSE_ON); //设置外部低速晶振(LSE) while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET)// 检 查 指 定 的 RCC 标 志 位 设 置 与 否 --LSE 晶 振 就 绪 RCC_FLAG_LSERDY {} //等待 LSE 晶振就绪 RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); RCC_RTCCLKCmd(ENABLE); RTC_WaitForSynchro();//等待最近一次对 RTC 寄存器的写操作完成 RTC_WaitForLastTask(); 管脚复位

RTC_ITConfig(RTC_IT_SEC, ENABLE);//开秒中断 RTC_WaitForLastTask(); RTC_SetPrescaler(32767); // RTC period = RTCCLK/RTC_PR = (32.768 KHz)/(32767+1) //一秒中断一次 RTC_WaitForLastTask(); } //调整现在时间 void Time_Adjust(void) { /* Wait until last write operation on RTC registers has finished */ RTC_WaitForLastTask();//等待 // Time_Regulate(); /* Change the current time */ RTC_SetCounter(Time_Regulate());//设置 RTC 寄存器的值,总秒数 /* Wait until last write operation on RTC registers has finished */ RTC_WaitForLastTask(); } //读取用户输入的时间 u32 Time_Regulate(void) { u32 Tmp_HH = 0xFF, Tmp_MM = 0xFF, Tmp_SS = 0xFF; printf("\r\n==============Time Settings====================================="); printf("\r\n Please Set Hours"); while(Tmp_HH == 0xFF) { Tmp_HH = USART_Scanf(23); } printf(": %d", Tmp_HH); printf("\r\n Please Set Minutes"); while(Tmp_MM == 0xFF) { Tmp_MM = USART_Scanf(59); } printf(": %d", Tmp_MM);

printf("\r\n Please Set Seconds"); while(Tmp_SS == 0xFF) { Tmp_SS = USART_Scanf(59); } printf(": %d", Tmp_SS);

//rtc_set(2010,11,29,Tmp_HH,Tmp_MM,Tmp_SS); //rtc_set(2011,3,1,12,28,0); /* Return the value to store in RTC counter register */ return((Tmp_HH*3600 + Tmp_MM*60 + Tmp_SS));

} //接收串口传入的设置时间的值 u8 USART_Scanf(u32 value) { u32 index = 0; u32 tmp[2] = {0, 0}; while (index < 2) { /* Loop until RXNE = 1 */ while (USART_GetFlagStatus(USART2, USART_FLAG_RXNE) == RESET); //等到接受数据为非空 tmp[index++] = (USART_ReceiveData(USART2)); if ((tmp[index - 1] < 0x30) || (tmp[index - 1] > 0x39))//检查是否输入有效 0~9 的数字 { printf("\n\rPlease enter valid number between 0 and 9"); index--; } }// /* Calculate the Corresponding value */ index = (tmp[1] - 0x30) + ((tmp[0] - 0x30) * 10); /* Checks */ if (index > value)//如果上一步计算出来的值大于 23(小时)或者 59(分\秒),value 是传进来的值 { printf("\n\rPlease enter valid number between 0 and %d", value); return 0xFF;//返回 0xFF 的话,外面函数的 while 语句不会退出,会继续设置时间 } return index; } //主函数一直在执行,但是标志 TimeDisplay!=1 的话不会显示,只等中断把 TimeDisplay 置 1 以后才显示 void Time_Show(void) { u16 temp; printf("\n\r"); /* Infinite loop */ while (1) { /* If 1s has paased */ if (TimeDisplay == 1) { /* Display current time */ temp=RTC_GetCounter(); Time_Display(temp);//RTC_GetCounter 获取 RTC 计数器的值 TimeDisplay = 0; } } } //中断函数 void RTC_IRQHandler(void)

//每隔一秒把标志 TimeDisplay 置 1,main 函数中的 showtime 显示一次时间.理论上是一秒一发 { if (RTC_GetITStatus(RTC_IT_SEC) != RESET) { /* Clear the RTC Second interrupt */ //RTC_IT_SEC 秒中断使能

RTC_ClearITPendingBit(RTC_IT_SEC); //清除中断 /* Enable time update */ TimeDisplay = 1; /* Wait until last write operation on RTC registers has finished */ RTC_WaitForLastTask(); /* Reset RTC Counter when Time is 23:59:59 */ if (RTC_GetCounter() == 0x00015180)//获取 RTC 计数器的值 { RTC_SetCounter(0x0);//满 24 小时一个循环设置成 0 秒 0x00015180=24hours

/* Wait until last write operation on RTC registers has finished */ RTC_WaitForLastTask();//等待最近一次对 RTC 寄存器的写操作完成 } } }

2.9PWR 电源管理(2011-12-2) 2.9.1 理论
很多单片机都有低功耗模式,STM32 也不例外。在系统或电源复位以后,微控制器处于运行状态。运行状态下 的 HCLK 为 CPU 提供时钟,内核执行程序代码。当 CPU 不需继续运行时,可以利用多个低功耗模式来节省功耗, 例如等待某个外部事件时。用户需要根据最低电源消耗,最快速启动时间和可用的唤醒源等条件,选定一个最佳的 低功耗模式。 STM32的低功耗模式有3种: 1)睡眠模式(CM3内核停止,外设仍然运行) 2)停止模式(所有时钟都停止) 3)待机模式(1.8V内核电源关闭) 在运行模式下,我们也可以通过降低系统时钟关闭 APB 和 AHB 总线上未被使用的外设的时钟来降低功耗。三种低 功耗模式一览表:

在这三种低功耗模式中,最低功耗的是待机模式,在此模式下,最低只需要 2uA 左右的电流。停机模式是次低功耗 的, 其典型的电流消耗在 20uA 左右。 最后就是睡眠模式了。 用户可以根据自己的需求来决定使用哪种低功耗模式。

2.9.2 编程实例
本节在 2.8 的 RTC 的基础上添加三种节能的模式。 1. 在工程中添加 PWR 文件

2. 在使用到休眠函数的文件中

3. 打开 PWR 和 BK 的时钟电源 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);//开 PWR 时钟&BKP 时钟 4. 设置好管脚,我们这里用的有两个外部中断来实现唤醒功能,一个为 PA0(wkup)和一个为普通的中断,其中, 用 wkup 在唤醒待机模式时,要在 pwr 的设置中调用 void PWR_WakeUpPinCmd(FunctionalState NewState)使能, 如只是当一般的中断使用则不需要。中断的设置和前面提到的一样,这里就不多说了。 5. 在需要调用的地方调用函数。 那么,如何分别进入这三种模式呢: 睡眠:在我们这个库中,没有进入睡眠模式的函数,所以我在网上找了一个:

void PWR_EnterSLEEPMode( u8 PWR_SLEEPEntry) { *(vu32 *) SCB_SysCtrl &= ~SysCtrl_SLEEPDEEP_Set;

// Clear SLEEPDEEP bit // Select SLEEP mode entry

if(PWR_SLEEPEntry == PWR_SLEEPEntry_WFI) __WFI(); // Request Wait For Interrupt else __WFE(); } 同时将下面的代码添加到 stm32f10x_pwr.h 中: /* SLEEP mode entry */ #define PWR_SLEEPEntry_WFI ((u8)0x01) // Request Wait For Event

#define PWR_SLEEPEntry_WFE ((u8)0x02) 唤醒模式,任意一个中断都可以唤醒。 停止:PWR_EnterSTOPMode(PWR_Regulator_ON,PWR_STOPEntry_WFI or PWR_STOPEntry_WFE) PWR_EnterSTOPMode(PWR_Regulator_LowPower,PWR_STOPEntry_WFI or PWR_STOPEntry_WFE) 唤醒模式:任意外部中断。 待机:PWR_EnterSTANDBYMode(void); 唤醒模式:如上表中的四种,如用 wkup,需要调用 void PWR_WakeUpPinCmd(FunctionalState NewState) 6.在调用睡眠或者停止模式后,要重新初始化时钟,而待机模式重启后,则相当于 NRST

2.9.3 补充:电源学习
STM32 的电源组成,其主要由 3 个部分:独立的 A/D 供电电压和参考电压、备份电压、电压调节器。 1.A/D 供电电压和参考电压 为了提高转换的精确度,ADC 使用一个独立的电源供电(VDDA,范围:2.4V~3.6V) ,过滤和屏蔽来自 PCB 上的 毛刺干扰。 参考电压 VREF+和 VREF-分别是正和负极,一般 VREF-接地,VREF+的范围为 VREF-~VDDA,这个电源 100 脚封装芯片和其他封装芯片不同, 脚的这个电压和 A/D 供电电压是独立的, 100 而其他封装它们是内部直接连接的。 2.备份电压 备份电压指的是备份域使用的供电电源,也就是 VBAT 引脚的供电,使用电池或其他电源连接到 VBAT 脚上,当 VDD 断电时,可以保存备份寄存器的内容。VBAT 脚也为 RTC 供电,这保证当主要电源被切断时 RTC 能继续工作。 如果应用中没有使用外部电池,VBAT 必须连接到 VDD 引脚上。 3.电压调节器 电压调节器为 STM32 提供所需的 1.8V 电源,也就是内部把你提供的 3.3V 转换成 1.8V,供 STM32 使用,其主 要有三种工作模式: -运行模式:以正常功耗模式提供 1.8V 电源(内核,内存和外设) 。 -停止模式:以低功耗模式提供 1.8V 电源,以保存寄存器和 SRAM 的内容。 -待机模式:停止供电。除了备用电路和备份域外,寄存器和 SRAM 的内容全部丢失。 4.电源管理器(PVD) 电源管理器的第 1 部分是上电复位(POR)和掉电复位(PDR):当电源小于 POR 和/或 PDR 门限时,系统处于复位 状态,其他时间处于正常工作状态。 电源管理器的第 2 部分是可编程电压监测器:用户可以利用 PVD 对 VDD 电压与电源控制寄存器中的 PLS[2:0] 位进行比较来监控电源, 这几位选择监控电压的阀值。 通过设置 PVDE 位来使能 PVD。 电源控制/状态寄存器中的 PVDO 标志用来表明 VDD 是高于还是低于 PVD 的电压阀值。 该事件在内部连接到外部中断的第 16 线。当 VDD 下降到 PVD 阀值以下和(或)当 VDD 上升到 PVD 阀值之上时,根据外部中断第 16 线的上升/下降边沿触发设置,就会产生 PVD 中断。这一特性可用于用于执行紧急关闭任务。

电源管理器的第 3 部分是低功耗模式,共有睡眠、停止、待机 3 种低功耗模式。 睡眠模式:在睡眠模式下,CPU 时钟处于停止状态,但是所有的外设继续运行(除非它们被关闭)电源功耗相应地 减少。任一中断或唤醒事件可将微处理器从睡眠模式中唤醒。在睡眠模式下,所有的 SRAM 和寄存器内的内容被保 存下来。睡眠模式有 2 种,一种是通过执行 WFI 进入睡眠模式,任意一个被嵌套向量中断控制器响应的外设中断都 能将系统从睡眠模式唤醒 ;令一种是执行 WFE 指令进入睡眠模式,外设控制寄存器中被使能的中断事件,但该中 断事件未被嵌套向量中断控制器使能,或者设置为事件模式的外部中断线上的事件可以将系统从睡眠模式唤醒。 停止模式:停止模式是在 Cortex-M3 的深睡眠模式基础上结合了外设的时钟控制机制,在停止模式下电压调节器可 运行在正常或低功耗模式。此时在 1.8V 供电区域的的所有时钟都被停止,PLL、HSI 和 HSE,RC 振荡器的功能被禁 止,SRAM 和寄存器内容被保留下来。 进入停止模式: – 设置 Cortex-M3 系统控制寄存器中的 SLEEPDEEP 位 – 清除电源控制寄存器(PWR_CR)中的 PDDS 位 –通过设置 PWR_CR 中 LPDS 位选择电压调节器的模式 在以上条件下执行 WFI 或 WFE 指令进入停止模式。 退出停止模式: 如果是使用 WFI 进入的停止模式,则任一外部中断线产生中断可以唤醒系统,如果是使用 WFE 禁止的停止模式, 则任一外部中断线产生事件可以唤醒系统。 待机模式:待机模式可实现系统的最低功耗。该模式是在 Cortex-M3 深睡眠模式时关闭电压调节器。整个 1.8V 供电 区域被断电。 PLL、 和 HSE 振荡器也被断电。 HSI SRAM 和寄存器内容丢失。 只有备份的寄存器和待机电路维持供电。 进入待机模式 在以下条件下执行 WFI 或 WFE 指令进入待机模式: – 设置 Cortex-M3 系统控制寄存器中的 SLEEPDEEP 位 – 设置电源控制寄存器(PWR_CR)中的 PDDS 位 – 清除电源控制/状态寄存器(PWR_CSR)中的 WUF 位被 退出待机模式 当一个外部复位(NRST 引脚)、IWDG 复位、WKUP 引脚上的上升沿或 RTC 闹钟事件发生时,微控制器从待机模式退 出。 最后是低功耗模式下的自动唤醒:RTC 可以唤醒低功耗模式下的微控制器(自动唤醒模式) 。RTC 可以按照可编程周 期从停止或待机模式下唤醒微控制器。通过对备份区域控制寄存器的 RTCSEL[1:0]位的编程,三个 RTC 时钟源中的二 个可以选作实现此功能: -低功耗 32.768kHz 外部晶振(LSE OSC) -低功耗内部 RC 振荡器(LSI RC) 为了用 RTC 闹钟事件将微处理器从停止模式下唤醒,必须进行如下操作: -配置外部中断线 17 为上升沿触发。 -配置 RTC 使其可产生 RTC 闹钟事件。 如果要从待机模式中唤醒,不必配置外部中断线 17。

2.10 LCD 的使用(20111208) 2.10.1 LCD 使用基本概念
1. LCD/LCM 的基本概念
液晶显示器(Liquid Crystal Display: LCD)的构造是在两片平行的玻璃当中放置液态的晶体, 两片玻璃中间有许多垂 直和水平的细小电线,透过通电与否来控制杆状水晶分子改变方向,将光线折射出来产生画面。 LCM(LCD Module)即 LCD 显示模组、液晶模块,是指将液晶显示器件,连接件,控制与驱动等外围电路,PCB 电路板,背光源,结构件等装配在一起的组件。在平时的学习开发中,我们一般使用的是 LCM,带有驱动 IC 和 LCD

屏幕等多个模块。

2.

FSMC 的基本概念

在 STM32 上开发 LCD 显示,可以有两种方式来对 LCD 进行操作,一种是通过普通的 IO 口,连接 LCM 的相应引 脚来进行操作,第 2 种是通过 FSMC 来进行操作,一般常用 FSMC 来操作。 可变静态存储控制器(Flexible Static Memory Controller: FSMC) 是 STM32 系列中内部集成 256 KB 以上 FlaSh,后 缀为 xC、xD 和 xE 的高存储密度微控制器特有的存储控制机制。之所以称为“可变”,是由于通过对特殊功能寄存器 的设置,FSMC 能够根据不同的外部存储器类型,发出相应的数据/地址/控制信号类型以匹配信号的速度,从而使 得 STM32 系列微控制器不仅能够应用各种不同类型、不同速度的外部静态存储器,而且能够在不增加外部器件的 情况下同时扩展多种不同类型的静态存储器,满足系统设计对存储容量、产品体积以及成本的综合要求。 FSMC 有很多优点: 1)支持多种静态存储器类型。STM32 通过 FSMC 可以与 SRAM、ROM、PSRAM、NOR Flash 和 NAND Flash 存储器的 引脚直接相连。 2.)支持丰富的存储操作方法。FSMC 不仅支持多种数据宽度的异步读/写操作,而且支持对 NOR、PSRAM、NAND 存储器的同步突发访问方式。 3)支持同时扩展多种存储器。FSMC 的映射地址空间中,不同的 BANK 是独立的,可用于扩展不同类型的存储器。 当系统中扩展和使用多个外部存储器时,FSMC 会通过总线悬空延迟时间参数的设置,防止各存储器对总线的访 问冲突。 4)支持更为广泛的存储器型号。通过对 FSMC 的时间参数设置,扩大了系统中可用存储器的速度范围,为用户提供 了灵活的存储芯片选择空间。 5)支持代码从 FSMC 扩展的外部存储器中直接运行,而不需要首先调入内部 SRAM。 FSMC 包含两类控制器: 1)1 个 NOR 闪存/SRAM 控制器,可以与 NOR 闪存、SRAM 和 PSRAM 存储器接口。 2)1 个 NAND 闪存/PC 卡控制器,可以与 NAND 闪存、PC 卡,CF 卡和 CF+存储器接口。

从上图可以看出, FSMC 对外部设备的地址映像从 0x6000 0000 开始, 0x9FFF FFFF 结束, 到 共分 4 个地址块, 每个地址块 256M 字节。可以看出,每个地址块又分为 4 个分地址块,大小 64M。对 NOR 的地址映像来说,我们 可以通过选择 HADDR[27:26]来确定当前使用的是哪个 64M 的分地址块,如下页表格。而这四个分存储块的片选,

则使用 NE[4:1]来选择。数据线/地址线/控制线是共享的。

这里的 HADDR 是需要转换到外部设备的内部 AHB 地址线,每个地址对应一个字节单元。因此,若外部设备 的地址宽度是 8 位 , HADDR[25:0]与 STM32 的 CPU 引脚 FSMC_A[25:0]一一对应, 的 则 最大可以访问 64M 字节的 空间。若外部设备的地址宽度是 16 位的,则是 HADDR[25:1]与 STM32 的 CPU 引脚 FSMC_A[24:0]一一对应。在应 用的时候,可以将 FSMC_A 总线连接到存储器或其他外设的地址总线引脚上。

HADDR
mcu

FSMC_A

LCM

控制器产生所有驱动这些存储器的信号时序: (不重要) 1)16 位数据线,用于连接 8 位或 16 位的存储器; 2)26 位地址线,最多可连续 64MB 的存储器(这里不包括片选线) ; 3)5 位独立的片选信号线; 4)1 组适合不同类型存储器的控制信号线: 控制读/写操作 与存储器通信,提供就绪/繁忙信号和中断信号 与所用配置的 PC 卡接口:PC 存储卡、PC I/O 卡和真正的 IDE 接口 注意:FSMC 只是提供了一个控制器,并不提供相应的存储设备,至于外设接的是什么设备,完全是由用户自己选 择,只要能用于 FSMC 控制,就可以,像本次实验中,我们接的就是 LCM。

3.

本例中 FSMC 的使用

由于本例只是利用 FSMC 对 LCM 进行操作,因此不用完全懂得 FSMC 的所有功能,而是懂得一部分相应的操作 即可。 1)FSMC 包括哪几个部分 FSMC 包含以下 4 个模块: · AHB 接口(包含 FSMC 配置寄存器) · NOR 闪存和 PSRAM 控制器 · NAND 闪存和 PC 卡控制器 · 外部设备接口 需要注意的是,FSMC 可以请求 AHB 进行数据宽度操作。如果 AHB 操作的数据宽度大于外部设备(NOR 或 NAND 或 LCD)的宽度,此时 FSMC 将 AHB 操作分割成几个连续的较小的数据宽度,以适应外部设备的数据宽度。 2)FSMC 对外部设备的地址映像

FSMC 对外部设备的地址映像从 0x6000 0000 开始, 0x9FFF FFFF 结束, 到 一共 4 个地址块, 每个地址块 256MB, 而每个地址块又分成 4 个分地址块, 大小为 64MB。 于 NOR 的地址映像来说, 对 我们可以通过选择 HADDR[27:26] 来 确定当前使用的是哪个 64M 的分地址块。而这四个分存储块的片选,则使用 NE[4:1]来选择。数据线/地址线/控制 线是共享的。 这里的 HADDR 是需要转换到外部设备的内部 AHB 地址线,每个地址对应一个字节单元。因此,若外部设备的 地址宽度是 8 位 , HADDR[25:0]与 STM32 的 CPU 引脚 FSMC_A[25:0]一一对应, 的 则 最大可以访问 64M 字节的空间。 若外部设备的地址宽度是 16 位的, 则是 HADDR[25:1]与 STM32 的 CPU 引脚 FSMC_A[24:0]一一对应。 在应用的时候, 可以将 FSMC_A 总线连接到存储器或其他外设的地址总线引脚上。

4.

ILI9325
而 LCM 中的驱动 IC 就是采用的 ILI9325。 ILI9325 的功能很多, 在此无法一一说明, 但是参考 ILI9325 的 Datasheet

我们发现有几个引脚还是非常重要的,而只要操作好了这几个引脚,基本上就可以实现简单的对 LCM 的控制了。 nCS: IC 的片选信号。如果是低电平,则 ILI9325 是被选中,并且可以进行操作,如果是高电平,这不被选中。 RS: 寄存器选择信号。如果是低电平,则选择的是索引或者状态寄存器,如果是高电平,则选择控制寄存器。 nWR/SCL: 写使能信号,低电平有效。 nRD: 读使能信号,低电平有效。 ILI9325 的寄存器非常多,详细的各个寄存器的功能请参考 ILI9325 的 Datasheet。在对 ILI9325 进行操作时,应 该先写地址,然后再写数据,设置好各个寄存器之后,ILI9325 就可以开始工作了。

5.

电路设计

1)信号线的连接 STM32F10x FSMC 有 4 个不同的 banks,每一 个 64MB,可支持 NOR 以及其他类似的存储器。这些外部设备的地 址线、 数据线和控制线是共享的。 每个设备的访问时通过片选信号来决定的, 而每次只能访问一个设备。 我们的 LCM 就是连接在 NOR 的 bank 上面。 FSMC_D[15:0]:16bit 的数据总线,连接 ILI9325 的数据线; FSMC_NEx:分配给 NOR 的 256MB 的地址空间还可以分为 4 个 banks,每一个区用来分配一个外设,这 4 个外 设分别就是 NE1-NE4; FSMC_NOE:输出使能,连接 ILI9325 的 nRD 引脚; FSMC_NWE:写使能,连接 ILI9325 的 nWR 引脚; FSMC_Ax:用在 LCD 显示 RAM 和寄存器之间进行选择的地址线,这个和 ILI9325 的 RS 引脚相连。该线可用任意 一根地址线,范围是 FSMC_A[25:0]。当 RS=0 时,表示读写寄存器,RS=1 时,表示读写数据 RAM。 其实关于 RS 的表述也并不完全准确,应该这么理解,RS=0 的时候,向这个地址写的数表示了选择什 么寄存器进行操作,然而要对寄存器进行什么操作,则要看当 RS=1 时,送入的数据了。 关于地址的计算,如果我们选择 NOR 的第一个存储区,并且使用 FSMC_A16 来控制 ILI9325 的 RS 引脚,则如果 要访问寄存器地址(RS=0), 那么地址是 0x6000 0000(起始地址), 如果要访问数据区(RS=1), 那么基地址应该是 0x6002 0000。 有人会问,为什么不是 0x6001 0000 呢?因为 FSMC_A16=1。因为在前文中已经说过,若外部设备的地址宽度 是 16 位的,则是 HADDR[25:1]与 STM32 的 CPU 引脚 FSMC_A[24:0]一一对应。也就是说,内部产生的地址应该要左 移一位,FSMC_A16=1,代表着第 17 位为 1,而不是第 16 位为 1。如果外部设备的地址宽度是 8 位的话,则不会出 现这个问题。 再举一个例子, 如果选择 NOR 的第 4 个存储区, 使用 FSMC_A0 来控制 RS 引脚,则访问数据区的地址为 0x6000 0002,访问 LCD 寄存器的地址为:0x6000 0000。 2)时序问题 一般使用模式 2 来做 LCD 的接口控制,不使用外扩模式。并且读写操作的时序一样。此种情况下,我们需要使 用 3 个参数:ADDSET、DATAST、ADDHOLD。时序的计算需要根据 NOR 闪存存储器的特性和 STM32F10x 的时钟 HCLK 来计算这些参数。 写或读访问时序是存储器片选信号的下降沿与上升沿之间的时间, 这个时间可以由 FSMC 时序参数的函数计算得到: 写/读访问时间 = ((ADDSET + 1) + (DATAST + 1)) × HCLK

在写操作中,DATAST 用于衡量写信号的下降沿与上升沿之间的时间参数: 写使能信号从低变高的时间 = t WP = DATAST × HCLK 为了得到正确的 FSMC 时序配置,下列时序应予以考虑: 最大的读/写访问时间、不同的 FSMC 内部延迟、不同的存储器内部延迟 因此得到: ((ADDSET + 1) + (DATAST + 1)) × HCLK = max (t WC , t RC ) DATAST × HCLK = tWP DATAST 必须满足:DATAST = (tAVQV+ tsu(Data_NE) + tv(A_NE) )/HCLK – ADDSET – 4 由于我没有找到 ILI9325 的这些时序的参数,所以就参考了一些以前别人写的程序里面的时序配置: 当 HCLK 的频率是 72MHZ,使用模式 B,则有如下时序: 地址建立时间:0x1 地址保持时间:0x0 数据建立时间:0x5

6.

程序编写步骤

对于程序的编写,一般步骤是: 1)初始化 RCC; 2)初始化 GPIO; 3)初始化 FSMC; 4)初始化 LCD; 5)调用函数写东西。 其中 RCC、GPIO、FSMC 的初始化函数在 STM32 的固件库中已经有相应的函数,在此就不一一赘述了。FSMC 的初始化参数很多 ,且基本上可以通用,因此在此也不对每一个参数具体有什么用进行解释了,一般来说,用通 用参数就足够普通的开发了。 而对 LCD 的初始化,则需要自己编写相应的代码。基本原则是,首先向寄存器地址写入需要操作的寄存器地址 (代码) 然后再根据 Datasheet, , 向数据区地址写入相应的数据, 以实现某些操作。 具体的操作在 ILI9325 的 Datasheet 第 8 节 Register Descriptions 中,有详细的解释。而 LCD 的初始化只要按照 Datasheet 里面的,把每一个寄存器都给 配置好了,就没有问题了。而这些寄存器的配置,大部分都是通用的,只是有一些屏幕方向选择,坐标系等会略有 差别。 LCD 配置好之后,就可以往 GRAM 里面写入图像数据了,在这里推荐一个软件“Image2LCD” ,这个软件能读取 图像,然后生成 C 代码的数据,只要将这些生成的代码直接写入 GRAM 中,就可以显示出图像了。不过要记住,在 图像转换的时候,输出数据类型选择“C 语言数组” ,扫描模式选择“水平扫描” ,输出灰度“16 位真彩色” ,最大 宽度和高度 “320” “240” “高位在前(MSB First)” 这些配置都是和 ILI9325 的寄存器配置相对应的, 勾选 。 如果说 ILI9325 的配置和本文中的不一样,则需要相应的选择其他的选项。

2.10.2 怎么向屏幕写一个汉字(2011-12-9)
步 骤 一 : 打 开 工 程 模 板 , 然 后 把 各 个 需 要 用 到 的 **.c 文 件 添 加 到 工 程 中 , 如 下 图 所 示 :

步骤二:初始化 RCC void RCC_Configuration(void) { /* Setup the microcontroller system. Initialize the Embedded Flash Interface, initialize the PLL and update the SystemFrequency variable. */ SystemInit(); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD|RCC_APB2Periph_GPIOE|RCC_APB2Periph_AFIO| RCC_APB1Periph_PWR, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_FSMC, ENABLE); } 步骤三:初始化 GPIO 在这一部分,我们必须知道硬件的连接电路: 显示器采用2.4” TFT320X240LCD(控制器ILI9325), 采用CPU 的FSMC 功能, LCD 片选CS ——FSMC_NE1(P88); FSMC_A16(P58)——LCD 的RS 选择; FSMC_nWE(P86)——LCD 的/WR; FSMC_nOE(P85)——LCD 的/RD; LCD 的RESET ——CPU 的PE1(P98)(LCD-RST); FSMC_D0---FSMC_D15 和LCD 的DB1-DB8 DB10-DB17 相互连接, LCD 寄存器地址为:0x6000 0000, LCD 数据区地址: 0x6002 0000。

void GPIO_Cfg(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 |GPIO_Pin_6|GPIO_Pin_3; GPIO_Init(GPIOD, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_Init(GPIOE, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 ; GPIO_Init(GPIOE, &GPIO_InitStructure); // // //D1

//LCD-RST

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10 | GPIO_Pin_14 | GPIO_Pin_15; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;

GPIO_Init(GPIOD, &GPIO_InitStructure); /* Set PE.07(D4), PE.08(D5), PE.09(D6), PE.10(D7), PE.11(D8), PE.12(D9), PE.13(D10), PE.14(D11), PE.15(D12) as alternate function push pull */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7 | GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10 | GPIO_Pin_11 | GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15; GPIO_Init(GPIOE, &GPIO_InitStructure); /* NE1 configuration */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; GPIO_Init(GPIOD, &GPIO_InitStructure); /* RS */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11 ; GPIO_Init(GPIOD, &GPIO_InitStructure); //RS

/* RST */ GPIO_SetBits(GPIOD, GPIO_Pin_7); //CS=1 GPIO_SetBits(GPIOD, GPIO_Pin_14| GPIO_Pin_15 |GPIO_Pin_0 | GPIO_Pin_1);

//低 8 位

GPIO_SetBits(GPIOE, GPIO_Pin_7 | GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10); //低 8 位 GPIO_ResetBits(GPIOE, GPIO_Pin_0); GPIO_ResetBits(GPIOE, GPIO_Pin_1); //RESET=0 GPIO_SetBits(GPIOD, GPIO_Pin_4); GPIO_SetBits(GPIOD, GPIO_Pin_5); } 步骤四:初始化 fsmc,这里直接调用 FSMC_LCD_Init(); //RD=1 //WR=1

步骤五:初始化 LCD

在这一步,要特别注意画方框的三句程序: LCD_WR_CMD(0x0003, (1<<12)|(3<<4)|(1<<3)); 这一句是对 ILI9325 寄存器 R03 的设置。 寄存器 03H

AM : 控制 GRAM 更新方向的控制位 AM = 0: 在水平方向更新地址 AM = 1: 在垂直方向更新地址 特别注意:我们这里用的 lcd_disp24.c 是在 AM=1 下使用的。 这个地方对 AM 的选择将直接影响 img2lcd 软件的扫描方式控制项,这一位就是控制扫描方式的。 I/D[1:0] : 当 更 新 显 示 区 域 的 一 个 像 素 点 的 时 候 , 控 制 AC 是 增 加 1 还 是 减 少 1 , 具 体 参 考 下 图

I/D[1:0] 的正确设置才能正确的显示图片, 比如有时候发现显示出来的图片和输入 img2lcd 的图片方向是左右方向是 反的,或者上下 或者都是反的,那就是 需要修改这个的地方了,可以根据上面的方向来选择合适的 I/D。 //改变原点 LCD_WR_CMD(0x0001, 0x0000); LCD_WR_CMD(0x0060, 0xA700);

在我们这块 LCM 的原点设置如下:

其中,排线是在右边。 步骤六:在设置好这些以后,就可以调用 lcd_disp24.c 的函数来显示来写所需的字符了。 如 int main(void) { //uint16_t a; /* System Clocks Configuration **********************************************/ int line1x = 210; int line1y = 20; Init_All(); LCD_Clear(WHITE); //让屏幕显示为一个白色底色 while (1) { writeString("你",line1x,line1y,BLACK);//函数的具体说明可以看 lcd_disp24.c,line1x 和 line1y 是最靠近原点的点 LCD_Fill(0,0,10,10,RED); LCD_DrawLine(0,0,100,50); } }

2.10.3 怎么向屏幕写任意汉字(2011-12-9)
这里我们需要用到软件《PCtoLCD2002 完美版》 ,其界面如下所示: 步骤一:设置红框的两处地方

步骤二:点击“选项”按钮

其中,取模方式和取模走向必须和 R03h 这个寄存器的设置匹配,具体设置这里还搞不清楚,在实际应用中,试着 调出正确的显示汉字即可。 步骤三:在生成字膜后,把生成的代码复制到 characterlib.h 文件中的对应字号的结构体中即可。

2.10.4 关于屏幕显示的拓展问题(2011-12-11)
C:\Documents and Settings\HP\桌面\建工给的东东\LCD 屏幕\LCD 屏幕\复件 12 FSMC 接口 TFT 显示图片

1)48 号字体的显示
在这里,我编写了 writeString48(unsigned char *pcStr, unsigned short x0, unsigned int y0, unsigned int color)来进行 屏幕的横屏 48 号字体显示。 A、 首先在 hz16.h 进行类型的定义 ,如下所示

B、在 hz16.c 文件中添加 findHzIndex48 函数 INT16U findHzIndex48(uchar *hz) { INT16U i=0; FNT_GB48 *ptGb48 = (FNT_GB48 *)GBHZ_48;

/* 在自定义汉字库在查找所要显示 */ /* 的汉字的位置 */

while(ptGb48[i].Index[0] > 0x80) { if ((*hz == ptGb48[i].Index[0]) && (*(hz+1) == ptGb48[i].Index[1])) { return i; } i++;

if(i > (sizeof(GBHZ_48) / sizeof(FNT_GB48) - 1)) { break; } } return 0;

/* 搜索下标约束

*/

} C、 在 characterlib.h 添加相应的字膜 字模生成时,选择逐列式,顺向,先上下翻转,再顺时针转 90 度。 const FNT_GB48 GBHZ_48[] = { "我", 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x00,0x18,0x3C,0x00,0x00,0x00,0x00, 0x7C,0x3C,0x40,0x00,0x00,0x03,0xFE,0x38,0x70,0x00,0x00,0x1F,0xF0,0x38,0x3C,0x00, 0x03,0xFF,0x00,0x38,0x1E,0x00,0x0C,0x03,0x00,0x38,0x0E,0x00,0x00,0x03,0x00,0x38, 0x0F,0x00,0x00,0x03,0x00,0x38,0x0F,0x00,0x00,0x03,0x00,0x38,0x06,0x00,0x00,0x03, 0x00,0x38,0x00,0x00,0x00,0x03,0x00,0x38,0x00,0x40,0x00,0x03,0x00,0x38,0x00,0xE0, 0x00,0x03,0x00,0x38,0x01,0xF0,0x0F,0xFF,0xFF,0xFF,0xFF,0xF8,0x00,0x03,0x00,0x38, 0x00,0x00,0x00,0x03,0x00,0x38,0x00,0x00,0x00,0x03,0x00,0x38,0x04,0x00,0x00,0x03, 0x00,0x38,0x0E,0x00,0x00,0x03,0x00,0x18,0x0F,0x00,0x00,0x03,0x00,0x18,0x0F,0x00, 0x00,0x03,0x01,0x9C,0x1E,0x00,0x00,0x03,0x0E,0x1C,0x3C,0x00,0x00,0x03,0x78,0x1C, 0x38,0x00,0x00,0x03,0xC0,0x1C,0x78,0x00,0x00,0x3F,0x00,0x1C,0xF0,0x00,0x03,0xFF, 0x00,0x1C,0xE0,0x00,0x3F,0xE3,0x00,0x0F,0xC0,0x00,0x1F,0x83,0x00,0x0F,0x80,0x00, 0x0E,0x03,0x00,0x0F,0x80,0x00,0x00,0x03,0x00,0x0F,0x00,0x00,0x00,0x03,0x00,0x1F, 0x80,0x08,0x00,0x03,0x00,0x3F,0x80,0x08,0x00,0x03,0x00,0x71,0xC0,0x10,0x00,0x03, 0x01,0xE1,0xE0,0x10,0x00,0x03,0x03,0x80,0xF0,0x10,0x00,0x03,0x0E,0x00,0x7C,0x18, 0x00,0x07,0x18,0x00,0x3E,0x18,0x01,0xFF,0x60,0x00,0x1F,0xF8,0x00,0x7F,0x00,0x00, 0x07,0xF8,0x00,0x1E,0x00,0x00,0x03,0xF8,0x00,0x0C,0x00,0x00,0x00,0x7C,0x00,0x08, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, //上面的数量为 48*48/8 "在", 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x00, 0x00,0x00,0x00,0x00,0x07,0x00,0x00,0x00,0x00,0x00,0x07,0x80,0x00,0x00,0x00,0x00, 0x07,0x00,0x00,0x00,0x00,0x00,0x0F,0x00,0x00,0x00,0x00,0x00,0x0E,0x00,0x00,0x00, 0x00,0x00,0x0E,0x00,0x00,0x40,0x00,0x00,0x1C,0x00,0x00,0xE0,0x00,0x00,0x1C,0x00, 0x01,0xF0,0x1F,0xFF,0xFF,0xFF,0xFF,0xF8,0x00,0x00,0x38,0x00,0x00,0x00,0x00,0x00, 0x30,0x00,0x00,0x00,0x00,0x00,0x70,0x18,0x00,0x00,0x00,0x00,0xE0,0x1E,0x00,0x00, 0x00,0x00,0xE0,0x1C,0x00,0x00,0x00,0x01,0xC0,0x1C,0x00,0x00,0x00,0x01,0x80,0x1C, 0x00,0x00,0x00,0x03,0x80,0x1C,0x00,0x00,0x00,0x07,0x00,0x1C,0x00,0x00,0x00,0x2E, 0x00,0x1C,0x00,0x00,0x00,0x3C,0x00,0x1C,0x00,0x00,0x00,0x38,0x00,0x1C,0x01,0x80, 0x00,0x38,0x00,0x1C,0x03,0xC0,0x00,0x78,0xFF,0xFF,0xFF,0xC0,0x00,0xF8,0x60,0x1C, 0x00,0x00,0x01,0xD8,0x00,0x1C,0x00,0x00,0x03,0x18,0x00,0x1C,0x00,0x00,0x06,0x18, 0x00,0x1C,0x00,0x00,0x0C,0x18,0x00,0x1C,0x00,0x00,0x10,0x18,0x00,0x1C,0x00,0x00, 0x20,0x18,0x00,0x1C,0x00,0x00,0x00,0x18,0x00,0x1C,0x00,0x00,0x00,0x18,0x00,0x1C, 0x00,0x00,0x00,0x18,0x00,0x1C,0x00,0x00,0x00,0x18,0x00,0x1C,0x00,0x00,0x00,0x18, 0x00,0x1C,0x00,0x00,0x00,0x18,0x00,0x1C,0x00,0x00,0x00,0x18,0x00,0x1C,0x00,0x60, 0x00,0x18,0x00,0x1C,0x00,0xF0,0x00,0x38,0x00,0x1C,0x01,0xF8,0x00,0x39,0xFF,0xE3, 0xFE,0x00,0x00,0x38,0x00,0x00,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x00,0x00,0x30, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,

};

const unsigned char ASCII48[][144] = { //" " 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, //“!” 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0x80,0x00,0x0F,0x80,0x00,0x0F, 0x80,0x00,0x0F,0x80,0x00,0x0F,0x80,0x00,0x0F,0x80,0x00,0x0F,0x80,0x00,0x0F,0x80, 0x00,0x07,0x80,0x00,0x07,0x80,0x00,0x07,0x80,0x00,0x07,0x00,0x00,0x07,0x00,0x00, 0x07,0x00,0x00,0x07,0x00,0x00,0x07,0x00,0x00,0x07,0x00,0x00,0x03,0x00,0x00,0x03, 0x00,0x00,0x03,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x07,0x80,0x00,0x0F,0x80,0x00,0x1F,0xC0,0x00,0x0F,0xC0,0x00, 0x0F,0x80,0x00,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, }; 单个字符的数量为 24*48/8

D、writestring48 函数如下: void writeString48(unsigned char *pcStr, unsigned short x0, unsigned int y0, unsigned int color) { unsigned int usIndex; unsigned int usWidth = 0; FNT_GB48 *ptGb48 = 0; ptGb48 = (FNT_GB48 *)GBHZ_48; while(1) { if(*pcStr == 0) { break; } y0 = y0 + (usWidth); /* 调节字符串显示松紧度 */ if(*pcStr > 0x80) { /* 判断为汉字 */ if((y0 + 48) >= Gus_LCM_YMAX) { /* 检查剩余空间是否足够 y0 = 0; x0 = x0 - 48; /* 改变显示坐标 /* 字符串结束 */ /* */

*/ */

if(x0 >= Gus_LCM_XMAX) { x0 = 0; } } usIndex = findHzIndex48(pcStr);

/* 纵坐标超出

*/

usWidth = __writeOneHzChar48((unsigned char *)&(ptGb48[usIndex].Msk[0]), x0, y0, color); /* 显示字符 */ pcStr += 2;//一个汉字为两个字节 } else { if (*pcStr == '\r') { x0 = x0 - 48; if(x0 >= Gus_LCM_XMAX) { x0 = 0; } pcStr++; usWidth = 0; continue; } else if (*pcStr == '\n') { y0 = 0; pcStr++; usWidth = 0; continue; } else { if((y0 + 24) >= Gus_LCM_YMAX) { y0 = 0; x0 = x0 - 48; if(x0 >= Gus_LCM_XMAX) { x0 = 0; } /* 检查剩余空间是否足够 /* 改变显示坐标 /* 纵坐标超出 */ */ */ /* 对齐到起点 */ /* 判断为非汉字 /* 换行 /* 改变显示坐标 /* 纵坐标超出 */ */ */ */

} usWidth = __writeOneASCII48((unsigned char *)&ASCII48[(*pcStr - 0x20)][0], x0, y0, color); /* ASCII 码表 21H 的值对应区位码 3 区*/ pcStr += 1;//一个 ascii 码占一个字节 } } } //恢复窗体大小 LCD_WR_CMD(R80, 0x0000); //水平方向 GRAM 起始地址 LCD_WR_CMD(R81, 0x00EF); //水平方向 GRAM 结束地址 LCD_WR_CMD(R82, 0x0000); //垂直方向 GRAM 起始地址 LCD_WR_CMD(R83, 0x013F); //垂直方向 GRAM 结束地址 } 同时,在头文件 LCD_disp24.h 声明函数

E、添加__writeOneHzChar48 函数 unsigned long __writeOneHzChar48(unsigned char *pucMsk, unsigned short x0, unsigned short y0, unsigned short color) { unsigned long i,j,k; unsigned char mod[288]; unsigned char *pusMsk; unsigned short x; LCD_WR_CMD(R80,x0); LCD_WR_CMD(R81,x0+47); LCD_WR_CMD(R82,y0); LCD_WR_CMD(R83,y0+47); /* 当前字模 /* 当前字库地址 //水平方向 GRAM 起始地址 //水平方向 GRAM 结束地址 //垂直方向 GRAM 起始地址 //垂直方向 GRAM 结束地址 */ */

pusMsk = (unsigned char *)pucMsk; for(i=0; i<288; i++) /* 保存当前汉字点阵式字模 { mod[i] = *pusMsk++; } //y = y0; x = x0 + 47; for(i=0; i<288; i=i+6) { //LCD_SetCursor(x0, y); LCD_SetCursor(x , y0); LCD_WriteRAM_Prepare(); for(k=0 ; k<6 ; k++){ for(j=0 ; j<8 ; j++) { if((mod[i+k] << j) & 0x80) { LCD_WR_Data(color); } else { LCD_WR_Data(BACK_COLOR); } } } x --;//这里是点阵里面的行,点阵里面的行就是 1 }

*/ */

/* 取得当前字模,半字对齐访问需要

/* 16 行

*/ /* 设置写数据地址指针 /* 开始写入 GRAM */ /* 16 列 /* 显示字模 */ */ */

/* 用读方式跳过写空白点的像素

*/

return (48); } F、添加__writeOneASCII48 函数 unsigned long __writeOneASCII48(unsigned char *pucMsk, unsigned short x0, unsigned int y0, unsigned int color) { unsigned long i,j,k; unsigned int x; unsigned char ucChar[144];

/* 返回 24 位列宽

*/

LCD_WR_CMD(R80,x0); //水平方向 GRAM 起始地址 LCD_WR_CMD(R81,x0+47);//水平方向 GRAM 结束地址 LCD_WR_CMD(R82,y0); //垂直方向 GRAM 起始地址 LCD_WR_CMD(R83,y0+23); //y = y0; x = x0 + 47; for(i=0;i<144;i++) { ucChar[i] = *pucMsk++; } for(i=0; i<144; i=i+3) {/* 64/2=32 行 */ //LCD_SetCursor(x0, y);//每个循环以后 y++,x0 不变,换行回车这里 LCD_SetCursor(x, y0); LCD_WriteRAM_Prepare(); //开始写入 GRAM for(k=0;k<3;k++){//每一行有几个 8 位,k 就取几 for(j=0; j<8; j++) { /* 8 列 if((ucChar[i+k] << j) & 0x80) { LCD_WR_Data(color); } else { LCD_WR_Data(BACK_COLOR); } } } x --; } return (24); } /* 返回 16 位列宽 */ /* 显示字模 //垂直方向 GRAM 结束地址

/* 设置写数据地址指针

*/

*/ */

2)横屏改成竖屏显示
竖屏显示的原理跟横屏的一样,这里对需要修改的地方作说明:

A 初始化函数 LCD_init LCD_WR_CMD(0x0003, (1<<12)|(3<<4)|(0<<3)); //这里 AM 需要改成 0, AM=1 时, 沿着 y 轴方向进行更新, 当 LCD 当 AM=0 时, 沿着 x 轴方向进行更新。 同时, 我们需要保持 I/D[1]=1, 这一位表示当沿 y 或者 x 更新时的更新方向。I/D[0]则无要求。 LCD_WR_CMD(0x0001, 0x0100); LCD_WR_CMD(0x0060, 0x2700);//改变原点 B writestring48 函数的变化 void writeString48h(unsigned char *pcStr, unsigned short x0, unsigned int y0, unsigned int color) { unsigned int usIndex; unsigned int usWidth = 0; FNT_GB48 *ptGb48 = 0; ptGb48 = (FNT_GB48 *)GBHZ_48; while(1) { if(*pcStr == 0) { break; } x0 = x0 + (usWidth); if(*pcStr > 0x80) { if((x0 + 48) >= Gus_LCM_XMAX) { x0 = 0; y0 = y0 - 48; if(y0 >= Gus_LCM_YMAX) { y0 = 0; } } usIndex = findHzIndex48(pcStr); usWidth = __writeOneHzChar48h((unsigned char *)&(ptGb48[usIndex].Msk[0]), x0, y0, color); /* 显示字符 */ pcStr += 2; } else { if (*pcStr == '\r') { /* 判断为非汉字 /* 换行 */ */ /* 改变显示坐标 /* 纵坐标超出 */ */ /* 调节字符串显示松紧度 /* 判断为汉字 /* 检查剩余空间是否足够 /* 改变显示坐标 /* 纵坐标超出 */ */ */ */ */ /* 字符串结束 */ /* */

y0 = y0 - 48; if(y0 >= Gus_LCM_YMAX) { y0 = 0; } pcStr++; usWidth = 0; continue; } else if (*pcStr == '\n') { x0 = 0; pcStr++; usWidth = 0; continue; } else { if((x0 + 24) >= Gus_LCM_XMAX) {

/* 对齐到起点

*/

/* 检查剩余空间是否足够

*/

x0 = 0; y0 = y0 - 48; if(y0 >= Gus_LCM_YMAX) { y0 = 0; }

/* 改变显示坐标 /* 纵坐标超出

*/ */

} usWidth = __writeOneASCII48h((unsigned char *)&ASCII48[(*pcStr - 0x20)][0], x0, y0, color); /* ASCII 码表 21H 的值对应区位码 3 区*/ pcStr += 1; } } } //恢复窗体大小 LCD_WR_CMD(R80, 0x0000); //水平方向 GRAM 起始地址 LCD_WR_CMD(R81, 0x00EF); //水平方向 GRAM 结束地址 LCD_WR_CMD(R82, 0x0000); //垂直方向 GRAM 起始地址 LCD_WR_CMD(R83, 0x013F); //垂直方向 GRAM 结束地址 } C__writeOneHzChar48h 和__writeOneASCII48h 的变化 unsigned long __writeOneHzChar48h(unsigned char *pucMsk, unsigned short x0, unsigned short y0, unsigned short color) { unsigned long i,j,k; unsigned char mod[288]; unsigned char *pusMsk; unsigned short y; LCD_WR_CMD(R80,x0); LCD_WR_CMD(R81,x0+47); LCD_WR_CMD(R82,y0); LCD_WR_CMD(R83,y0+47); /* 当前字模 /* 当前字库地址 //水平方向 GRAM 起始地址 //水平方向 GRAM 结束地址 //垂直方向 GRAM 起始地址 //垂直方向 GRAM 结束地址 */ */

pusMsk = (unsigned char *)pucMsk; for(i=0; i<288; i++) { mod[i] = *pusMsk++; } //y = y0; y = y0 + 47; for(i=0; i<288; i=i+6) {

/* 保存当前汉字点阵式字模

*/ */

/* 取得当前字模,半字对齐访问需要

/* 16 行

*/ */

//LCD_SetCursor(x0, y); /* 设置写数据地址指针 LCD_SetCursor(x0 , y);//要注意与 I/D[1]=1 对应,当 I/D[1]=0 时,要让 x0=x0+47; LCD_WriteRAM_Prepare(); /* 开始写入 GRAM */ for(k=0 ; k<6 ; k++){ for(j=0 ; j<8 ; j++) { if((mod[i+k] << j) & 0x80) { LCD_WR_Data(color); } else { /* 16 列 /* 显示字模

*/ */

LCD_WR_Data(BACK_COLOR); } }

/* 用读方式跳过写空白点的像素

*/

} y --;//注意与 AM 对应,当 AM=0,沿 x 轴更新,每更新一行,就要开始另外一行,也就是 y-} return (48); } /* 返回 24 位列宽 */

unsigned long __writeOneASCII48h(unsigned char *pucMsk, unsigned short x0, unsigned int y0, unsigned int color) { unsigned long i,j,k; unsigned int y; unsigned char ucChar[144]; LCD_WR_CMD(R80,x0); //水平方向 GRAM 起始地址 LCD_WR_CMD(R81,x0+47);//水平方向 GRAM 结束地址 LCD_WR_CMD(R82,y0); LCD_WR_CMD(R83,y0+23); //y = y0; y = y0 + 47; for(i=0;i<144;i++) { ucChar[i] = *pucMsk++; } for(i=0; i<144; i=i+3) {/* 64/2=32 行 */ //LCD_SetCursor(x0, y);//每个循环以后 y++,x0 不变,换行回车这里 LCD_SetCursor(x0, y); LCD_WriteRAM_Prepare(); //开始写入 GRAM for(k=0;k<3;k++){//每一行有几个 8 位,k 就取几 for(j=0; j<8; j++) { if((ucChar[i+k] << j) & 0x80) { LCD_WR_Data(color); } else { LCD_WR_Data(BACK_COLOR); } } } y --; } return (24); /* 返回 16 位列宽 */ /* 8 列 /* 显示字模 */ */ //垂直方向 GRAM 起始地址 //垂直方向 GRAM 结束地址

/* 设置写数据地址指针

*/

}

附录: (2011-12-3)
? ?
?

Ctrl+Shift+F2

keil 中的 find 里面的 Mark All 怎么取消

F12 为 go to definition 快捷键 遇到“D:\Keil\ARM\INC\ST\STM32F10x\stm32f10x_type.h(23): error: #256: invalid redeclaration of type name "s32" (declared
at line 470 of "Src\App\stm32f10x.h")”这个错误,是因为少包含头文件导致的。 定义变脸要在函数的开头定义,其他地方不行。

? ?

例程编译错误的解决方法 由于例程的 STM32F10x.H 和 MDK 环境下的 STM32F10X_lib.h 的重复定义冲突, 因此将 STM32F10X_lib.h 中的 include "stm32f10x_map.h" 的语句注释掉,即可解决例程的编译错误。 在定义一个全局变量 state,然后在各个.c 文件里面都可以调用:在函数外面定义 如 int state;然后 在要使用的 地方 extern int state; 快捷键的设置

? ?

在 Keil->Edit->Advanced 中有两项 Comment Selection /Uncomment Selection 其作用是注释所选代码,与取消注释代码的作 用。

可以看到图中这两项并没有快捷键可以用,使用起来非常不方便。下面讲解如何为这两项加入用户自定义快捷键。 打开 Keil->Edit->Configuration,如图

找到并切换到 Shortcuts 选项卡

在 Select a command 中找到 Edit:Advanced:Comment Selection 点击 Creat Shortcut 按下你喜欢的快捷键(例如 Ctrl+Shif t+C),点击 OK,同理可设置 Unomment Selection ,具体可参见附图。


相关文章:
STM32入门基本知识
("");函数将停止工作,这个现象很奇怪 STM32 笔记之十三:恶搞,两只看门狗 a) 目的: 了解两种看门狗(我叫它:系统运行故障探测器和独立系统故障探测器,新手往往被...
STM32 中断入门最简单资料
文档贡献者 柳荫镇人 贡献于2013-01-20 ...STM32入门 8页 免费 初学STM32-1 33页 免费 芯...STM32定时器详细讲解及应... 12页 免费 stm32定时...
图文结合建立第一个STM32工程_初学者必看
STM32工程 图文结合教你在 RVMDK 上建立自己的 STM32工程发布: 2009-5-20 15:59 | 作者: hnrain | 查看: 53次 在网上找了很久, 还是没有能找到一篇图文...
STM32嵌入式入门必看之文章---介绍非常详细!(学习STM32...
有很多爱好者反映,买到的开发板没 : 有手册或手册...家装材料选购攻略 高端水龙头贵在哪儿 橱柜行业多“...STM32库函数基础学习 3页 2下载券 初学EasyMx PRO...
stm32f107_的学习(新手入门)_图文
stm32f107_的学习(新手入门)_学习总结_总结/汇报_实用文档。1,说明:为什么...STM32 入门篇三——ADC 的基本操作 在前面介绍的例程中有 ADC 的详细解释,...
STM32 开发入门教程_图文
因为 STM32 是一个低功耗的 MCU , 每一个你使用的外围设备都 需要单独开启时钟, 如果不开启将不能使用, 这个也是对于 STM32 初学者容易疏忽的地方 /* Enable...
STM32入门C语言详解
STM32入门C语言详解_其它语言学习_外语学习_教育专区。STM32入门C语言阅读...新手可用简化的延时函 数代替: void Delay(vu32 nCount) //简单延时函数 { ...
STM32入门与学习笔记
供电整合)和单独设计两类,详细产品比较见 豆皮的《如何选择 STM32 开发板》 ...(我叫它:系统运行故障探测器和独立系统故障探测器,新手往往被这个并 不形象的...
stm32初学例程
当然,STM32 许多过人之处还没有细细研究,巧妙设计之处还没有完全的感受到,暂...初学者在经历了环境搭建、GPIO、Time 定时之后,就需要接触 USART 相关了。 本...
献给新手:解析STM32的库函数
献给新手:解析STM32的库函数_电子/电路_工程科技_专业资料。非常意...只需要填写言简意赅的参数就可以在完全不关心底层寄存器的前提下完成 相关寄存器...
更多相关标签: