首页资源分类嵌入式处理器ARM MPU > STM32F1开发指南-库函数版本_V3.0

STM32F1开发指南-库函数版本_V3.0

已有 445117个资源

下载专区

上传者其他资源

    文档信息举报收藏

    标    签:STM32F1开发指南库函数版本_V3 0

    分    享:

    文档简介

    本篇将详细介绍我们用来学习 STM32

    的硬件平台: ALIENTEK 战舰 STM32 开发板, 通过该篇的介绍, 你将了解到我们的学习平台

    ALIENTEK 战舰 STM32 开发板的功能及特点。

    为了让读者更好的使用 ALIENTEK 战舰 STM32 开发板,本篇还介绍了开发板的一些使用

    注意事项,请读者在使用开发板的时候一定要注意。

    文档预览

    STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 STM32F1 开发指南 V3.0(库函数版本) −ALIENTEK 战舰 STM32F103 开发板教程 I STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 广州市星翼电子科技有限公司 淘宝店铺 1:http://eboard.taobao.com 淘宝店铺 2:http://openedv.taobao.com 技术支持论坛 (开源电子网) :www.openedv.com 官方网站:www.alientek.com 最新资料下载链接:http://www.openedv.com/posts/list/13912.htm E-mail: 389063473@qq.com QQ: 389063473 咨询电话:020-38271790 传真号码:020-36773971 团队:正点原子团队 正点原子,做最全面、最优秀的嵌入式开发平台软硬件供应商。 友情提示 如果您想及时免费获取“正点原子”最新资料,敬请关注正点原子 微信公众平台,我们将及时给您发布最新消息和重要资料。 关注方法: (1)微信“扫一扫”,扫描右侧二维码,添加关注 (2)微信添加朋友公众号输入“正点原子”关注 (3)微信添加朋友输入“alientek_stm32” 关注 II STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 内容简介 ......................................................................................................................16 前言 ..............................................................................................................................17 第一篇 硬件篇 ............................................................................................................19 第一章 实验平台简介 ................................................................................................20 1.1 ALIENTEK 战舰 STM32F103 资源初探 .......................................................... 20 1.2 ALIENTEK 战舰 STM32F103 资源说明 .......................................................... 22 1.2.1 硬件资源说明 ................................................................................................. 22 1.2.2 软件资源说明 ................................................................................................. 27 1.2.3 战舰 V3 IO 引脚分配 ..................................................................................... 28 1.3 ALIENTEK 战舰 STM32 V3.0 升级说明......................................................... 32 第二章 实验平台硬件资源详解 ................................................................................34 2.1 开发板原理图详解 ............................................................................................ 34 2.1.1 MCU ................................................................................................................. 34 2.1.2 引出 IO 口 ....................................................................................................... 36 2.1.3 USB 串口/串口 1 选择接口 ............................................................................ 36 2.1.4 JTAG/SWD....................................................................................................... 37 2.1.5 SRAM ............................................................................................................... 37 2.1.6 LCD 模块接口 ................................................................................................. 38 2.1.7 复位电路 ......................................................................................................... 39 2.1.8 启动模式设置接口 ......................................................................................... 39 2.1.9 RS232 串口/JOYPAD 接口 ............................................................................. 40 2.1.10 RS485 接口 .................................................................................................... 40 2.1.11 CAN/USB 接口 .............................................................................................. 41 2.1.12 EEPROM ........................................................................................................ 42 2.1.13 光敏传感器 ................................................................................................... 42 2.1.14 SPI FLASH..................................................................................................... 42 2.1.15 温湿度传感器接口 ....................................................................................... 43 2.1.16 红外接收头 ................................................................................................... 43 2.1.17 无线模块接口 ............................................................................................... 44 2.1.18 LED ................................................................................................................ 44 2.1.19 按键 ............................................................................................................... 45 2.1.20 TPAD 电容触摸按键 ..................................................................................... 45 III STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 2.1.21 OLED/摄像头模块接口 ................................................................................ 45 2.1.22 有源蜂鸣器 ................................................................................................... 46 2.1.23 SD 卡接口 ...................................................................................................... 47 2.1.24 ATK 模块接口................................................................................................ 47 2.1.25 多功能端口 ................................................................................................... 48 2.1.26 以太网接口(RJ45) ................................................................................... 49 2.1.27 耳机输出 ....................................................................................................... 50 2.1.28 板载喇叭 ....................................................................................................... 50 2.1.29 音频编解码 ................................................................................................... 51 2.1.30 电源 ............................................................................................................... 52 2.1.31 电源输入输出接口 ....................................................................................... 52 2.1.32 USB 串口 ....................................................................................................... 53 2.2 开发板使用注意事项 ......................................................................................... 54 2.3 STM32F103 学习方法........................................................................................ 54 第二篇 软件篇 ............................................................................................................56 第三章 MDK5 软件入门............................................................................................57 3.1 STM32 官方固件库简介 .................................................................................... 57 3.1.1 库开发与寄存器开发的关系 ........................................................................ 57 3.1.2 STM32 固件库与 CMSIS 标准讲解 ............................................................. 58 3.1.3 STM32 官方库包介绍 ................................................................................... 59 3.1.3.1 文件夹介绍: .............................................................................................. 60 3.1.3.2 关键文件介绍: .......................................................................................... 61 3.2 MDK5 简介......................................................................................................... 62 3.3 新建基于固件库的 MDK5 工程模板................................................................ 63 3.4 程序下载与调试 ................................................................................................. 85 3.4.1 STM32 软件仿真 ............................................................................................. 85 3.4.2 STM32 串口程序下载 ..................................................................................... 93 3.4.3 JTAG/SWD 程序下载和调试.......................................................................... 99 3.5 MDK5 使用技巧............................................................................................... 104 3.5.1 文本美化 ....................................................................................................... 104 3.5.2 语法检测&代码提示 .................................................................................... 108 3.5.3 代码编辑技巧 ............................................................................................... 109 3.5.4 其他小技巧 ................................................................................................... 113 IV STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第四章 STM32 开发基础知识入门 ..........................................................................116 4.1 MDK 下 C 语言基础复习 ................................................................................ 116 4.1.1 位操作 ........................................................................................................... 116 4.1.2 define 宏定义 ................................................................................................. 117 4.1.3 ifdef 条件编译................................................................................................ 117 4.1.4 extern 变量申明 ............................................................................................. 118 4.1.5 typedef 类型别名 ........................................................................................... 119 4.1.6 结构体 ........................................................................................................... 119 4.2 STM32 系统架构 .............................................................................................. 121 4.3 STM32 时钟系统 .............................................................................................. 122 4.4 端口复用和重映射 .......................................................................................... 126 4.4.1 端口复用功能 ................................................................................................ 126 4.4.2 端口重映射 .................................................................................................... 127 4.5 STM32 NVIC 中断优先级管理 ....................................................................... 128 4.6 MDK 中寄存器地址名称映射分析................................................................. 131 4.7 MDK 固件库快速组织代码技巧..................................................................... 133 第五章 SYSTEM 文件夹介绍 .................................................................................139 5.1 delay 文件夹代码介绍 ..................................................................................... 139 5.1.1 操作系统支持宏定义及相关函数 ............................................................... 140 5.1.2delay_init 函数 ................................................................................................ 142 5.1.3 delay_us 函数 ................................................................................................. 143 5.1.4 delay_ms 函数................................................................................................ 145 5.2 sys 文件夹代码介绍 ......................................................................................... 146 5.2.1 IO 口的位操作实现 .................................................................................... 146 5.3 usart 文件夹介绍 .............................................................................................. 148 5.3.1 printf 函数支持 .............................................................................................. 148 5.3.2 uart_init 函数.................................................................................................. 149 5.3.3 USART1_IRQHandler 函数........................................................................... 152 第三篇 实战篇 ..........................................................................................................154 第六章 跑马灯实验 ..................................................................................................155 6.1 STM32 IO 简介................................................................................................. 156 6.2 硬件设计 .......................................................................................................... 162 6.3 软件设计 .......................................................................................................... 163 V STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 6.4 仿真与下载 ...................................................................................................... 168 第七章 蜂鸣器实验 ..................................................................................................171 7.1 蜂鸣器简介 ...................................................................................................... 172 7.2 硬件设计 .......................................................................................................... 172 7.3 软件设计 .......................................................................................................... 173 7.4 仿真与下载 ...................................................................................................... 175 第八章 按键输入实验 ..............................................................................................177 8.1 STM32 IO 口简介............................................................................................. 178 8.2 硬件设计 .......................................................................................................... 178 8.3 软件设计 .......................................................................................................... 178 8.4 仿真与下载 ...................................................................................................... 181 第九章 串口实验 ......................................................................................................186 9.1 STM32 串口简介 .............................................................................................. 187 9.2 硬件设计 .......................................................................................................... 189 9.3 软件设计 .......................................................................................................... 190 9.4 下载验证 .......................................................................................................... 193 第十章 外部中断实验 ..............................................................................................196 10.1 STM32 外部中断简介 .................................................................................... 197 10.2 硬件设计 ........................................................................................................ 200 10.3 软件设计 ........................................................................................................ 200 10.4 下载验证 ........................................................................................................ 202 第十一章 独立看门狗(IWDG)实验 ...................................................................203 11.1 STM32 独立看门狗简介 ................................................................................ 204 11.2 硬件设计 ........................................................................................................ 205 11.3 软件设计 ........................................................................................................ 205 11.4 下载验证 ........................................................................................................ 207 第十二章 窗口门狗(WWDG)实验.....................................................................208 12.1 STM32F1 窗口看门狗简介............................................................................ 209 12.2 硬件设计 ........................................................................................................ 211 12.3 软件设计 ........................................................................................................ 211 12.4 下载验证 ........................................................................................................ 213 第十三章 定时器中断实验 ......................................................................................214 VI STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 13.1 STM32 通用定时器简介 ................................................................................ 215 13.2 硬件设计 ........................................................................................................ 220 13.3 软件设计 ........................................................................................................ 220 13.4 下载验证 ........................................................................................................ 222 第十四章 PWM 输出实验........................................................................................223 14.1 PWM 简介....................................................................................................... 224 14.2 硬件设计 ........................................................................................................ 227 14.3 软件设计 ........................................................................................................ 227 14.4 下载验证 ........................................................................................................ 229 第十五章 输入捕获实验 ..........................................................................................230 15.1 输入捕获简介 ................................................................................................ 231 15.2 硬件设计 ........................................................................................................ 235 15.3 软件设计 ........................................................................................................ 235 15.4 下载验证 ........................................................................................................ 239 第十六章 电容触摸按键实验 ..................................................................................240 16.1 电容触摸按键简介 ........................................................................................ 241 16.2 硬件设计 ........................................................................................................ 242 16.3 软件设计 ........................................................................................................ 242 16.4 下载验证 ........................................................................................................ 247 第十七章 OLED 显示实验 ......................................................................................248 17.1 OLED 简介 ..................................................................................................... 249 17.2 硬件设计 ........................................................................................................ 255 17.3 软件设计 ........................................................................................................ 256 17.4 下载验证 ........................................................................................................ 263 第十八章 TFTLCD 显示实验 ..................................................................................265 18.1 TFTLCD&FSMC 简介 ................................................................................... 266 18.1.1 TFTLCD 简介 .............................................................................................. 266 18.1.2 FSMC 简介 .................................................................................................. 271 18.2 硬件设计 ........................................................................................................ 280 18.3 软件设计 ........................................................................................................ 281 18.4 下载验证 ........................................................................................................ 292 第十九章 USMART 调试组件实验.........................................................................293 19.1 USMART 调试组件简介................................................................................ 294 VII STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 19.2 硬件设计 ........................................................................................................ 297 19.3 软件设计 ........................................................................................................ 297 19.4 下载验证 ........................................................................................................ 301 第二十章 RTC 实时时钟实验..................................................................................305 20.1 STM32F1 RTC 时钟简介 ............................................................................... 306 20.2 硬件设计 ........................................................................................................ 312 20.3 软件设计 ........................................................................................................ 312 20.4 下载验证 ........................................................................................................ 319 第二十一章 待机唤醒实验 ......................................................................................320 21.1 STM32 待机模式简介 .................................................................................... 321 21.2 硬件设计 ........................................................................................................ 324 21.3 软件设计 ........................................................................................................ 324 21.4 下载与测试 .................................................................................................... 327 第二十二章 ADC 实验.............................................................................................328 22.1 STM32 ADC 简介 .......................................................................................... 329 22.2 硬件设计 ........................................................................................................ 337 22.3 软件设计 ........................................................................................................ 337 22.4 下载验证 ........................................................................................................ 340 第二十三章 内部温度传感器实验 ..........................................................................341 23.1 STM32 内部温度传感器简介 ....................................................................... 342 23.2 硬件设计 ........................................................................................................ 342 23.3 软件设计 ........................................................................................................ 342 23.4 下载验证 ........................................................................................................ 345 第二十四章 光敏传感器实验 ..................................................................................346 24.1 光敏传感器简介 ............................................................................................ 347 24.2 硬件设计 ........................................................................................................ 347 24.3 软件设计 ........................................................................................................ 348 24.4 下载验证 ........................................................................................................ 349 第二十五章 DAC 实验.............................................................................................350 25.1 STM32 DAC 简介 .......................................................................................... 351 25.2 硬件设计 ........................................................................................................ 355 25.3 软件设计 ........................................................................................................ 356 25.4 下载验证 ........................................................................................................ 359 VIII STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第二十六章 PWM DAC 实验 ..................................................................................361 26.1 PWM DAC 简介 ............................................................................................. 362 26.2 硬件设计 ........................................................................................................ 363 26.3 软件设计 ........................................................................................................ 364 26.4 下载验证 ........................................................................................................ 367 第二十七章 DMA 实验............................................................................................369 27.1 STM32 DMA 简介.......................................................................................... 370 27.2 硬件设计 ........................................................................................................ 375 27.3 软件设计 ........................................................................................................ 375 27.4 下载验证 ........................................................................................................ 378 第二十八章 IIC 实验................................................................................................380 28.1 IIC 简介........................................................................................................... 381 28.2 硬件设计 ........................................................................................................ 381 28.3 软件设计 ........................................................................................................ 382 28.4 下载验证 ........................................................................................................ 389 第二十九章 SPI 实验 ..............................................................................................391 29.1 SPI 简介 ......................................................................................................... 392 29.2 硬件设计 ........................................................................................................ 395 29.3 软件设计 ........................................................................................................ 396 29.4 下载验证 ........................................................................................................ 401 第三十章 485 实验 ..................................................................................................403 30.1 485 简介 ......................................................................................................... 404 30.2 硬件设计 ........................................................................................................ 405 30.3 软件设计 ........................................................................................................ 406 30.4 下载验证 ........................................................................................................ 410 第三十一章 CAN 通讯实验.....................................................................................412 31.1 CAN 简介........................................................................................................ 413 31.2 硬件设计 ........................................................................................................ 431 31.3 软件设计 ........................................................................................................ 432 31.4 下载验证 ........................................................................................................ 438 第三十二章 触摸屏实验 ..........................................................................................440 32.1 触摸屏简介 .................................................................................................... 441 IX STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 32.1.1 电阻式触摸屏 .............................................................................................. 441 32.1.2 电容式触摸屏 .............................................................................................. 441 32.2 硬件设计 ........................................................................................................ 446 32.3 软件设计 ........................................................................................................ 447 32.4 下载验证 ........................................................................................................ 463 第三十三章 红外遥控实验 ....................................................................................465 33.1 红外遥控简介 ................................................................................................. 466 33.2 硬件设计 ........................................................................................................ 467 33.3 软件设计 ........................................................................................................ 468 33.4 下载验证 ........................................................................................................ 473 第三十四章 游戏手柄实验 ....................................................................................474 34.1 游戏手柄简介 ................................................................................................. 475 34.2 硬件设计 ........................................................................................................ 476 34.3 软件设计 ........................................................................................................ 478 34.4 下载验证 ........................................................................................................ 480 第三十五章 DS18B20 数字温度传感器实验 .......................................................482 35.1 DS18B20 简介 ................................................................................................ 483 35.2 硬件设计 ........................................................................................................ 484 35.3 软件设计 ........................................................................................................ 485 35.4 下载验证 ........................................................................................................ 489 第三十六章 DHT11 数字温湿度传感器实验 .......................................................491 36.1 DHT11 简介 .................................................................................................... 492 36.2 硬件设计 ........................................................................................................ 494 36.3 软件设计 ........................................................................................................ 494 36.4 下载验证 ........................................................................................................ 498 第三十七章 MPU6050 六轴传感器实验 ................................................................499 37.1 MPU6050 简介 ............................................................................................... 500 37.1.1 MPU6050 基础介绍 .................................................................................... 500 37.1.2 DMP 使用简介 ............................................................................................ 504 37.2 硬件设计 ........................................................................................................ 508 37.3 软件设计 ........................................................................................................ 510 37.4 下载验证 ........................................................................................................ 517 第三十八章 无线通信实验 ....................................................................................520 X STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 38.1 NRF24L01 无线模块简介 .............................................................................. 521 38.2 硬件设计 ........................................................................................................ 521 38.3 软件设计 ........................................................................................................ 523 38.4 下载验证 ........................................................................................................ 530 第三十九章 FLASH 模拟 EEPROM 实验 ..............................................................532 39.1 STM32 FLASH 简介 ...................................................................................... 533 39.2 硬件设计 ........................................................................................................ 539 39.3 软件设计 ........................................................................................................ 539 39.4 下载验证 ........................................................................................................ 543 第四十章 摄像头实验 ..............................................................................................544 40.1 OV7670 简介 .................................................................................................. 545 40.2 硬件设计 ........................................................................................................ 549 40.3 软件设计 ........................................................................................................ 551 40.4 下载验证 ........................................................................................................ 559 第四十一章 外部 SRAM 实验.................................................................................561 41.1 IS62WV51216 简介 ........................................................................................ 562 41.2 硬件设计 ........................................................................................................ 564 41.3 软件设计 ........................................................................................................ 564 41.4 下载验证 ........................................................................................................ 568 第四十二章 内存管理实验 ......................................................................................570 42.1 内存管理简介 ................................................................................................ 571 42.2 硬件设计 ........................................................................................................ 572 42.3 软件设计 ........................................................................................................ 572 42.4 下载验证 ........................................................................................................ 579 第四十三章 SD 卡实验 ..........................................................................................581 43.1 SDIO 简介....................................................................................................... 582 43.1.1 SDIO 主要功能及框图................................................................................ 582 43.1.2 SDIO 的时钟................................................................................................ 583 43.1.3 SDIO 的命令与响应.................................................................................... 583 43.1.4 SDIO 相关寄存器介绍................................................................................ 585 43.1.5 SD 卡初始化流程 ........................................................................................ 590 43.2 硬件设计 ........................................................................................................ 593 43.3 软件设计 ........................................................................................................ 594 XI STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 43.4 下载验证 ........................................................................................................ 604 第四十四章 FATFS 实验........................................................................................606 44.1 FATFS 简介..................................................................................................... 607 44.2 硬件设计 ........................................................................................................ 612 44.3 软件设计 ........................................................................................................ 612 44.4 下载验证 ........................................................................................................ 620 第四十五章 汉字显示实验 ......................................................................................622 45.1 汉字显示原理简介 ........................................................................................ 623 45.2 硬件设计 ........................................................................................................ 627 45.3 软件设计 ........................................................................................................ 627 45.4 下载验证 ........................................................................................................ 636 第四十六章 图片显示实验 ......................................................................................638 46.1 图片格式简介 ................................................................................................ 639 46.2 硬件设计 ........................................................................................................ 640 46.3 软件设计 ........................................................................................................ 641 46.4 下载验证 ........................................................................................................ 649 第四十七章 照相机实验 ..........................................................................................651 47.1 BMP 编码简介................................................................................................ 652 47.2 硬件设计 ........................................................................................................ 654 47.3 软件设计 ........................................................................................................ 655 47.4 下载验证 ........................................................................................................ 660 第四十八章 音乐播放器实验 ..................................................................................661 48.1 VS1053 简介 ................................................................................................... 662 48.2 硬件设计 ........................................................................................................ 667 48.3 软件设计 ........................................................................................................ 668 48.4 下载验证 ........................................................................................................ 673 第四十九章 录音机实验 ..........................................................................................674 49.1 WAV 简介........................................................................................................ 675 49.2 硬件设计 ........................................................................................................ 678 49.3 软件设计 ........................................................................................................ 678 49.4 下载验证 ........................................................................................................ 684 第五十章 手写识别实验 ..........................................................................................687 XII STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 50.1 手写识别简介 ................................................................................................ 688 50.2 硬件设计 ........................................................................................................ 691 50.3 软件设计 ........................................................................................................ 692 50.4 下载验证 ........................................................................................................ 695 第五十一章 T9 拼音输入法实验.............................................................................697 51.1 拼音输入法简介 ............................................................................................ 698 51.2 硬件设计 ........................................................................................................ 700 51.3 软件设计 ........................................................................................................ 700 51.4 下载验证 ........................................................................................................ 708 第五十二章 串口 IAP 实验......................................................................................710 52.1 IAP 简介.......................................................................................................... 711 52.2 硬件设计 ........................................................................................................ 716 52.3 软件设计 ........................................................................................................ 717 52.4 下载验证 ........................................................................................................ 723 第五十三章 USB 虚拟串口实验 .............................................................................725 53.1 USB 简介 ........................................................................................................ 726 53.2 硬件设计 ........................................................................................................ 728 53.3 软件设计 ........................................................................................................ 729 53.4 下载验证 ........................................................................................................ 737 第五十四章 USB 读卡器实验 .................................................................................739 54.1 USB 读卡器简介 ............................................................................................ 740 54.2 硬件设计 ........................................................................................................ 740 54.3 软件设计 ........................................................................................................ 741 54.4 下载验证 ........................................................................................................ 744 第五十五章 网络通信实验 ......................................................................................746 55.1 DM9000、TCP/IP 和 LWIP 简介 ................................................................. 747 55.1.1 DM9000 简介............................................................................................... 747 55.1.2 TCP/IP 协议简介 ......................................................................................... 750 55.1.3 LWIP 简介.................................................................................................... 750 55.2 硬件设计 ........................................................................................................ 752 55.3 软件设计 ........................................................................................................ 753 55.4 下载验证 ........................................................................................................ 757 55.4.1 Web Server 测试........................................................................................... 759 XIII STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 55.4.2 TCP Server 测试........................................................................................... 761 55.4.3 TCP Client 测试 ........................................................................................... 763 55.4.4 UDP 测试 ..................................................................................................... 765 第五十六章 UCOSII 实验 1-任务调度 ...................................................................767 56.1 UCOSII 简介................................................................................................... 768 56.2 硬件设计 ........................................................................................................ 773 56.3 软件设计 ........................................................................................................ 773 56.4 下载验证 ........................................................................................................ 777 56.5 任务删除,挂起和恢复测试 ........................................................................ 777 第五十七章 UCOSII 实验 2-信号量和邮箱 ...........................................................782 57.1 UCOSII 信号量和邮箱简介........................................................................... 783 57.2 硬件设计 ........................................................................................................ 785 57.3 软件设计 ........................................................................................................ 786 57.4 下载验证 ........................................................................................................ 793 第五十八章 UCOSII 实验 3-消息队列、信号量集和软件定时器 .......................795 58.1 UCOSII 消息队列、信号量集和软件定时器简介....................................... 796 58.2 硬件设计 ........................................................................................................ 803 58.3 软件设计 ........................................................................................................ 804 58.4 下载验证 ........................................................................................................ 812 第五十九章 战舰 V3 综合测试实验 .......................................................................814 59.1 战舰 V3 综合测试实验简介 ......................................................................... 815 59.2 战舰 V3 综合测试实验详解 .......................................................................... 815 59.2.1 电子图书 ..................................................................................................... 820 59.2.2 数码相框 ..................................................................................................... 822 59.2.3 音乐播放 ..................................................................................................... 824 59.2.4 TOM 猫 ........................................................................................................ 827 59.2.5 时钟 ............................................................................................................. 828 59.2.6 系统设置 ..................................................................................................... 829 59.2.7 FC 游戏机 .................................................................................................... 840 59.2.8 记事本 ......................................................................................................... 844 59.2.9 运行器 ......................................................................................................... 846 59.2.10 手写画笔 ................................................................................................... 847 59.2.11 照相机 ....................................................................................................... 850 XIV STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 59.2.12 录音机 ....................................................................................................... 855 59.2.13 USB 连接 ................................................................................................... 857 59.2.14 网络通信 ................................................................................................... 859 59.2.15 无线传书 ................................................................................................... 863 59.2.16 计算器 ....................................................................................................... 865 59.2.17 拨号 ........................................................................................................... 868 59.2.18 应用中心 ................................................................................................... 871 59.2.19 短信 ........................................................................................................... 872 XV STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 内容简介 本手册将由浅入深,带领大家学习 STM32F103 的各个功能,为您开启全新的 STM32 之旅。 本手册总共分为三篇:1,硬件篇,主要介绍本手册硬件平台;2,软件篇,主要介绍 STM32F1 常用开发软件的使用以及一些下载调试的技巧,并详细介绍了几个常用的系统文件(程序);3, 实战篇,主要通过 54 个实例(绝大部分是直接操作寄存器完成的)带领大家一步步深入了解 STM32F1。 本手册为 ALIENTEK 战舰 STM32F103 开发板的配套教程,在开发板配套的光盘里面,有 详细原理图以及所有实例的完整代码,这些代码都有详细的注释,所有源码都经过我们严格测 试,不会有任何警告和错误,另外,源码有我们生成好的 hex 文件,大家只需要通过串口/仿真 器下载到开发板即可看到实验现象,亲自体验实验过程。 本手册不仅非常适合广大学生和电子爱好者学习 STM32F103,其大量的实验以及详细的解 说,也是公司产品开发的不二参考。 本手册自 2012 年发布以来,深得广大网友的喜爱,同时也提出了很多建设性意见,本手册 (V3.0)针对以往版本,主要变化有以下几点: 1,硬件平台的变更。 本手册针对的硬件平台是:ALIENTEK 战舰 STM32 开发板 V3.0 及以后版本,设计更 合理。本手册大部分例程在 V3.0 之前的开发板上,均能直接使用,部分例程得做适当修改, 才可以在之前版本使用。V3.0 平台与之前平台的资源变更明细,请看本手册 1.3 节。 2,开发环境的变更。 本手册采用 MDK 最新的集成开发环境:MDK5.14,作为 STM32 的开发环境,而之前 版本采用的是 MDK3.80A 开发环境。 3,例程变更。 ALIENTEK 战舰 STM32 开发板 V3.0 在原来版本上,删减了一些不常用的功能(收音 机/PS2 接口等),增加了常用的网卡等外设,所以例程也有所变更,详见:1.2.2 节。 16 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 前言 Cortex-M3 采用 ARM V7 构架,不仅支持 Thumb-2 指令集,而且拥有很多新特性。较之 ARM7 TDMI,Cortex-M3 拥有更强劲的性能、更高的代码密度、位带操作、可嵌套中断、低成 本、低功耗等众多优势。 国内 Cortex-M3 市场,ST(意法半导体)公司的 STM32 无疑是最大赢家,作为 Cortex-M3 内核最先尝蟹的两个公司(另一个是 Luminary(流明))之一,ST 无论是在市场占有率,还 是在技术支持方面,都是远超其他对手。在 Cortex-M3 芯片的选择上,STM32 无疑是大家的首 选。 现在 ST 公司又推出了 STM32F0 系列 Cortex M0 芯片以及 STM32F4 系列 Coretx M4 芯片, 这两种芯片都已经量产,而且可以比较方便的购买到,但是本指南,我们只讨论 Cortex M3, (因为这个现在是性价比最高的 ^_^)有兴趣的读者可以自行了解一下。 STM32 的优异性体现在如下几个方面: 1, 超低的价格。以 8 位机的价格,得到 32 位机,是 STM32 最大的优势。 2, 超多的外设。STM32 拥有包括:FSMC、TIMER、SPI、IIC、USB、CAN、IIS、SDIO、 ADC、DAC、RTC、DMA 等众多外设及功能,具有极高的集成度。 3, 丰富的型号。STM32 仅 M3 内核就拥有 F100、F101、F102、F103、F105、F107、F207、 F217 等 8 个系列上百种型号,具有 QFN、LQFP、BGA 等封装可供选择。同时 STM32 还推出了 STM32L 和 STM32W 等超低功耗和无线应用型的 M3 芯片。 4, 优异的实时性能。84 个中断,16 级可编程优先级,并且所有的引脚都可以作为中断输 入。 5, 杰出的功耗控制。STM32 各个外设都有自己的独立时钟开关,可以通过关闭相应外设 的时钟来降低功耗。 6, 极低的开发成本。STM32 的开发不需要昂贵的仿真器,只需要一个串口即可下载代码, 并且支持 SWD 和 JTAG 两种调试口。SWD 调试可以为你的设计带来跟多的方便,只 需要 2 个 IO 口,即可实现仿真调试。 学习 STM32 有两份不错的中文资料: 《STM32 参考手册》中文版 V10.0 《Cortex-M3 权威指南》中文版(宋岩 译) 前者是 ST 官方针对 STM32 的一份通用参考资料,内容翔实,但是没有实例,也没有对 Cortex-M3 构架进行多少介绍(估计 ST 是把读者都当成一个 Cortex-M3 熟悉者来写的),读者 只能根据自己对书本的理解来编写相关代码。后者是专门介绍 Cortex-M3 构架的书,有简短的 实例,但没有专门针对 STM32 的介绍。所以,在学习 STM32 的时候,必须结合这份资料来看。 STM32 拥有非常多的寄存器,对于新手来说,直接操作寄存器有很大的难度,所以 ST 官 方提供了一套固件库函数,大家不需要再直接操作繁琐的寄存器,而是直接调用固件库函数即 可实现操作寄存器的目的。当然,我们要了解一些外设的原理,必须对寄存器有一定的了解, 这对以后开发和调试也是非常有帮助的,所以在我们手册中我们会保留一些重要寄存器的讲解, 但是我们的实例代码基本都是调用固件库来实现的。有关寄存器操作的实例,大家可以参考我 们寄存器版本的手册及代码。 本指南将结合《STM32 参考手册》和《Cortex-M3 权威指南》两者的优点,并从固件库级 17 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 别出发,深入浅出,向读者展示 STM32 的各种功能。总共配有 56 个实例,基本上每个实例在 均配有软硬件设计,在介绍完软硬件之后,马上附上实例代码,并带有详细注释及说明,让读 者快速理解代码。 这些实例涵盖了 STM32 的绝大部分内部资源,并且提供很多实用级别的程序,如:内存 管理、拼音输入法、手写识别、图片解码、IAP 等。所有实例在 MDK5 编译器下编译通过,大 家只需下载程序到 ALIENTEK 战舰 STM32 开发板,即可验证实验。 不管你是一个 STM32 初学者,还是一个老手,本指南都非常适合。尤其对于初学者,本 指南将手把手的教你如何使用 MDK,包括新建工程、编译、仿真、下载调试等一系列步骤, 让你轻松上手。本指南适用于想通过库函数学习 STM32 的读者,大家可以结合官方提供的库 函数实例对照学习。 本指南的实验平台是 ALIENTEK 战舰 STM32 开发板,有这款开发板的朋友则直接可以拿 本指南配套的光盘上的例程在开发板上运行、验证。而没有这款开发板而又想要的朋友,可以 上淘宝购买。当然你如果有了一款自己的开发板,而又不想再买,也是可以的,只要你的板子 上有 ALIENTEK 战舰 STM32 开发板上的相同资源(需要实验用到的),代码一般都是可以通 用的,你需要做的就只是把底层的驱动函数(一般是 IO 操作)稍做修改,使之适合你的开发 板即可。 俗话说:人无完人。本指南也不例外,在编写过程中虽然得到了不少网友的指正,但难免 不会再有出错的地方,如果大家发现指南中有什么错误的地方,还请告诉我们,我们的官方技 术支持论坛为 www.openedv.com(开源电子网),大家可以在论坛上发帖提问。在此先向各位朋 友表示衷心的感谢。 ALIENTEK//广州市星翼电子科技有限公司 18 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第一篇 硬件篇 实践出真知,要想学好 STM32,实验平台必不可少!本篇将详细介绍我们用来学习 STM32 的硬件平台:ALIENTEK 战舰 STM32 开发板,通过该篇的介绍,你将了解到我们的学习平台 ALIENTEK 战舰 STM32 开发板的功能及特点。 为了让读者更好的使用 ALIENTEK 战舰 STM32 开发板,本篇还介绍了开发板的一些使用 注意事项,请读者在使用开发板的时候一定要注意。 本篇将分为如下两章: 1,实验平台简介; 2,实验平台硬件资源详解; 19 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第一章 实验平台简介 本章,主要向大家简要介绍我们的实验平台:ALIENTEK 战舰 STM32F103。通过本章的 学习,你将对我们后面使用的实验平台有个大概了解,为后面的学习做铺垫。 本章将分为如下两节: 1.1,ALIENTEK 战舰 STM32F103 资源初探; 1.2,ALIENTEK 战舰 STM32F103 资源说明; 1.1 ALIENTEK 战舰 STM32F103 资源初探 自从 2012 年上市以来,ALIENTEK 战舰 STM32F103 开发板广受客户好评,并常年稳居淘 宝 STM32 系列开发板销量冠军,总销量超过 1.5W 套。最新的战舰 STM32F103 V3.0 开发板, 则是根据广大客户反馈,在原有战舰板的基础上进行改进而来(具体改变见 1.3 节),下面我们 开始介绍战舰 STM32F103 开发板 V3.0 版本。 ALIENTEK 战舰 STM32F103 V3.0 的资源图如图 1.1.1 所示: CAN JOYPAD/RS2 JOYPAD/RS2 IS62WV51216 引出 LCD 引出 RS232 以太网接 RS485 接口 32 接口(公) 32 选择开关 8M SRAM IO 口 接口 IO 口 接口(母) 口(RJ45) 接口 WIRELESS 模块接口 W25Q128 128M FLASH DC6~24V 电源输入 电源开关 SD 卡接口(在背面) 引出 IO 口 CAN/USB 选择口 JTAG/SWD 接口 USB 串口/串口 1 STM32F103ZET6 24C02 EEPROM USB SLAVE 后备电池接口 USB 转串口 小喇叭 (在底部) RS232/RS485 选择接口 5V 电源输入/输出 3.3V 电源输入/输出 RS232/模块 选择接口 ATK 模块接口 耳机输出接口 录音输入接口 MIC(咪头) 多功能端口 电源指示灯 触摸按钮 OLED/摄像 头 模块接口 光敏 传感器 有源 蜂鸣器 红外 DS18B20/ 2 个 复位 启动选 接收头 DHT11 接口 LED 按钮 择端口 4个 按键 参考电压 选择端口 图 1.1.1 战舰 STM32F103 资源图 从图 1.1.1 可以看出,ALIENTEK 战舰 STM32F103,资源十分丰富,并把 STM32F103 的 内部资源发挥到了极致,基本所有 STM32F103 的内部资源,都可以在此开发板上验证,同时 扩充丰富的接口和功能模块,整个开发板显得十分大气。 开发板的外形尺寸为 121mm*160mm 大小,板子的设计充分考虑了人性化设计,并结合 20 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 ALIENTEK 多年的 STM32 开发板设计经验,同时听取了很多网友以及客户的建议,经过多次 改进,最终确定了这样的设计。 ALIENTEK 战舰 STM32F103 板载资源如下: ◆ CPU:STM32F103ZET6,LQFP144,FLASH:512K,SRAM:64K; ◆ 外扩 SRAM:IS62WV51216,1M 字节 ◆ 外扩 SPI FLASH:W25Q128,16M 字节 ◆ 1 个电源指示灯(蓝色) ◆ 2 个状态指示灯(DS0:红色,DS1:绿色) ◆ 1 个红外接收头,并配备一款小巧的红外遥控器 ◆ 1 个 EEPROM 芯片,24C02,容量 256 字节 ◆ 1 个板载扬声器(在底面,用于音频输出) ◆ 1 个光敏传感器 ◆ 1 个高性能音频编解码芯片,VS1053 ◆ 1 个无线模块接口(可接 NRF24L01/RFID 模块等) ◆ 1 路 CAN 接口,采用 TJA1050 芯片 ◆ 1 路 485 接口,采用 SP3485 芯片 ◆ 2 路 RS232 串口(一公一母)接口,采用 SP3232 芯片 ◆ 1 个游戏手柄接口(与公头串口共用 DB9 口),可接插 FC(红白机)游戏手柄 ◆ 1 路数字温湿度传感器接口,支持 DS18B20 /DHT11 等 ◆ 1 个 ATK 模块接口,支持 ALIENTEK 蓝牙/GPS 模块/MPU6050 模块等 ◆ 1 个标准的 2.4/2.8/3.5 寸 LCD 接口,支持触摸屏 ◆ 1 个摄像头模块接口 ◆ 1 个 OLED 模块接口(与摄像头接口共用) ◆ 1 个 USB 串口,可用于程序下载和代码调试(USMART 调试) ◆ 1 个 USB SLAVE 接口,用于 USB 通信 ◆ 1 个有源蜂鸣器 ◆ 1 个游戏手柄/RS232 选择开关 ◆ 1 个 RS232/RS485 选择接口 ◆ 1 个 RS232/模块选择接口 ◆ 1 个 CAN/USB 选择接口 ◆ 1 个串口选择接口 ◆ 1 个 SD 卡接口(在板子背面,SDIO 接口) ◆ 1 个 10M/100M 以太网接口(RJ45) ◆ 1 个标准的 JTAG/SWD 调试下载口 ◆ 1 个录音头(MIC/咪头) ◆ 1 路立体声音频输出接口 ◆ 1 路立体声录音输入接口 ◆ 1 组多功能端口(DAC/ADC/PWM DAC/AUDIO IN/TPAD) ◆ 1 组 5V 电源供应/接入口 ◆ 1 组 3.3V 电源供应/接入口 ◆ 1 个参考电压设置接口 ◆ 1 个直流电源输入接口(输入电压范围:6~24V) ◆ 1 个启动模式选择配置接口 21 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 ◆ 1 个 RTC 后备电池座,并带电池 ◆ 1 个复位按钮,可用于复位 MCU 和 LCD ◆ 4 个功能按钮,其中 WK_UP 兼具唤醒功能 ◆ 1 个电容触摸按键 ◆ 1 个电源开关,控制整个板的电源 ◆ 独创的一键下载功能 ◆ 除晶振占用的 IO 口外,其余所有 IO 口全部引出 ALIENTEK 战舰 STM32F103 的特点包括: 1) 接口丰富。板子提供十来种标准接口,可以方便的进行各种外设的实验和开发。 2) 设计灵活。板上很多资源都可以灵活配置,以满足不同条件下的使用。我们引出了除晶 振占用的 IO 口外的所有 IO 口,可以极大的方便大家扩展及使用。另外板载一键下载 功能,可避免频繁设置 B0、B1 的麻烦,仅通过 1 根 USB 线即可实现 STM32 的开发。 3) 资源充足。主芯片采用自带 512K 字节 FLASH 的 STM32F103ZET6,并外扩 1M 字节 SRAM 和 16M 字节 FLASH,满足大内存需求和大数据存储。板载高性能音频编解码芯 片、双 RS232 串口、百兆网卡、光敏传感器以及各种接口芯片,满足各种应用需求。 4) 人性化设计。各个接口都有丝印标注,且用方框框出,使用起来一目了然;部分常用外 设大丝印标出,方便查找;接口位置设计安排合理,方便顺手。资源搭配合理,物尽其 用。 1.2 ALIENTEK 战舰 STM32F103 资源说明 资源说明部分,我们将分为三个部分说明:硬件资源说明、软件资源说明和战舰 V3 IO 引 脚分配。 1.2.1 硬件资源说明 这里我们详细介绍战舰 STM32F103 的各个部分(图 1.1.1 中的标注部分)的硬件资源,我 们将按逆时针的顺序依次介绍。 1. WIRELESS 模块接口 这是开发板板载的无线模块接口(U4),可以外接 NRF24L01/RFID 等无线模块。从而实现 无线通信等功能。注意:接 NRF24L01 模块进行无线通信的时候,必须同时有 2 个模块和 2 个 板子,才可以测试,单个模块/板子例程是不能测试的。 2. W25Q128 128M FLASH 这是开发板外扩的 SPI FLASH 芯片(U10),容量为 128Mbit,也就是 16M 字节,可用于 存储字库和其他用户数据,满足大容量数据存储要求。当然如果觉得 16M 字节还不够用,你可 以把数据存放在外部 SD 卡。 3. SD 卡接口 这是开发板板载的一个标准 SD 卡接口(SD_CARD),该接口在开发板的背面,采用大 SD 卡接口(即相机卡,也可以是 TF 卡+卡套的形式),SDIO 方式驱动,有了这个 SD 卡接口,就 可以满足海量数据存储的需求。 4. 引出 IO 口(总共有三处) 这是开发板 IO 引出端口,总共有三组主 IO 引出口:P1、P2 和 P3。其中,P1 和 P2 分别 采用 2*22 排针引出,总共引出 86 个 IO 口,P3 采用 1*16 排针,按顺序引出 FSMC_D0~D15 等 16 个 IO 口。而 STM32F103ZET6 总共只有 112 个 IO,除去 RTC 晶振占用的 2 个 IO,还剩 下 110 个,前面三组排针总共引出 102 个 IO,剩下的分别通过:P4、P7、P8 和 P9 引出。 22 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 5. CAN/USB 选择口 这是一个 CAN/USB 的选择接口(P9),因为 STM32 的 USB 和 CAN 是共用一组 IO(PA11 和 PA12),所以我们通过跳线帽来选择不同的功能,以实现 USB/CAN 的实验。 6. JTAG/SWD 接口 这是 ALIENTEK 战舰 STM32F103 板载的 20 针标准 JTAG 调试口(JTAG),该 JTAG 口直 接可以和 ULINK、JLINK 或者 STLINK 等调试器(仿真器)连接,同时由于 STM32 支持 SWD 调试,这个 JTAG 口也可以用 SWD 模式来连接。 用标准的 JTAG 调试,需要占用 5 个 IO 口,有些时候,可能造成 IO 口不够用,而用 SWD 则只需要 2 个 IO 口,大大节约了 IO 数量,但他们达到的效果是一样的,所以我们强烈建议仿 真器使用 SWD 模式! 7. USB 串口/串口 1 这是 USB 串口同 STM32F103ZET6 的串口 1 进行连接的接口(P4),标号 RXD 和 TXD 是 USB 转串口的 2 个数据口(对 CH340G 来说),而 PA9(TXD)和 PA10(RXD)则是 STM32 的串口 1 的两个数据口(复用功能下)。他们通过跳线帽对接,就可以和连接在一起了,从而实现 STM32 的程序下载以及串口通信。 设计成 USB 串口,是出于现在电脑上串口正在消失,尤其是笔记本,几乎清一色的没有串 口。所以板载了 USB 串口可以方便大家下载代码和调试。而在板子上并没有直接连接在一起, 则是出于使用方便的考虑。这样设计,你可以把 ALIENTEK 战舰 STM32F103 当成一个 USB 转 TTL 串口,来和其他板子通信,而其他板子的串口,也可以方便地接到 ALIENTEK 战舰 STM32F103 上。 8. STM32F103ZET6 这是开发板的核心芯片(U2),型号为:STM32F103ZET6。该芯片具有 64KB SRAM、512KB FLASH、2 个基本定时器、4 个通用定时器、2 个高级定时器、2 个 DMA 控制器(共 12 个通道)、 3 个 SPI、2 个 IIC、5 个串口、1 个 USB、1 个 CAN、3 个 12 位 ADC、1 个 12 位 DAC、1 个 SDIO 接口、1 个 FSMC 接口以及 112 个通用 IO 口。 9. 24C02 EEPROM 这是开发板板载的 EEPROM 芯片(U11),容量为 2Kb,也就是 256 字节。用于存储一些 掉电不能丢失的重要数据,比如系统设置的一些参数/触摸屏校准数据等。有了这个就可以方便 的实现掉电数据保存。 10. USB SLAVE 这是开发板板载的一个 MiniUSB 头(USB_SLAVE),用于 USB 从机(SLAVE)通信,一 般用于 STM32 与电脑的 USB 通信。通过此 MiniUSB 头,开发板就可以和电脑进行 USB 通信 了。 开发板总共板载了 2 个 MiniUSB 头,一个(USB_232)用于 USB 转串口,连接 CH340G 芯片;另外一个(USB_SLAVE)用于 STM32 内带的 USB。同时开发板可以通过此 MiniUSB 头供电,板载两个 MiniUSB 头(不共用),主要是考虑了使用的方便性,以及可以给板子提供 更大的电流(两个 USB 都接上)这两个因素。 11. 后备电池接口 这是 STM32 后备区域的供电接口(BAT),可安装 CR1220 电池(默认安装了),可以用来给 STM32 的后备区域提供能量,在外部电源断电的时候,维持后备区域数据的存储,以及 RTC 的运行。 12. USB 转串口 这是开发板板载的另外一个 MiniUSB 头(USB_232),用于 USB 连接 CH340G 芯片,从而 23 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 实现 USB 转 TTL 串口。同时,此 MiniUSB 接头也是开发板电源的主要提供口。 13. 小喇叭 这是开发板自带的一个 8Ω 2W 的小喇叭,安装在开发板的背面,并带了一个小音腔,可 以用来播放音频。该喇叭由 HT6872 单声道 D 类功放 IC 驱动,最大输出功率可达 2W。 特别注意:HT6872 受 VS1053 的 GPIO4 控制,必须程序上控制 VS1053 的 GPIO4 输出 1, 才可以控制 HT6872 工作,从而听到声音。默认条件下(GPIO4=0)HT6872 是关闭的。 14. OLED/摄像头模块接口 这是开发板板载的一个 OLED/摄像头模块接口(P6),如果是 OLED 模块,靠左插即可(右 边两个孔位悬空)。如果是摄像头模块(ALIENTEK 提供),则刚好插满。通过这个接口,可以 分别连接 2 种外部模块,从而实现相关实验。 15. 光敏传感器 这是开发板板载的一个光敏传感器(LS1),通过该传感器,开发板可以感知周围环境光线 的变化,从而可以实现类似自动背光控制的应用。 16. 有源蜂鸣器 这是开发板的板载蜂鸣器(BEEP),可以实现简单的报警/闹铃等功能。 17. 红外接收头 这是开发板的红外接收头(U8),可以实现红外遥控功能,通过这个接收头,可以接受市 面常见的各种遥控器的红外信号,大家甚至可以自己实现万能红外解码。当然,如果应用得当, 该接收头也可以用来传输数据。 战舰 STM32F103 给大家配备了一个小巧的红外遥控器,该遥控器外观如图 1.2.1.1 所示: 图 1.2.1.1 红外遥控器 18. DS18B20/DHT11 接口 这 是 开 发 板 的 一 个 复 用 接 口 ( U6 ), 该 接 口 由 4 个 镀 金 排 孔 组 成 , 可 以 用 来 接 DS18B20/DS1820 等数字温度传感器。也可以用来接 DHT11 这样的数字温湿度传感器。实现一 个接口,2 个功能。不用的时候,大家可以拆下上面的传感器,放到其他地方去用,使用上是 十分方便灵活的。 19. 2 个 LED 这是开发板板载的两个 LED 灯(DS0 和 DS1),DS0 是红色的,DS1 是绿色的,主要是方 便大家识别。这里提醒大家不要停留在 51 跑马灯的思维,搞这么多灯,除了浪费 IO 口,实在 是想不出其他什么优点。 我们一般的应用 2 个 LED 足够了,在调试代码的时候,使用 LED 来指示程序状态,是非 24 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 常不错的一个辅助调试方法。战舰 STM32F103 几乎每个实例都使用了 LED 来指示程序的运行 状态。 20. 复位按钮 这是开发板板载的复位按键(RESET),用于复位 STM32,还具有复位液晶的功能,因为 液晶模块的复位引脚和 STM32 的复位引脚是连接在一起的,当按下该键的时候,STM32 和液 晶一并被复位。 21. 启动选择端口 这是开发板板载的启动模式选择端口(BOOT),STM32 有 BOOT0(B0)和 BOOT1(B1) 两个启动选择引脚,用于选择复位后 STM32 的启动模式,作为开发板,这两个是必须的。在 开发板上,我们通过跳线帽选择 STM32 的启动模式。关于启动模式的说明,请看 2.1.8 小节。 22. 4 个按键 这是开发板板载的 4 个机械式输入按键(KEY0、KEY1、KEY2 和 WK_UP),其中 WK_UP 具有唤醒功能,该按键连接到 STM32 的 WAKE_UP(PA0)引脚,可用于待机模式下的唤醒, 在不使用唤醒功能的时候,也可以做为普通按键输入使用。 其他 3 个是普通按键,可以用于人机交互的输入,这 3 个按键是直接连接在 STM32 的 IO 口上的。这里注意 WK_UP 是高电平有效,而 KEY0、KEY1 和 KEY2 是低电平有效,大家在 使用的时候留意一下。 23. 参考电压选择端口 这是 STM32 的参考电压选择端口(P5),我们默认是接开发板的 3.3V(VDDA)。如果大 家想设置其他参考电压,只需要把你的参考电压源接到 Vref+和 GND 即可。 24. 触摸按钮 这是开发板板载的一个电容触摸输入按键(TPAD),利用电容充放电原理,实现触摸按键 检测。 25. 电源指示灯 这是开发板板载的一颗蓝色的 LED 灯(PWR),用于指示电源状态。在电源开启的时候(通 过板上的电源开关控制),该灯会亮,否则不亮。通过这个 LED,可以判断开发板的上电情况。 26. 多功能端口 这是 1 个由 6 个排针组成的一个接口(P10&P11)。不过大家可别小看这 6 个排针,这可是 本开发板设计的很巧妙的一个端口(由 P10 和 P11 组成),这组端口通过组合可以实现的功能 有:ADC 采集、DAC 输出、PWM DAC 输出、外部音频输入、电容触摸按键、DAC 音频、PWM DAC 音频、DAC ADC 自测等,所有这些,你只需要 1 个跳线帽的设置,就可以逐一实现。 27. MIC(咪头) 这是开发板的板载录音输入口(MIC),该咪头直接接到 VS1053 的输入上,可以用来实现 录音功能。 28. 录音输入接口 这是开发板板载的外部录音输入接口(LINE_IN),通过咪头我们只能实现单声道的录音, 而通过这个 LINE_IN,我们可以实现立体声录音。 29. 耳机输出接口 这是开发板板载的音频输出接口(PHONE),该接口可以插 3.5mm 的耳机,当 VS1053 放 音的时候,就可以通过在该接口插入耳机,欣赏音乐。 30. ATK 模块接口 这是开发板板载的一个 ALIENTEK 通用模块接口(U5),目前可以支持 ALIENTEK 开发 的 GPS 模块、蓝牙模块和 MPU6050 模块等,直接插上对应的模块,就可以进行开发。后续我 25 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 们将开发更多兼容该接口的其他模块,实现更强大的扩展性能。 31. RS232/模块选择接口 这是开发板板载的一个 RS232(COM3)/ATK 模块接口(U5)选择接口(P8),通过该选 择接口,我们可以选择 STM32 的串口 3 连接在 COM3 还是连接在 ATK 模块接口上面,以实现 不同的应用需求。这样的设计还有一个好处,就是我们的开发板还可以充当 RS232 到 TTL 串 口的转换(注意,这里的 TTL 高电平是 3.3V)。 32. 3.3V 电源输入/输出 这是开发板板载的一组 3.3V 电源输入输出排针(2*3)(VOUT1),用于给外部提供 3.3V 的电源,也可以用于从外部接 3.3V 的电源给板子供电。 大家在实验的时候可能经常会为没有 3.3V 电源而苦恼不已,有了 ALIENTEK 战舰 STM32F103,你就可以很方便的拥有一个简单的 3.3V 电源(USB 供电的时候,最大电流不能 超过 500mA,外部供电的时候,最大可达 1000mA)。 33. 5V 电源输入/输出 这是开发板板载的一组 5V 电源输入输出排针(2*3)(VOUT2),该排针用于给外部提供 5V 的电源,也可以用于从外部接 5V 的电源给板子供电。 同样大家在实验的时候可能经常会为没有 5V 电源而苦恼不已,ALIENTEK 充分考虑到了 大家需求,有了这组 5V 排针,你就可以很方便的拥有一个简单的 5V 电源(USB 供电的时候, 最大电流不能超过 500mA,外部供电的时候,最大可达 1000mA)。 34. RS232/485 选择接口 这是开发板板载的 RS232(COM2)/485 选择接口(P7),因为 RS485 基本上就是一个半 双工的串口,为了节约 IO,我们把 RS232(COM2)和 RS485 共用一个串口,通过 P7 来设置 当前是使用 RS232(COM2)还是 RS485。这样的设计还有一个好处。就是我们的开发板既可 以充当 RS232 到 TTL 串口的转换,又可以充当 RS485 到 TTL485 的转换。(注意,这里的 TTL 高电平是 3.3V)。 35. 电源开关 这是开发板板载的电源开关(K2)。该开关用于控制整个开发板的供电,如果切断,则整 个开发板都将断电,电源指示灯(PWR)会随着此开关的状态而亮灭。 36. DC6~24V 电源输入 这是开发板板载的一个外部电源输入口(DC_IN),采用标准的直流电源插座。开发板板载 了 DC-DC 芯片(MP2359),用于给开发板提供高效、稳定的 5V 电源。由于采用了 DC-DC 芯 片,所以开发板的供电范围十分宽,大家可以很方便的找到合适的电源(只要输出范围在 DC6~24V 的基本都可以)来给开发板供电。在耗电比较大的情况下,比如用到 4.3 屏/7 寸屏/ 网口的时候,建议使用外部电源供电,可以提供足够的电流给开发板使用。 37. RS485 总线接口 这是开发板板载的 RS485 总线接口(RS485),通过 2 个端口和外部 485 设备连接。这里提 醒大家,RS485 通信的时候,必须 A 接 A,B 接 B。否则可能通信不正常!另外,开发板自带 了终端电阻(120Ω)。 38. 以太网接口(RJ45) 这是开发板板载的网口(EARTHNET),可以用来连接网线,实现网络通信功能。该接口 使用 DM9000 作为网络芯片,该芯片自带 MAC 和 PHY,支持 10M/100M 网络,通过 8080 并 口同 STM32F103 的 FSMC 接口连接。 39. RS232 接口(母) 这是开发板板载的一个 RS232 接口(COM2),通过一个标准的 DB9 母头和外部的串口连 26 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 接。通过这个接口,我们可以连接带有串口的电脑或者其他设备,实现串口通信。 40. LCD 接口 这是开发板板载的 LCD 模块接口,该接口兼容 ALIENTEK 全系列 TFTLCD 模块,包括: 2.4 寸、2.8 寸、3.5 寸、4.3 寸和 7 寸等 TFTLCD 模块,并且支持电阻/电容触摸功能。 41. IS62WV51216 8M SRAM 这是开发板外扩的 SRAM 芯(U1)片,容量为 8M 位,也就是 1M 字节,这样,对大内存 需求的应用(比如 GUI), 就可以很好的实现了 42. JOYPAD/RS232 选择开关 这是开发板板载的一个游戏手柄接口(JOYPAD)和 RS232 接口选择开关(K1),开发板 的游戏手柄接口和 RS232 接口共用 COM3,它们需要分时复用。当插游戏手柄时,K1 需要打 在 JOYPAD 位置,此时,该接口(COM3)可以用来连接 FC 手柄(红白机/小霸王游戏机手柄), 这样大家可以在开发板上编写游戏程序,直接通过手柄玩游戏。当作为串口使用时,K1 需要打 在 RS232 位置。 43. JOYPAD/RS232 接口(公) 这是开发板板载的一个游戏手柄/RS232 接口(COM3),通过一个标准的 DB9 公头和外部 的 FC 手柄/RS232 串口连接。具体用作接游戏手柄接口还是 RS232 接口,可通过 K1 开关进行 选择。 44. CAN 接口 这是开发板板载的 CAN 总线接口(CAN),通过 2 个端口和外部 CAN 总线连接,即 CANH 和 CANL。这里提醒大家:CAN 通信的时候,必须 CANH 接 CANH,CANL 接 CANL,否则 可能通信不正常! 1.2.2 软件资源说明 上面我们详细介绍了 ALIENTEK 战舰 STM32F103 的硬件资源。接下来,我们将向大家简 要介绍一下战舰 STM32F103 的软件资源。 战舰 STM32F103 提供的标准例程多达 54 个,一般的 STM32 开发板仅提供库函数代码, 而我们则提供寄存器和库函数两个版本的代码(本手册以寄存器版本作为介绍)。我们提供的这 些例程,基本都是原创,拥有非常详细的注释,代码风格统一、循序渐进,非常适合初学者入 门。而其他开发板的例程,大都是来自 ST 库函数的直接修改,注释也比较少,对初学者来说 不那么容易入门。 战舰 STM32F103 的例程列表如表 1.2.2.1 所示: 编号 实验名字 编号 实验名字 1 跑马灯实验 28 红外遥控实验 2 蜂鸣器实验 29 游戏手柄实验 3 按键输入实验 30 DS18B20 数字温度传感器实验 4 串口实验 31 DHT11 数字温湿度传感器实验 5 外部中断实验 32 MPU6050 六轴传感器实验 6 独立看门狗实验 33 无线通信实验 7 窗口看门狗实验 34 FLASH 模拟 EEPROM 实验 8 定时器中断实验 35 摄像头实验 9 PWM 输出实验 36 外部 SRAM 实验 10 输入捕获实验 37 内存管理实验 11 电容触摸按键实验 38 SD 卡实验 27 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 12 OLED 显示实验 39 FATFS 实验 13 TFTLCD 显示实验 40 汉字显示实验 14 USMART 调试实验 41 图片显示实验 15 RTC 实验 42 照相机实验 16 待机唤醒实验 43 音乐播放器实验 17 ADC 实验 44 录音机实验 18 内部温度传感器实验 45 手写识别实验 19 光敏传感器实验 46 T9 拼音输入法实验 20 DAC 实验 47 串口 IAP 实验 21 PWM DAC 实验 48 USB 虚拟串口实验 22 DMA 实验 49 USB 读卡器实验 23 IIC 实验 50 网络通信实验 24 SPI 实验 51 UCOSII 实验 1-任务调度 25 485 实验 52 UCOSII 实验 2-信号量和邮箱 UCOSII 实验 3-消息队列、信号量 26 CAN 收发实验 53 集和软件定时器 27 触摸屏实验 54 战舰系统综合实验 表 1.2.2.1 ALIENTEK 战舰 STM32F103 例程表 从上表可以看出,ALIENTEK 战舰 STM32F103 的例程基本上涵盖了 STM32F103ZET6 的 所有内部资源,并且外扩展了很多有价值的例程,比如:FLASH 模拟 EEPROM 实验、USMART 调试实验、ucosii 实验、内存管理实验、IAP 实验、拼音输入法实验、手写识别实验、综合实验 等。 而且从上表可以看出,例程安排是循序渐进的,首先从最基础的跑马灯开始,然后一步步 深入,从简单到复杂,有利于大家的学习和掌握。所以,ALIENTEK 战舰 STM32F103 是非常 适合初学者的。当然,对于想深入了解 STM32 内部资源的朋友,ALIENTEK 战舰 STM32F103 也绝对是一个不错的选择。 1.2.3 战舰 V3 IO 引脚分配 为了让大家跟快更好的使用我们的战舰 V3 开发板,这里特地将战舰 V3 开发板主芯片: STM32F103ZET6 的 IO 资源分配做了一个总表,以便大家查阅。战舰 V3 的 IO 引脚分配总表 如表:1.2.3.1 所示: 战舰 V3 IO 资源分配表 引 独 脚 GPIO 连接资源 立 连接关系说明 34 PA0 WK_UP Y 1,按键 KEY_UP 2,可以做待机唤醒脚(WKUP) 35 PA1 STM_ADC TPAD Y ADC 输入引脚,同时做 TPAD 检测脚 36 PA2 USART2_TX 485_RX Y 1,RS232 串口 2(COM2)RX 脚(P7 设置) 2,RS485 RX 脚(P7 设置) 37 PA3 USART2_RX 485_TX Y 1,RS232 串口 2(COM2)TX 脚(P7 设置) 2,RS485 TX 脚(P7 设置) 28 40 PA4 STM_DAC 41 PA5 SPI1_SCK 42 PA6 SPI1_MISO 43 PA7 SPI1_MOSI 100 PA8 OV_VSYNC 101 PA9 USART1_TX 102 PA10 USART1_RX 103 PA11 USB_D- 104 PA12 USB_D+ 105 PA13 JTMS 109 PA14 JTCK 110 PA15 JTDI 46 PB0 47 PB1 48 PB2 LCD_BL T_SCK BOOT1 133 PB3 JTDO 134 PB4 JTRST 135 PB5 136 PB6 137 PB7 139 PB8 140 PB9 LED0 IIC_SCL IIC_SDA BEEP REMOTE_IN 69 PB10 USART3_TX 70 PB11 USART3_RX 73 PB12 F_CS GBC_KEY VS_SCK VS_MISO VS_MOSI PWM_DAC CAN_RX CAN_TX SWDIO SWDCLK GBC_LED T_MISO FIFO_WEN FIFO_RCLK STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 Y 1,DAC_OUT1 输出脚 2,ATK-MODULE 接口的 KEY 引脚 N SPI1 的 SCK 脚,连接 VS1053 的 SCK N SPI1 的 MISO 脚,连接 VS1053 的 MISO N SPI1 的 MOSI 脚,连接 VS1053 的 MOSI 1,OLED/CAMERA 接口的 VSYNC 脚 N 2,PWM_DAC 输出脚 Y 串口 1 TX 脚,默认连接 CH340 的 RX(P4 设置) Y 串口 1 RX 脚,默认连接 CH340 的 TX(P4 设置) Y 1,USB D-引脚(P9 设置) 2,CAN_RX 引脚(P9 设置) Y 1,USB D+引脚(P9 设置) 2,CAN_TX 引脚(P9 设置) N JTAG/SWD 仿真接口,没接任何外设 注意:如要做普通 IO,需先禁止 JTAG&SWD N JTAG/SWD 仿真接口,没接任何外设 注意:如要做普通 IO,需先禁止 JTAG&SWD 1,JTAG 仿真口(JTDI) N 2,ATK-MODULE 接口的 LED 引脚(使用时,需先禁 止 JTAG,才可以当普通 IO 使用) Y TFTLCD 接口背光控制脚 Y TFTLCD 接口触摸屏 SCK 信号 N 1,BOOT1,启动选择配置引脚(仅上电时用) 2,TFTLCD 接口触摸屏 MISO 信号 1,JTAG 仿真口(JTDO) N 2,OLED/CAMERA 接口 WEN 脚(使用时,需先禁止 JTAG,才可以当普通 IO 使用) 1,JTAG 仿真口(JTRST) N 2,OLED/CAMERA 接口 RCLK 脚(使用时,需先禁止 JTAG,才可以当普通 IO 使用) N 接 DS0 LED 灯(红色) N 接 24C02 的 SCL N 接 24C02 的 SDA N 接蜂鸣器(BEEP) N 接 HS0038 红外接收头 1,RS232 串口 3(COM3)RX 脚(P8 设置+K1 选择) Y 2,JOYPAD_DAT(P8 设置+K1 选择) 3,ATK-MODULE 接口的 RXD 脚(P8 设置) 1,RS232 串口 3(COM3)TX 脚(P8 设置+K1 选择) Y 2,JOYPAD_LAT(P8 设置+K1 选择) 3,ATK-MODULE 接口的 TXD 脚(P8 设置) N W25Q128 的片选信号 29 74 PB13 75 PB14 76 PB15 26 PC0 27 PC1 28 PC2 29 PC3 44 PC4 45 PC5 96 PC6 97 PC7 98 PC8 99 PC9 111 PC10 112 PC11 113 PC12 7 PC13 8 PC14 9 PC15 114 PD0 115 PD1 116 PD2 SPI2_SCK SPI2_MISO SPI2_MOSI OV_D0 OV_D1 OV_D2 OV_D3 OV_D4 OV_D5 OV_D6 OV_D7 SDIO_D0 SDIO_D1 SDIO_D2 SDIO_D3 SDIO_SCK VS_DREQ FSMC_D2 FSMC_D3 SDIO_CMD RTC 晶振 RTC 晶振 117 PD3 OV_SCL JOY_CLK 118 PD4 119 PD5 122 PD6 FSMC_NOE FSMC_NWE FIFO_WRST 123 PD7 DM9000_RST RS485_RE 77 PD8 78 PD9 79 PD10 80 PD11 81 PD12 82 PD13 85 PD14 86 PD15 141 PE0 142 PE1 1 PE2 2 PE3 3 PE4 FSMC_D13 FSMC_D14 FSMC_D15 FSMC_A16 FSMC_A17 FSMC_A18 FSMC_D0 FSMC_D1 FSMC_NBL0 FSMC_NBL1 KEY2 KEY1 KEY0 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 N W25Q128 和 WIRELESS 接口的 SCK 信号 N W25Q128 和 WIRELESS 接口的 MISO 信号 N W25Q128 和 WIRELESS 接口的 MOSI 信号 Y OLED/CAMERA 接口的 D0 脚 Y OLED/CAMERA 接口的 D1 脚 Y OLED/CAMERA 接口的 D2 脚 Y OLED/CAMERA 接口的 D3 脚 Y OLED/CAMERA 接口的 D4 脚 Y OLED/CAMERA 接口的 D5 脚 Y OLED/CAMERA 接口的 D6 脚 Y OLED/CAMERA 接口的 D7 脚 N SD 卡接口的 D0 N SD 卡接口的 D1 N SD 卡接口的 D2 N SD 卡接口的 D3 Y SD 卡接口的 SCK N 接 VS1053 芯片的 DREQ 脚 N 接 32.768K 晶振,不可用做 IO N 接 32.768K 晶振,不可用做 IO N FSMC 总线数据线 D2(LCD/SRAM/DM9000 共用) N FSMC 总线数据线 D3(LCD/SRAM/DM9000 共用) N SD 卡接口的 CMD N 1,OLED/CAMERA 接口的 SCL 信号 2,JOYPAD 的 CLK 信号(接在 COM3 口) N FSMC 总线 NOE(RD)(LCD/SRAM/DM9000 共用) N FSMC 总线 NWE(WR)(LCD/SRAM/DM9000 共用) Y OLED/CAMERA 接口的 WRST 信号 N 1,DM9000 复位引脚 2,RS485 RE 引脚 N FSMC 总线数据线 D13(LCD/SRAM/DM9000 共用) N FSMC 总线数据线 D14(LCD/SRAM/DM9000 共用) N FSMC 总线数据线 D15(LCD/SRAM/DM9000 共用) N FSMC 总线地址线 A17(SRAM 专用) N FSMC 总线地址线 A18(SRAM 专用) N FSMC 总线地址线 A19(SRAM 专用) N FSMC 总线数据线 D0(LCD/SRAM/DM9000 共用) N FSMC 总线数据线 D1(LCD/SRAM/DM9000 共用) N FSMC 总线 NBL0(SRAM 专用) N FSMC 总线 NBL1(SRAM 专用) Y 接按键 KEY2 Y 接按键 KEY1 Y 接按键 KEY0 30 4 PE5 5 PE6 58 PE7 59 PE8 60 PE9 63 PE10 64 PE11 65 PE12 66 PE13 67 PE14 68 PE15 10 PF0 11 PF1 12 PF2 13 PF3 14 PF4 15 PF5 18 PF6 19 PF7 20 PF8 21 PF9 22 PF10 49 PF11 50 PF12 53 PF13 54 PF14 55 PF15 56 PG0 57 PG1 87 PG2 88 PG3 89 PG4 90 PG5 91 PG6 92 PG7 93 PG8 124 PG9 125 PG10 126 PG11 127 PG12 LED1 VS_RST FSMC_D4 FSMC_D5 FSMC_D6 FSMC_D7 FSMC_D8 FSMC_D9 FSMC_D10 FSMC_D11 FSMC_D12 FSMC_A0 FSMC_A1 FSMC_A2 FSMC_A3 FSMC_A4 FSMC_A5 VS_XDCS VS_XCS LIGHT_SENSOR T_MOSI T_PEN T_CS FSMC_A6 FSMC_A7 FSMC_A8 FSMC_A9 FSMC_A10 FSMC_A11 FSMC_A12 FSMC_A13 FSMC_A14 FSMC_A15 NRF_IRQ DM9000_INT NRF_CS NRF_CE FSMC_NE2 FSMC_NE3 1WIRE_DQ FSMC_NE4 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 N 接 DS1 LED 灯(绿色) N 接 VS1053 芯片的 RST 脚 N FSMC 总线数据线 D4(LCD/SRAM/DM9000 共用) N FSMC 总线数据线 D5(LCD/SRAM/DM9000 共用) N FSMC 总线数据线 D6(LCD/SRAM/DM9000 共用) N FSMC 总线数据线 D7(LCD/SRAM/DM9000 共用) N FSMC 总线数据线 D8(LCD/SRAM/DM9000 共用) N FSMC 总线数据线 D9(LCD/SRAM/DM9000 共用) N FSMC 总线数据线 D10(LCD/SRAM/DM9000 共用) N FSMC 总线数据线 D11(LCD/SRAM/DM9000 共用) N FSMC 总线数据线 D12(LCD/SRAM/DM9000 共用) N FSMC 总线地址线 A0(SRAM 专用) N FSMC 总线地址线 A1(SRAM 专用) N FSMC 总线地址线 A2(SRAM 专用) N FSMC 总线地址线 A3(SRAM 专用) N FSMC 总线地址线 A4(SRAM 专用) N FSMC 总线地址线 A5(SRAM 专用) N 接 VS1053 芯片的 XDCS 脚 N 接 VS1053 芯片的 XCS 脚 N 接光敏传感器(LS1) Y TFTLCD 接口触摸屏 MOSI 信号 Y TFTLCD 接口触摸屏 PEN 信号 Y TFTLCD 接口触摸屏 CS 信号 N FSMC 总线地址线 A6(SRAM 专用) N FSMC 总线地址线 A7(SRAM/DM9000 共用) N FSMC 总线地址线 A8(SRAM 专用) N FSMC 总线地址线 A9(SRAM 专用) N FSMC 总线地址线 A10(SRAM/LCD 共用) N FSMC 总线地址线 A11(SRAM 专用) N FSMC 总线地址线 A12(SRAM 专用) N FSMC 总线地址线 A13(SRAM 专用) N FSMC 总线地址线 A14(SRAM 专用) N FSMC 总线地址线 A15(SRAM 专用) N 1,WIRELESS 接口 IRQ 信号 2,DM9000 中断信号 Y WIRELESS 接口的 CS 信号 Y WIRELESS 接口的 CE 信号 N FSMC 总线的片选信号 2,为 DM9000 片选信号 N FSMC 总线的片选信号 3,为外部 SRAM 片选信号 N 单总线接口(U6)数据线,接 DHT11/DS18B20 Y FSMC 总线的片选信号 4,为 LCD 片选信号 31 STM32F1 开发指南(库函数版) 128 PG13 129 PG14 132 PG15 OV_SDA FIFO_RRST FIFO_OE ALIENTEK 战舰 STM32F103 V3 开发板教程 Y OLED/CAMERA 接口的 SDA 脚 Y OLED/CAMERA 接口的 RRST 脚 Y OLED/CAMERA 接口的 OE 脚 表 1.2.3.1 战舰 V3 IO 资源分配总表 表 1.2.3.1 中,引脚栏即 STM32F103ZET6 的引脚编号;GPIO 栏则表示 GPIO;连接资源栏 表示了对应 GPIO 所连接到的网络;独立栏,表示该 IO 是否可以完全独立(不接其他任何外设 和上下拉电阻)使用,通过一定的方法,可以达到完全独立使用该 IO,Y 表示可做独立 IO,N 表示不可做独立 IO;连接关系栏,则对每个 IO 的连接做了简单的介绍。 该表在:光盘3,ALIENTEK 战舰 STM32F1 V3 开发板原理图 文件夹下有提供 Excel 格 式,并注有详细说明和使用建议,大家可以打开该表格的 Excel 版本,详细查看。 1.3 ALIENTEK 战舰 STM32 V3.0 升级说明 ALIENTEK 战舰 STM32 V3.0 开发板相对于过往版本,主要变化如表 1.3.1 所示: 编 对比项 号 ALIENTEK 战舰 STM32 开发板 之前版本 V3.0 版本 说明 1 SPI FLASH 芯片 W25Q64 W25Q128 容量更大 2 10/100M 以太网 无 有 新增 3 ATK-MODULE 接口 无 有(U5) 新增 4 光敏传感器 无 有(LS1) 新增 5 板载喇叭 无 有 新增 6 亚克力保护板 无 有 新增 JOYPAD/RS232 7 无 选择开关 有(K1) 新增 8 RS232/模块选择接口 无 有(P8) 新增 9 按键帽 无 有 新增 10 RS232 串口 1个 2个 新增 1 路 RS232 串口 11 SPI/SDIO 选择口 有(P10&P11) 无 统一用 SDIO 驱动 SD 卡 12 PS2 接口 有 无 淘汰老式接口 13 FM 收发 有 无 去掉不常用功能 14 3D 传感器 有 无 ATK-MODULE 接口扩展 15 VS1053 IIS 输出接口 有(P1) 无 去掉不常用接口 16 MIC 选择口 有(P2) 无 去掉不常用接口 17 参考电压选择端口 有(P7) 有(P5) 精简设计 18 CAN 接口 有(3P) 有(2P) 精简设计 19 RS485 接口 有(3P) 有(2P) 精简设计 20 DC 输入电压范围 6~16V 6~24V 改进设计 21 丝印加框标注 无 有 改进设计 22 部分外设大丝印标注 无 有 改进设计 23 PA4 STM_DAC GBC_KEY/STM_DAC 引脚复用 24 PA8 OV_VSYNC PWM_DAC/OV_VSYNC 引脚复用 25 PA15 JTDI GBC_LED/JTDI 引脚复用 26 PB2 T_CS T_MISO 引脚变更 32 STM32F1 开发指南(库函数版) 27 PB6 PWM_DAC ALIENTEK 战舰 STM32F103 V3 开发板教程 IIC_SCL 引脚变更 28 PB7 ASEL_B IIC_SDA 引脚变更 29 PB10 IIC_SCL USART3_TX 引脚变更 30 PB11 31 PC8 32 PC9 IIC_SDA JOY_LAT/SDIO_D0 JOY_DAT/SDIO_D1 USART3_RX SDIO_D0 SDIO_D1 引脚变更 取消复用 取消复用 33 PC10 PS_DAT/SDIO_D2 SDIO_D2 取消复用 34 PC11 PS_CLK/SDIO_D3 SDIO_D3 取消复用 35 PC12 JOY_CLK/SDIO_SCK SDIO_SCK 36 PD2 SD_CS/SDIO_CMD SDIO_CMD 取消复用 取消复用 37 PD3 OV_SCL JOY_CLK/OV_SCL 引脚复用 38 PD7 ASEL_A RS485_RE/DM9K_RST 引脚变更 39 PF8 40 PF11 41 PG6 T_MISO 3D_INT NRF_CE LIGHT_SENSOR T_CS NRF_IRQ/DM9K_IRQ 引脚变更 引脚变更 引脚变更 42 PG8 NRF_IRQ NRF_CE 引脚变更 43 PG9 RS485_RE FSMC_NE2 引脚变更 表 1.3.1 V3.0 版本 VS 过往版本硬件变更表 从表 1.3.1 可以看出,战舰 STM32F103 开发板 V3 版本在之前版本的基础上进行了较大的改 动,前 16 项是硬件删减改动,17~22 是对硬件的精简和改进设计,其余项目是 IO 引脚的改动 和变更。 硬件删减改动方面:SPI FLASH 换成了 W25Q128,容量为 16M 字节,比原来大了一倍。另外, 新增了 10/100M 自适应以太网、光敏传感器、扬声器、亚克力保护板等,同时,去掉了 PS2 接 口、FM 收发功能,3D 重力传感器等不常用的接口和功能,SD 采用兼容性更好的 SDIO 方式驱动。 硬件精简改进方面:对某些多余的接口进行了精简,另外,直流电压输入范围、丝印标注 等方面进行了改善加强,使用更加方便。 线路变更方面:根据硬件的改动,做了二十多项改变,详见表 1.3.1。 33 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第二章 实验平台硬件资源详解 本章,我们将节将向大家详细介绍 ALIENTEK 战舰 STM32F103 各部分的硬件原理图,让 大家对该开发板的各部分硬件原理有个深入理解,并向大家介绍开发板的使用注意事项,为后 面的学习做好准备。 本章将分为如下两节: 2.1,开发板原理图详解; 2.2,开发板使用注意事项; 2.3,STM32F103 学习方法; 2.1 开发板原理图详解 2.1.1 MCU ALIENTEK 战舰 STM32 开发板选择的是 STM32F103ZETT6 作为 MCU,该芯片是 STM32F103 里面配置非常强大的了,它拥有的资源包括:64KB SRAM、512KB FLASH、2 个 基本定时器、4 个通用定时器、2 个高级定时器、2 个 DMA 控制器(共 12 个通道)、3 个 SPI、 2 个 IIC、5 个串口、1 个 USB、1 个 CAN、3 个 12 位 ADC、1 个 12 位 DAC、1 个 SDIO 接口、 1 个 FSMC 接口以及 112 个通用 IO 口。该芯片的配置十分强悍,并且还带外部总线(FSMC) 可以用来外扩 SRAM 和连接 LCD 等,通过 FSMC 驱动 LCD,可以显著提高 LCD 的刷屏速度, 是 STM32F1 家族常用型号里面,最高配置的芯片了,所以我们选择了它作为我们战舰板的主 芯片。MCU 部分的原理图如图 2.1.1.1(因为原理图比较大,缩小下来可能有点看不清,请大 家打开开发板光盘的原理图进行查看)所示: 34 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 2.1.1.1 MCU 部分原理图 上图中 U2 为我们的主芯片:STM32F103ZET6。 这里主要讲解以下 3 个地方: 1,后备区域供电脚 VBAT 脚的供电采用 CR1220 纽扣电池和 VCC3.3 混合供电的方式,在 有外部电源(VCC3.3)的时候,CR1220 不给 VBAT 供电,而在外部电源断开的时候,则由 CR1220 给其供电。这样,VBAT 总是有电的,以保证 RTC 的走时以及后备寄存器的内容不丢失。 2,图中的 R8 和 R9 用隔离 MCU 部分和外部的电源,这样的设计主要是考虑了后期维护, 如果 3.3V 电源短路,可以断开这两个电阻,来确定是 MCU 部分短路,还是外部短路,有助于 生产和维修。当然大家在自己的设计上,这两个电阻是完全可以去掉的。 3,图中 P5 是参考电压选择端口。我们开发板默认是接板载的 3.3V 作为参考电压,如果 大家想用自己的参考电压,则把你的参考电压接入 Vref+即可。 35 2.1.2 引出 IO 口 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 ALIENTEK 战舰 STM32F103 引出了 STM32F103ZET6 的所有 IO 口,如图 2.1.2.1 所示: 图 2.1.2.1 引出 IO 口 图中 P1、P2 和 P3 为 MCU 主 IO 引出口,这三组排针共引出了 102 个 IO 口,STM32F103ZET6 总共有 112 个 IO,除去 RTC 晶振占用的 2 个,还剩 110 个,这三组主引出排针,总共引出了 102 个 IO,剩下的 8 个 IO 口分别通过:P4(PA9&PA10)、P7(PA2&PA3)、P8(PB10&PB11) 和 P9(PA11&PA12)等 4 组排针引出。 2.1.3 USB 串口/串口 1 选择接口 ALIENTEK 战舰 STM32F103 板载的 USB 串口和 STM32F103ZET6 的串口是通过 P4 连接 起来的,如图 2.1.3.1 所示: 图 2.3.1.1 USB 串口/串口 1 选择接口 36 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图中 TXD/RXD 是相对 CH340G 来说的,也就是 USB 串口的发送和接收引脚。而 USART1_RX 和 USART1_TX 则是相对于 STM32F103ZET6 来说的。这样,通过对接,就可以 实现 USB 串口和 STM32F103ZET6 的串口通信了。同时,P4 是 PA9 和 PA10 的引出口。 这样设计的好处就是使用上非常灵活。比如需要用到外部 TTL 串口和 STM32 通信的时候, 只需要拔了跳线帽,通过杜邦线连接外部 TTL 串口,就可以实现和外部设备的串口通信了;又 比如我有个板子需要和电脑通信,但是电脑没有串口,那么你就可以使用开发板的 RXD 和 TXD 来连接你的设备,把我们的开发板当成 USB 转 TTL 串口用了。 2.1.4 JTAG/SWD ALIENTEK 战舰 STM32F103 板载的标准 20 针 JTAG/SWD 接口电路如图 2.1.4.1 所示: 图 2.1.4.1 JTAG/SWD 接口 这里,我们采用的是标准的 JTAG 接法,但是 STM32 还有 SWD 接口,SWD 只需要 2 根 线(SWCLK 和 SWDIO)就可以下载并调试代码了,这同我们使用串口下载代码差不多,而且 速度非常快,能调试。所以建议大家在设计产品的时候,可以留出 SWD 来下载调试代码,而 摒弃 JTAG。STM32 的 SWD 接口与 JTAG 是共用的,只要接上 JTAG,你就可以使用 SWD 模 式了(其实并不需要 JTAG 这么多线),当然,你的调试器必须支持 SWD 模式,JLINK V7/V8、 ULINK2 和 ST LINK 等都支持 SWD 调试。 特别提醒,JTAG 有几个信号线用来接其他外设了,但是 SWD 是完全没有接任何其他外设 的,所以在使用的时候,推荐大家一律使用 SWD 模式!!! 2.1.5 SRAM ALIENTEK 战舰 STM32F103 外扩了 1M 字节的 SRAM 芯片,如图 2.1.5.1 所示,注意图中 的地址线标号,是以 IS61LV51216 为模版的,但是和 IS62WV51216 的 datasheet 标号有出入, 不过,因为地址的唯一性,这并不会影响我们使用 IS62WV51216(特别提醒:地址线可以乱, 但是数据线必须一致!!),因此,该原理图对这两个芯片都是可以正常使用的。 37 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 2.1.5.1 外扩 SRAM 图中 U1 为外扩的 SRAM 芯片,型号为:IS62WV51216,容量为 1M 字节,该芯片挂在 STM32 的 FSMC 上。这样大大扩展了 STM32 的内存(芯片本身有 64K 字节),从而在需要大内存的场 合,战舰 STM32F103 也可以胜任。 2.1.6 LCD 模块接口 ALIENTEK 战舰 STM32F103 板载的 LCD 模块接口电路如图 2.1.6.1 所示: 图 2.1.6.1 LCD 模块接口 38 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图中 TFT_LCD 是一个通用的液晶模块接口,支持 ALIENTEK 全系列 TFTLCD 模块,包括: 2.4 寸、2.8 寸、3.5 寸、4.3 寸和 7 寸等尺寸的 TFTLCD 模块。LCD 接口连接在 STM32F103ZET6 的 FSMC 总线上面,可以显著提高 LCD 的刷屏速度。 图中的 T_MISO/T_MOSI/T_PEN/T_SCK/T_CS 连接在 MCU 的 PB2/PF9/PF10/PB1/PF11 上, 这些信号用来实现对液晶触摸屏的控制(支持电阻屏和电容屏)。LCD_BL 连接在 MCU 的 PB0 上,用于控制 LCD 的背光。液晶复位信号 RESET 则是直接连接在开发板的复位按钮上,和 MCU 共用一个复位电路。 2.1.7 复位电路 ALIENTEK 战舰 STM32F103 的复位电路如图 2.1.7.1 所示: 图 2.1.7.1 复位电路 因为 STM32 是低电平复位的,所以我们设计的电路也是低电平复位的,这里的 R3 和 C12 构成了上电复位电路。同时,开发板把 TFT_LCD 的复位引脚也接在 RESET 上,这样这个复位 按钮不仅可以用来复位 MCU,还可以复位 LCD。 2.1.8 启动模式设置接口 ALIENTEK 战舰 STM32F103 的启动模式设置端口电路如图 2.1.8.1 所示: 图 2.1.8.1 启动模式设置接口 上图的 BOOT0 和 BOOT1 用于设置 STM32 的启动方式,其对应启动模式如表 2.1.8.1 所示: 表 2.1.8.1 BOOT0、BOOT1 启动模式表 按照表 2. 1.8.1,一般情况下如果我们想用用串口下载代码,则必须配置 BOOT0 为 1,BOOT1 39 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 为 0,而如果想让 STM32 一按复位键就开始跑代码,则需要配置 BOOT0 为 0,BOOT1 随便设 置都可以。这里 ALIENTEK 战舰 STM32F103 专门设计了一键下载电路,通过串口的 DTR 和 RTS 信号,来自动配置 BOOT0 和 RST 信号,因此不需要用户来手动切换他们的状态,直接串 口下载软件自动控制,可以非常方便的下载代码。 2.1.9 RS232 串口/JOYPAD 接口 ALIENTEK 战舰 STM32F103 板载了一公一母两个 RS232 接口,其中 COM3 不但可以接 RS232 还可以接游戏手柄(JOYPAD),电路原理图如图 2.1.9.1 所示: 图 2.1.9.1 RS232 串口 因为 RS232 电平不能直接连接到 STM32,所以需要一个电平转换芯片。这里我们选择的 是 SP3232(也可以用 MAX3232)来做电平转接,同时图中的 P7 用来实现 RS232(COM2)/RS485 的选择,P8 用来实现 RS232(COM3)/ATK 模块接口的选择,以满足不同实验的需要。 图中 COM2 是母头,COM3 是公头,而且 COM3 可以接 RS232 串口或者接 FC 游戏手柄 (JOYPAD),具体选择哪个功能,则是通过 K1 开关来切换(请看板载丝印)。使用的时候,要 特别注意,K1 先设置对了,再去接 RS232 串口/FC 游戏手柄。 图中 USART2_TX/USART2_RX 连接在 MCU 的串口 2 上(PA2/PA3),所以这里的 RS232(COM2)/RS485 都是通过串口 2 来实现的。图中 RS485_TX 和 RS485_RX 信号接在 SP3485 的 DI 和 RO 信号上。 而图中的 USART3_TX/USART3_RX 则是连接在 MCU 的串口 3 上(PB10/PB11),所以 RS232(COM3)/ATK 模块接口都是通过串口 3 来实现的。图中 GBC_RX 和 GBC_TX 连接在 ATK 模块接口 U5 上面。 因为 P7/P8 的存在,其实还带来另外一个好处,就是我们可以把开发板变成一个 RS232 电 平转换器,或者 RS485 电平转换器,比如你买的核心板,可能没有板载 RS485/RS232 接口,通 过连接战舰 STM32F103 的 P7/P8 端口,就可以让你的核心板拥有 RS232/RS485 的功能。 2.1.10 RS485 接口 ALIENTEK 战舰 STM32F103 板载的 RS485 接口电路如图 2.1.10.1 所示: 40 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 2.1.10.1 RS485 接口 RS485 电平也不能直接连接到 STM32,同样需要电平转换芯片。这里我们使用 SP3485 来 做 485 电平转换,其中 R25 为终端匹配电阻,而 R22 和 R19,则是两个偏置电阻,以保证静默 状态时,485 总线维持逻辑 1。 RS485_RX/RS485_TX 连接在 P7 上面,通过 P7 跳线来选择是否连接在 MCU 上面, RS485_RE 则是直接连接在 MCU 的 IO 口(PD7)上的,该信号用来控制 SP3485 的工作模式(高 电平为发送模式,低电平为接收模式)。 另外,特别注意:RS485_RE 和 DM9000_RST 共同接在 PD7 上面,在同时用到这两个外设 的时候,需要注意下。 2.1.11 CAN/USB 接口 ALIENTEK 战舰 STM32F103 板载的 CAN 接口电路以及 STM32 USB 接口电路如图 2.1.11.1 所示: 图 2.1.11.1 CAN/USB 接口 CAN 总线电平也不能直接连接到 STM32,同样需要电平转换芯片。这里我们使用 TJA1050 来做 CAN 电平转换,其中 R30 为终端匹配电阻。 USB_D+/USB_D-连接在 MCU 的 USB 口(PA12/PA11)上,同时,因为 STM32 的 USB 和 CAN 共用这组信号,所以我们通过 P9 来选择使用 USB 还是 CAN。 USB_SLAVE 可以用来连接电脑,实现 USB 读卡器或 USB 虚拟串口等 USB 从机实验。另 外,该接口还具有供电功能,VUSB 为开发板的 USB 供电电压,通过这个 USB 口,就可以给 整个开发板供电了。 41 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 2.1.12 EEPROM ALIENTEK 战舰 STM32F103 板载的 EEPROM 电路如图 2.1.12.1 所示: 图 2.1.12.1 EEPROM EEPROM 芯片我们使用的是 24C02,该芯片的容量为 2Kb,也就是 256 个字节,对于我们 普通应用来说是足够了的。当然,你也可以选择换大容量的芯片,因为我们的电路在原理上是 兼容 24C02~24C512 全系列 EEPROM 芯片的。 这里我们把 A0~A2 均接地,对 24C02 来说也就是把地址位设置成了 0 了,写程序的时候 要注意这点。IIC_SCL 接在 MCU 的 PB6 上,IIC_SDA 接在 MCU 的 PB7 上,这里我们虽然接 到了 STM32 的硬件 IIC 上,但是我们并不提倡使用硬件 IIC,因为 STM32 的 IIC 是鸡肋!请谨 慎使用。 2.1.13 光敏传感器 ALIENTEK 战舰 STM32F103 板载了一个光敏传感器,可以用来感应周围光线的变化,该 部分电路如图 2.1.13.1 所示: 图 2.1.13.1 光敏传感器电路 图中的 LS1 就是光敏传感器,其实就是一个光敏二极管,周围环境越亮,电流越大,反之 电流越小,即可等效为一个电阻,环境越亮阻值越小,反之越大,从而通过读取 LIGHT_SENSOR 的电压,即可知道周围环境光线强弱。LIGHT_SENSOR 连接在 MCU 的 ADC3_IN6(ADC3 通 道 6)上面,即 PF8 引脚。 2.1.14 SPI FLASH ALIENTEK 战舰 STM32F103 板载的 SPI FLASH 电路如图 2.1.14.1 所示: 42 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 2.1.14.1 SPI FLASH 芯片 SPI FLASH 芯片型号为 W25Q128,该芯片的容量为 128Mb,也就是 16M 字节。该芯片和 NRF24L01 共用一个 SPI(SPI2),通过片选来选择使用某个器件,在使用其中一个器件的时候, 请务必禁止另外一个器件的片选信号。 图中 F_CS 连接在 MCU 的 PB12 上,SPI2_SCK/SPI2_MOSI/SPI2_MISO 则分别连接在 MCU 的 PB13/PB15/PB14 上。 2.1.15 温湿度传感器接口 ALIENTEK 战舰 STM32F103 板载的温湿度传感器接口电路如图 2.1.15.1 所示: 图 2.1.15.1 温湿度传感器接口 该接口(U6)支持 DS18B20/DS1820/DHT11 等单总线数字温湿度传感器。1WIRE_DQ 是 传感器的数据线,该信号连接在 MCU 的 PG11 上。 2.1.16 红外接收头 ALIENTEK 战舰 STM32F103 板载的红外接收头电路如图 2.1.16.1 所示: 图 2.1.16.1 红外接收头 43 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 HS0038 是一个通用的红外接收头,几乎可以接收市面上所有红外遥控器的信号,有了它, 就可以用红外遥控器来控制开发板了。REMOTE_IN 为红外接收头的输出信号,该信号连接在 MCU 的 PB9 上。 2.1.17 无线模块接口 ALIENTEK 战舰 STM32F103 板载的无线模块接口电路如图 2.1.17.1 所示: 图 2.1.17.1 无线模块接口 该接口用来连接 NRF24L01 或者 RFID 等无线模块,从而实现开发板与其他设备的无线数 据传输(注意:NRF24L01 不能和蓝牙/WIFI 连接)。NRF24L01 无线模块的最大传输速度可以 达到 2Mbps,传输距离最大可以到 30 米左右(空旷地,无干扰)。 NRF_CE/NRF_CS/NRF_IRQ 连接在 MCU 的 PG8/PG7/PG6 上,而另外 3 个 SPI 信号则和 SPI FLASH 共用,接 MCU 的 SPI2。这里需要注意的是 PG6 还接了 DM9000_INT 这个信号, 所以在使用 NRF_IRQ 中断引脚的时候,不能和 DM9000 同时使用,不过,如果没用到 NRF_IRQ 中断引脚,那么 DM9000 和无线模块就可以同时使用了。 2.1.18 LED ALIENTEK 战舰 STM32F103 板载总共有 3 个 LED,其原理图如图 2.1.18.1 所示: 图 2.1.18.1 LED 其中 PWR 是系统电源指示灯,为蓝色。LED0(DS0)和 LED1(DS1)分别接在 PB5 和 PE5 上。 为了方便大家判断,我们选择了 DS0 为红色的 LED,DS1 为绿色的 LED。 44 2.1.19 按键 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 ALIENTEK 战舰 STM32F103 板载总共有 4 个输入按键,其原理图如图 2.1.19.1 所示: 图 2.1.19.1 输入按键 KEY0、KEY1 和 KEY2 用作普通按键输入,分别连接在 PE4、PE3 和 PE2 上,这里并没有 使用外部上拉电阻,但是 STM32 的 IO 作为输入的时候,可以设置上下拉电阻,所以我们使用 STM32 的内部上拉电阻来为按键提供上拉。 WK_UP 按键连接到 PA0(STM32 的 WKUP 引脚),它除了可以用作普通输入按键外,还可 以用作 STM32 的唤醒输入。注意:这个按键是高电平触发的。 2.1.20 TPAD 电容触摸按键 ALIENTEK 战舰 STM32F103 板载了一个电容触摸按键,其原理图如图 2.1.20.1 所示: 图 2.1.20.1 电容触摸按键 图中 5.1M 电阻是电容充电电阻,TPAD 并没有直接连接在 MCU 上,而是连接在多功能端 口(P10)上面,通过跳线帽来选择是否连接到 STM32。多功能端口,我们将在 2.1.25 节介绍。 电容触摸按键的原理我们将在后续的实战篇里面介绍。 2.1.21 OLED/摄像头模块接口 ALIENTEK 战舰 STM32F103 板载了一个 OLED/摄像头模块接口,其原理图如图 2.1.21.1 所示: 45 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 2.1.21.1 OLED/摄像头模块接口 图中 P6 是接口可以用来连接 ALIENTEK OLED 模块或者 ALIENTEK 摄像头模块。如果 是 OLED 模块,则 FIFO_WEN 和 OV_VSYNC 不需要接(在板上靠左插即可),如果是摄像头 模块,则需要用到全部引脚。 其中,OV_SCL/OV_SDA/FIFO_WRST/FIFO_RRST/FIFO_OE 这 5 个信号是分别连接在 MCU 的 PD3/PG13/PD6/PG14/PG15 上面,OV_D0~OV_D7 则连接在 PC0~7 上面(放在连续的 IO 上,可以提高读写效率),FIFO_RCLK/FIFO_WEN/OV_VSYNC 这 3 个信号是分别连接在 MCU 的 PB4/PB3/PA8 上面。其中 PB3 和 PB4 又是 JTAG 的 JTRST/JTDO 信号,所以在使用 OV7670 的时候,不要用 JTAG 仿真,要选择 SWD 模式(所以我们建议大家直接用 SWD 模式 来连接我们的开发板,这样所有的实验都可以仿真!)。 特别注意:OV_SCL 和 JOY_CLK 共用 PD3,OV_VSYNC 和 PWM_DAC 共用 PA8,他们 必须分时复用。在使用的时候,需要注意这个问题。 2.1.22 有源蜂鸣器 ALIENTEK 战舰 STM32F103 板载了一个有源蜂鸣器,其原理图如图 2.1.22.1 所示: 图 2.1.22.1 有源蜂鸣器 有源蜂鸣器是指自带了震荡电路的蜂鸣器,这种蜂鸣器一接上电就会自己震荡发声。而如 果是无源蜂鸣器,则需要外加一定频率(2~5Khz)的驱动信号,才会发声。这里我们选择使用 有源蜂鸣器,方便大家使用。 图中 Q1 是用来扩流,R38 则是一个下拉电阻,避免 MCU 复位的时候,蜂鸣器可能发声的 46 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 现象。BEEP 信号直接连接在 MCU 的 PB8 上面,PB8 可以做 PWM 输出,所以大家如果想玩 高级点(如:控制蜂鸣器“唱歌”),就可以使用 PWM 来控制蜂鸣器。 2.1.23 SD 卡接口 ALIENTEK 战舰 STM32F103 板载了一个 SD 卡(大卡/相机卡)接口,其原理图如图 2.1.23.1 所示: 图 2.1.23.1 SD 卡 图中 SD_CARD 为 SD 卡接口,该接口在开发板的底面。 SD 卡采用 4 位 SDIO 方式驱动,理论上最大速度可以达到 12MB/S,非常适合需要高速存 储的情况。图中:SDIO_D0/SDIO_D1/SDIO_D2/SDIO_D3/SDIO_SCK/SDIO_CMD 分别连接在 MCU 的 PC8/PC9/PC10/PC11/PC12/PD2 上面。 2.1.24 ATK 模块接口 ALIENTEK 战舰 STM32F103 板载了 ATK 模块接口,其原理图如图 2.1.24.1 所示: 图 2.1.24.1 ATK 模块接口 如图所示,U5 是一个 1*6 的排座,可以用来连接 ALIENTEK 推出的一些模块,比如:蓝 牙串口模块、GPS 模块、MPU6050 模块等。有了这个接口,我们连接模块就非常简单,插上即 可工作。 47 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图中:GBC_TX/GBC_RX 可通过 P8 排针,选择接入 PB11/PB10(即串口 3),详见 2.1.9 节。而 GBC_KEY 和 GBC_LED 则分别连接在 MCU 的 PA4 和 PA15 上面。特别注意:GBC_KEY 与 PWM_DAC 共用 PA4,GBC_LED 和 JTDI 共用 PA15,在使用的时候,要注意这个问题。 2.1.25 多功能端口 ALIENTEK 战舰 STM32F103 板载的多功能端口,是由 P10 和 P11 构成的一个 6PIN 端口, 其原理图如图 2.1.25.1 所示: 图 2.1.25.1 多功能端口 从上图,大家可能还看不出这个多功能端口的全部功能,别担心,下面我们会详细介绍。 首先介绍右侧的 P10,其中 TPAD 为电容触摸按键信号,连接在电容触摸按键上。STM_ADC 和 STM_DAC 则分别连接在 PA1 和 PA4 上,用于 ADC 采集或 DAC 输出。当需要电容触摸按 键的时候,我们通过跳线帽短接 TPAD 和 STM_ADC,就可以实现电容触摸按键(利用定时器 的输入捕获)。STM_DAC 信号则既可以用作 DAC 输出,也可以用作 ADC 输入,因为 STM32 的该管脚同时具有这两个复用功能。特别注意:STM_DAC 与摄像头的 GBC_KEY 共用 PA4, 所以他们不可以同时使用,但是可以分时复用。 我们再来看看 P11,PWM_DAC 连接在 MCU 的 PA8,是定时器 1 的通道 1 输出,后面跟 一个二阶 RC 滤波电路,其截止频率为 33.8Khz。经过这个滤波电路,MCU 输出的方波就变为 直流信号了。PWM_AUDIO 是一个音频输入通道,它连接到 TDA1308 和 HT6872 的输入,输 出到耳机/扬声器。特别注意:PWM_DAC 和 OV_VSYNC 共用 PA8,所以 PWM_DAC 和摄像 头模块,不可以同时使用,不过,可以分时复用。 单独介绍完了 P10 和 P11,我们再来看看他们组合在一起的多功能端口,如图 2.1.25.2 所 示: 图 2.1.25.2 组合后的多功能端口 图中 AIN 是 PWM_AUDIO,PDC 是滤波后的 PWM_DAC 信号。下面我们来看看通过 1 个 跳线帽,这个多功能接口可以实现哪些功能。 48 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 当不用跳线帽的时候:1,AIN 和 GND 组成一个音频输入通道。2,PDC 和 GND 组成一 个 PWM_DAC 输出;3,DAC 和 GND 组成一个 DAC 输出/ADC 输入(因为 DAC 脚也刚好也 可以做 ADC 输入);4,ADC 和 GND 组成一组 ADC 输入;5,TPAD 和 GND 组成一个触摸按 键接口,可以连接其他板子实现触摸按键。 当使用 1 个跳线帽的时候:1,AIN 和 PDC 组成一个 MCU 的音频输出通道,实现 PWM DAC 播放音乐。2,AIN 和 DAC 同样可以组成一个 MCU 的音频输出通道,也可以用来播放音乐。 3,DAC 和 ADC 组成一个自输出测试,用 MCU 的 ADC 来测试 MCU 的 DAC 输出。4,PDC 和 ADC,组成另外一个子输出测试,用 MCU 的 ADC 来测试 MCU 的 PWM DAC 输出。5, ADC 和 TPAD,组成一个触摸按键输入通道,实现 MCU 的触摸按键功能。 从上面的分析,可以看出,这个多功能端口可以实现 10 个功能,所以,只要设计合理,1+1 是大于 2 的。 2.1.26 以太网接口(RJ45) ALIENTEK 战舰 STM32F103 板载了一个以太网接口(RJ45),其原理图如图 2.1.26.1 所示: 图 2.1.26.1 以太网接口电路 STM32F1 本身并不带网络功能,战舰 STM32 开发板 V3 板载了一颗 DM9000 网络接口芯 片,用于给 STM32F1 提供网络接口,该芯片集成以太网 MAC 控制器与一般处理接口,一个 10/100M 自适应的 PHY 和 4K DWORD 值的 SRAM,有了它,战舰 V3 STM32 开发板就可以实 现网络相关的功能了。 DM9000 和 STM32F103ZET6 的连接通过 16 位并口连接(FSMC),其中 SD0~SD15 连接 在 FSMC_D0~FSMC_D15,DM9000_RST/DM9000_INT/FSMC_NE2/FSMC_NWE/FSMC_NOW/ FSMC_A7 分别连接在 PD7/PG6/PG9/PD5/PD4/PF13 上。战舰 STM32F103 开发板的 FSMC 总线 上总共挂了 3 个器件:LCD、IS62WV51216 和 DM9000,他们通过不同片选分时复用,互不影 响。 49 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 特别注意:DM9000_RST 和 RS485_RE 共用 PD7,DM9000_INT 和 NRF_IRQ 共用 PG6, 在使用的时候,注意要分时复用。另外,DM9000 的中断引脚,我们加了 D9 二极管,防止干 扰 NRF_IRQ,所以,在战舰板上,仅支持低电平有效的中断方式。 2.1.27 耳机输出 ALIENTEK 战舰 STM32 开发板板载的音频输出电路,其原理图如图 2.1.27.1 所示: 图 2.1.27.1 音频输出电路 图 中 PHONE 为 立 体 声音 频 输 出 插座 , 可 以 直接 插 3.5mm 的 耳 机 。 MP3_LEFT 和 MP3_RIGHT 和是 VS1053 的左右声道输出信号。另外 PWM_AUDIO,则是来自多功能接口 P11 的 PWM 音频/外部音频输入,耦合到 TDA1308 的一个通道,所以,PWM_AUDIO 只可以单声 道输出。另外,SPK_IN,则是 HT6872 的输入,这个信号最终将通过板载喇叭输出声音,详见 2.1.28 节。 图中的 TDA1308 是 AB 类的数字音频(CD)专用耳机功放 IC。其具有低电压、低失真、高 速率、强输出等优异的性能是以往的 TDA2822、TDA7050、LM386 等“经典”功放望尘莫及 的。同时战舰 STM32 开发板搭载了效果一流的 VS1053 编解码芯片,所以,战舰 STM32 开发 板播放 MP3 的音质是非常不错的,胜过市面上很多中低端 MP3 的音质。 2.1.28 板载喇叭 ALIENTEK 战舰 STM32 开发板板载了一个小喇叭(扬声器),通过 D 类功放驱动,其原理 图如图 2.1.28.1 所示: 图 2.1.28.1 喇叭输出电路 HT6872 是一款低 EMI,防削顶失真的,单声道免滤波 D 类音频功率放大器。在 6.5V 电源, 50 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 10% THD+N,4Ω 负载条件下,输出 4.71W 功率,在各类音频终端应用中维持高效率并提供 AB 类放大器的性能。在战舰 STM32 开发板 V3 上面,我们采用它来驱动板载的 8Ω 2W 喇叭。 图中 SP-和 SP+是喇叭焊接焊盘(在开发板底部),开发板板载的喇叭就是焊接在这两个焊 盘上。SPK_IN 是 HT6872 的音频信号输入端,该信号来自 MP3_LEFT/PWM_AUDIO(见 2.1.27 节)。SPK_CTRL 则由 VS1053 的 GPIO4 控制(见 2.1.29 节),当 SPK_CTRL 为低电平的时候, HT6872 进入关断模式,当 SPK_CTRL 为高电平的时候,HT6872 正常工作,这里我们加了 10K 的下拉电阻,所以,默认情况下,HT6872 是关断的,也就是喇叭并不会发声。我们必须在程 序上,控制 VS1053 的 GPIO4 输出高电平,才可以使板载喇叭发声。 有了板载喇叭,我们就可以直接通过板载喇叭欣赏开发板播放的音乐或者其他音频了,更 加人性化。 2.1.29 音频编解码 ALIENTEK 战舰 STM32 开发板板载 VS1053 音频编解码芯片,其原理图如图 2.1.29.1 所示: 2.1.29.1 音频编解码芯片 VS1053 是一颗单片 OGG/MP3/AAC/WMA/MIDI 音频解码器,通过 patch 可以实现 FLAC 的解码,同时该芯片可以支持 IMA ADPCM 编码,通过 patch 可以实现 OGG 编码。相比它的 前辈:VS1003,VS1053 性能提升了不少,比如支持 OGG 编解码,支持 FLAC 解码,同时音 质上也有比较大的提升,还支持空间效果设置。 图中 MP3_LEFT/MP3_RIGHT 这两个信号是 VS1053 的音频输出接口,输出到耳机/板载喇 51 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 叭(详见:2.1.27 节)。VS1053 通过 7 根线连接到 MCU,VS_MISO/VS_MOSI/VS_SCK/VS_XCS/ VS_XDCS/VS_DREQ/VS_RST 这 7 根线分别连接到 MCU 的 PA6/PA7/PA5/PF7/PF6/PC13/PE6 上,VS1053 通过 STM32 的 SPI1 访问。 图中的 SPK_CTRL 连接在 VS1053 的 GPIO4 上面,用于控制 HT6872 是否工作,从而控制 板载喇叭是否出声,要让板载喇叭发声,必须通过软件控制 VS1053 的 GPIO4 输出高电平,否 则板载喇叭关闭。 2.1.30 电源 ALIENTEK 战舰 STM32F103 板载的电源供电部分,其原理图如图 2.1.30.1 所示: 图 2.1.30.1 电源 图中,总共有 3 个稳压芯片:U12/U13/U15,DC_IN 用于外部直流电源输入,范围是 DC6~24V,输入电压经过 U13 DC-DC 芯片转换为 5V 电源输出,其中 D4 是防反接二极管,避 免外部直流电源极性搞错的时候,烧坏开发板。K2 为开发板的总电源开关,F1 为 1000ma 自恢 复保险丝,用于保护 USB。U12 为 3.3V 稳压芯片,给开发板提供 3.3V 电源,而 U15 则是 1.8V 稳压芯片,供 VS1053 的 CVDD 使用。 这里还有 USB 供电部分没有列出来,其中 VUSB 就是来自 USB 供电部分,我们将在 2.1.32 节进行介绍。 2.1.31 电源输入输出接口 ALIENTEK 战舰 STM32F103 板载了两组简单电源输入输出接口,其原理图如图 2.1.31.1 所示: 52 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 2.1.31.1 电源 图中,VOUT1 和 VOUT2 分别是 3.3V 和 5V 的电源输入输出接口,有了这 2 组接口,我们 可以通过开发板给外部提供 3.3V 和 5V 电源了,虽然功率不大(最大 1000ma),但是一般情况 都够用了,大家在调试自己的小电路板的时候,有这两组电源还是比较方便的。同时这两组端 口,也可以用来由外部给开发板供电。 图中 D6 和 D7 为 TVS 管,可以有效避免 VOUT 外接电源/负载不稳的时候(尤其是开发板 外接电机/继电器/电磁阀等感性负载的时候),对开发板造成的损坏。同时还能一定程度防止外 接电源接反,对开发板造成的损坏。 2.1.32 USB 串口 ALIENTEK 战舰 STM32F103 板载了一个 USB 串口,其原理图如图 2.1.32.1 所示: 图 2.1.32.1 USB 串口 USB 转串口,我们选择的是 CH340G,是国内芯片公司南京沁恒的产品,稳定性经测试还 不错,所以还是多支持下国产。 图中 Q2 和 Q3 的组合构成了我们开发板的一键下载电路,只需要在 flymcu 软件设置:DTR 的低电平复位,RTS 高电平进 BootLoader。就可以一键下载代码了,而不需要手动设置 B0 和 按复位了。其中,RESET 是开发板的复位信号,BOOT0 则是启动模式的 B0 信号。 一键下载电路的具体实现过程:首先,flymcu 控制 DTR 输出低电平,则 DTR_N 输出高, 然后 RTS 置高,则 RTS_N 输出低,这样 Q3 导通了,BOOT0 被拉高,即实现设置 BOOT0 为 1, 53 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 同时 Q2 也会导通,STM32F1 的复位脚被拉低,实现复位。然后,延时 100ms 后,flymcu 控制 DTR 为高电平,则 DTR_N 输出低电平,RTS 维持高电平,则 RTS_N 继续为低电平,此时 STM32F1 的复位引脚,由于 Q2 不再导通,变为高电平,STM32F1 结束复位,但是 BOOT0 还是维持为 1,从而进入 ISP 模式,接着 flymcu 就可以开始连接 STM32F1,下载代码了,从而实现一键下 载。 USB_232 是一个 MiniUSB 座,提供 CH340G 和电脑通信的接口,同时可以给开发板供电, VUSB 就是来自电脑 USB 的电源,USB_232 是本开发板的主要供电口。 2.2 开发板使用注意事项 为了让大家更好的使用 ALIENTEK 战舰 STM32F103,我们在这里总结该开发板使用的时 候尤其要注意的一些问题,希望大家在使用的时候多多注意,以减少不必要的问题。 1, 开发板一般情况是由 USB_232 口供电,在第一次上电的时候由于 CH340G 在和电脑建 立连接的过程中,导致 DTR/RTS 信号不稳定,会引起 STM32 复位 2~3 次左右,这个 现象是正常的,后续按复位键就不会出现这种问题了。 2, 1 个 USB 供电最多 500mA,且由于导线电阻存在,供到开发板的电压,一般都不会有 5V,如果使用了很多大负载外设,比如 4.3 寸屏、7 寸屏、网络、摄像头模块等,那么 可能引起 USB 供电不够,所以如果是使用 4.3 屏/7 寸屏的朋友,或者同时用到多个模 块的时候,建议大家使用一个独立电源供电。 如果没有独立电源,建议可以同时插 2 个 USB 口,并插上 JTAG,这样供电可以更足一些。 3, JTAG 接口有几个信号(JTDI/JTDO/JTRST)被 GBC_LED(ATK MODULE)/ FIFO_WEN (摄像头模块)/ FIFO_RCLK(摄像头模块)占用了,所以在调试这些模块的时候, 请大家选择 SWD 模式,其实最好就是一直用 SWD 模式。 4, 当你想使用某个 IO 口用作其他用处的时候,请先看看开发板的原理图,该 IO 口是否 有连接在开发板的某个外设上,如果有,该外设的这个信号是否会对你的使用造成干 扰,先确定无干扰,再使用这个 IO。比如 PB8 就不怎么适合再用做其他输出,因为他 接了蜂鸣器,如果你输出高电平就会听到蜂鸣器的叫声了。 5, 开发板上的跳线帽比较多,大家在使用某个功能的时候,要先查查这个是否需要设置 跳线帽,以免浪费时间。 6, 当液晶显示白屏的时候,请先检查液晶模块是否插好(拔下来重新插试试),如果还不 行,可以通过串口看看 LCD ID 是否正常,再做进一步的分析。 至此,本手册的实验平台(ALIENTEK 战舰 STM32F103)的硬件部分就介绍完了,了 解了整个硬件对我们后面的学习会有很大帮助,有助于理解后面的代码,在编写软件的时 候,可以事半功倍,希望大家细读!另外 ALIENTEK 开发板的其他资料及教程更新,都可 以在技术论坛 www.openedv.com 下载到,大家可以经常去这个论坛获取更新的信息。 2.3 STM32F103 学习方法 STM32 作为目前最热门的 ARM Cortex M3 处理器,正在被越来越多的公司选择使用。学 习 STM32 的朋友也越来越多,初学者,可能会认为 STM32 很难学,以前只学过 51,或者甚至 连 51 都没学过的,一看到 STM32 那么多寄存器,就懵了。其实,万事开头难,只要掌握了方 法,学好 STM32,还是非常简单的,这里我们总结学习 STM32 的几个要点: 1,一款实用的开发板。 这个是实验的基础,有时候软件仿真通过了,在板上并不一定能跑起来,而且有个开发板 在手,什么东西都可以直观的看到,效果不是仿真能比的。但开发板不宜多,多了的话连自己 54 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 都不知道该学哪个了,觉得这个也还可以,那个也不错,那就这个学半天,那个学半天,结果 学个四不像。倒不如从一而终,学完一个在学另外一个。 2,两本参考资料,即《STM32 中文参考手册》和《Cortex-M3 权威指南》。 《STM32 中文参考手册》是 ST 出的官方资料,有 STM32 的详细介绍,包括了 STM32 的 各种寄存器定义以及功能等,是学习 STM32 的必备资料之一。而《Cortex-M3 权威指南》则是 对《STM32 中文参考手册》的补充,后者一般认为使用 STM32 的人都对 CM3 有了较深的了解, 所以 Cortex-M3 的很多东西它只是一笔带过,但前者对 Cortex-M3 有非常详细的说明,这样两 者搭配,你就基本上任何问题都能得到解决了。 3,掌握方法,勤学慎思。 STM32 不是妖魔鬼怪,不要畏难,STM32 的学习和普通单片机一样,基本方法就是: a) 掌握时钟树图(见《STM32 中文参考手册_V10 版》图 8)。 任何单片机,必定是靠时钟驱动的,时钟就是单片机的动力,STM32 也不例外,通过时钟 树,我们可以知道,各种外设的时钟是怎么来的?有什么限制?从而理清思路,方便理解。 b) 多思考,多动手。 所谓熟能生巧,先要熟,才能巧。如何熟悉?这就要靠大家自己动手,多多练习了,光看/ 说,是没什么太多用的,很多人问我,STM32 这么多寄存器,如何记得啊?回答是:不需要全 部记住。我至今也就只记得 STM32 的 IO 口控制这几个寄存器,因为有规律可循,好记。其他 的一概不记得。学习 STM32,不是应试教育,不需要考试,不需要你倒背如流。你只需要知道 这些寄存器,在哪个地方,用到的时候,可以迅速查找到,就可以了。完全是可以翻书,可以 查资料的,可以抄袭的,不需要死记硬背。掌握学习的方法,远比掌握学习的内容重要的多。 熟悉了之后,就应该进一步思考,也就是所谓的巧了。我们提供了几十个例程,供大家学 习,跟着例程走,无非就是熟悉 STM32 的过程,只有进一步思考,才能更好的掌握 STM32, 也即所谓的举一反三。例程是死的,人是活的,所以,可以在例程的基础上,自由发挥,实现 更多的其他功能,并总结规律,为以后的学习/使用打下坚实的基础,如此,方能信手拈来。 所以,学习一定要自己动手,光看视频,光看文档,是不行的。举个简单的例子,你看视 频,教你如何煮饭,几分钟估计你就觉得学会了。实际上你可以自己测试下,是否真能煮好? 机会总是留给有准备的人,只有平时多做准备,才可能抓住机会。 只要以上三点做好了,学习 STM32 基本上就不会有什么太大问题了。如果遇到问题,可 以在我们的技术论坛:开源电子网:www.openedv.com 提问,论坛 STM32 板块已经有 3.7W 多 个主题,很多疑问已经有网友提过了,所以可以在论坛先搜索一下,很多时候,就可以直接找 到答案了。论坛是一个分享交流的好地方,是一个可以让大家互相学习,互相提高的平台,所 以有时间,可以多上去看看。 另外,很多 ST 官方发布的所有资料(芯片文档、用户手册、应用笔记、固件库、勘误手册 等),大家都可以在 www.stmcu.org 这个地方下载到。也可以经常关注下,ST 会将最新的资料 放到这个网站。 最后,大家如果有兴趣,还可以关注我们的官方微信公众平台“正点原子”,我们会及时把 我们的最新资料发布到平台上供大家下载。 55 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第二篇 软件篇 上一篇,我们介绍了本指南的实验平台,本篇我们将详细介绍 STM32 的开发软件:MDK5。 通过该篇的学习,你将了解到:1、如何在 MDK5 下新建 STM32 工程;2、工程的编译;3、 MDK5 的一些使用技巧;4、软件仿真;5、程序下载;6、在线调试;以上几个环节概括了了 一个完整的 STM32 开发流程。本篇将图文并茂的向大家介绍以上几个方面,通过本篇的学习, 希望大家能掌握 STM32 的开发流程,并能独立开始 STM32 的编程和学习。 在学习 STM32 库函数之前,请大家准备如下资料,这些资料在我们光盘中都有: 1. STM32 固件库 V3.5 文件包:STM32F10x_StdPeriph_Lib_V3.5.0 光盘目录:软件资料\STM32 固件库使用参考资料\ 2. STM32F10x_StdPeriph_Driver_3.5.0(中文版).chm:中文版的固件库使用手册。 光盘目录:软件资料\STM32 固件库使用参考资料\ 3. 《STM32 中文参考手册 V10》:这个资料很重要,很多概念都在这个资料中。 光盘目录:STM32 参考资料/ STM32 中文参考手册_V10.pdf 4. 《Cortex-M3 权威指南 Cn》这个讲解了很多 CM3 底层的东西。 光盘目录:STM32 参考资料/Cortex-M3 权威指南(中文).pdf 5. MDK5.13 软件:这是编程编译软件。 光盘目录:软件资料\软件\MDK5 本篇将分为如下 3 个章节: 2.1,MDK5 软件入门; 2.2,STM32 开发基础知识入门; 2.3,SYSTEM 文件介绍; 56 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第三章 MDK5 软件入门 本章将向大家介绍 MDK5 软件的使用,ST 官方固件库介绍,同时还介绍怎样建一个基于 STM32 官方固件库 V3.5 的工程模板。通过本章的学习,我们最终将建立一个自己的 MDK5 工 程,最后本章还将向大家介绍 MDK5 软件的一些使用技巧,希望大家在本章之后,能够对 MDK5 这个软件有个比较全面的了解。 本章分为如下个小结: 3.1,STM32 官方固件库简介; 3.2,MDK5 简介; 3.3,新建基于固件库的 MDK5 工程模板; 3.4,程序下载与调试; 3.5,MDK 使用技巧; 3.1 STM32 官方固件库简介 ST(意法半导体)为了方便用户开发程序,提供了一套丰富的 STM32 固件库。到底什么是固 件库?它与直接操作寄存器开发有什么区别和联系?很多初学用户很是费解,这一节,我们将 讲解 STM32 固件库相关的基础知识,希望能够让大家对 STM32 固件库有一个初步的了解,至 于固件库的详细使用方法,我们会在后面的章节一一介绍。这章节有一些图片是截图的权威手 册。这一节的知识可以参考《STM32 固件库使用手册中文翻译版》P32,固件库手册讲解更加 详细,这里只是提到一下,希望大家谅解。 官方包的地址:软件资料\STM32 固件库使用参考资料\ STM32F10x_StdPeriph_Lib_V3.5.0 3.1.1 库开发与寄存器开发的关系 很多用户都是从学 51 单片机开发转而想进一步学习 STM32 开发,他们习惯了 51 单片机 的寄存器开发方式,突然一个 ST 官方库摆在面前会一头雾水,不知道从何下手。下面我们将 通过一个简单的例子来告诉 STM32 固件库到底是什么,和寄存器开发有什么关系?其实一句 话就可以概括:固件库就是函数的集合,固件库函数的作用是向下负责与寄存器直接打交道, 向上提供用户函数调用的接口(API)。 在 51 的开发中我们常常的作法是直接操作寄存器,比如要控制某些 IO 口的状态,我们直 接操作寄存器: P0=0x11; 而在 STM32 的开发中,我们同样可以操作寄存器: GPIOx->BRR = 0x0011; 这种方法当然可以,但是这种方法的劣势是你需要去掌握每个寄存器的用法,你才能正确使用 STM32,而对于 STM32 这种级别的 MCU,数百个寄存器记起来又是谈何容易。于是 ST(意法 半导体)推出了官方固件库,固件库将这些寄存器底层操作都封装起来,提供一整套接口(API) 供开发者调用,大多数场合下,你不需要去知道操作的是哪个寄存器,你只需要知道调用哪些 函数即可。 比如上面的控制 BRR 寄存器实现电平控制,官方库封装了一个函数: void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) { GPIOx->BRR = GPIO_Pin; 57 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 } 这个时候你不需要再直接去操作 BRR 寄存器了,你只需要知道怎么使用 GPIO_ResetBits()这个 函数就可以了。在你对外设的工作原理有一定的了解之后,你再去看固件库函数,基本上函数 名字能告诉你这个函数的功能是什么,该怎么使用,这样是不是开发会方便很多? 任何处理器,不管它有多么的高级,归根结底都是要对处理器的寄存器进行操作。但是固 件库不是万能的,您如果想要把 STM32 学透,光读 STM32 固件库是远远不够的。你还是要了 解一下 STM32 的原理,而这些原理了解了,你在进行固件库开发过程中才可能得心应手游刃 有余。 3.1.2 STM32 固件库与 CMSIS 标准讲解 前一节我们讲到,STM32 固件库就是函数的集合,那么对这些函数有什么要求呢??这里 就涉及到一个 CMSIS 标准的基础知识,这部分知识可以从《Cortex-M3 权威指南》中了解到, 我们这里只是对权威指南的讲解做个概括性的介绍。经常有人问到 STM32 和 ARM 以及 ARM7 是什么关系这样的问题,其实 ARM 是一个做芯片标准的公司,它负责的是芯片内核的架构设 计,而 TI,ST 这样的公司,他们并不做标准,他们是芯片公司,他们是根据 ARM 公司提供的 芯片内核标准设计自己的芯片。所以,任何一个做 Cortex-M3 芯片,他们的内核结构都是一样 的,不同的是他们的存储器容量,片上外设,IO 以及其他模块的区别。所以你会发现,不同公 司设计的 Cortex-M3 芯片他们的端口数量,串口数量,控制方法这些都是有区别的,这些资源 他们可以根据自己的需求理念来设计。同一家公司设计的多种 Cortex-m3 内核芯片的片上外设 也会有很大的区别,比如 STM32F103RBT 和 STM32F103ZET,他们的片上外设就有很大的区 别。我们可以通过《Cortex-M3 权威指南》中的一个图来了解一下: 图 3.1.2.1 Cortex-m3 芯片结构 从上图可以看出,芯片虽然是芯片公司设计,但是内核却要服从 ARM 公司提出的 Cortex-M3 内核标准了,理所当然,芯片公司每卖出一片芯片,需要向 ARM 公司交一定的专利费。 既然大家都使用的是 Cortex-M3 核,也就是说,本质上大家都是一样的,这样 ARM 公司 为了能让不同的芯片公司生产的 Cortex-M3 芯片能在软件上基本兼容,和芯片生产商共同提出 58 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 了一套标准 CMSIS 标准(Cortex Microcontroller Software Interface Standard) ,翻译过来是 “ARM Cortex™ 微控制器软件接口标准”。ST 官方库就是根据这套标准设计的。这里我们又 要引用参考资料里面的图片来看看基于 CMSIS 应用程序基本结构: 图 3.1.2.2 基于 CMSIS 应用程序基本结构 CMSIS 分为 3 个基本功能层: 1) 核内外设访问层:ARM 公司提供的访问,定义处理器内部寄存器地址以及功能函数。 2) 中间件访问层:定义访问中间件的通用 API,也是 ARM 公司提供。 3) 外设访问层:定义硬件寄存器的地址以及外设的访问函数。 从图中可以看出,CMSIS 层在整个系统中是处于中间层,向下负责与内核和各个外设直接打交 道,向上提供实时操作系统用户程序调用的函数接口。如果没有 CMSIS 标准,那么各个芯片公 司就会设计自己喜欢的风格的库函数,而 CMSIS 标准就是要强制规定,芯片生产公司设计的库 函数必须按照 CMSIS 这套规范来设计。 其实不用这么讲这么复杂的,一个简单的例子,我们在使用 STM32 芯片的时候首先要进 行系统初始化,CMSIS 规范就规定,系统初始化函数名字必须为 SystemInit,所以各个芯片公 司写自己的库函数的时候就必须用 SystemInit 对系统进行初始化。CMSIS 还对各个外设驱 动文件的文件名字规范化,以及函数名字规范化等等一系列规定。上一节讲的函数 GPIO_ResetBits 这个函数名字也是不能随便定义的,是要遵循 CMSIS 规范的。 至于 CMSIS 的具体内容就不必多讲了,需要了解详细的朋友可以到网上搜索资料,相 关资料可谓满天飞。 3.1.3 STM32 官方库包介绍 这一节内容主要讲解 ST 官方提供的 STM32 固件库包的结构。ST 官方提供的固件库完整包 可以在官方下载,我们光盘也会提供。固件库是不断完善升级的,所以有不同的版本,我们使 用的是 V3.5 版本的固件库 大家可以到光盘目录: 软件资料\STM32 固件库使用参考资料\ 59 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 STM32F10x_StdPeriph_Lib_V3.5.0 下面查看,这在我们论坛有下载。下面看看官方库包的目录 结构: 图 3.1.2.3 官方库包根目录 图 3.1.2.4 官方库目录列表 3.1.3.1 文件夹介绍: Libraries 文件夹下面有 CMSIS 和 STM32F10x_StdPeriph_Driver 两个目录,这两个目录包 含 固 件 库 核 心 的 所 有 子 文 件 夹 和 文 件 。 其 中 CMSIS 目 录 下 面 是 启 动 文 件 , STM32F10x_StdPeriph_Driver 放的是 STM32 固件库源码文件。源文件目录下面的 inc 目录存放 的是 stm32f10x_xxx.h 头文件,无需改动。src 目录下面放的是 stm32f10x_xxx.c 格式的固件库源 码文件。每一个.c 文件和一个相应的.h 文件对应。这里的文件也是固件库的核心文件,每个外 设对应一组文件。 Libraries 文件夹里面的文件在我们建立工程的时候都会使用到。 Project 文件夹下面有两个文件夹。顾名思义,STM32F10x_StdPeriph_Examples 文件夹下面 存放的 ST 官方提供的固件实例源码,在以后的开发过程中,可以参考修改这个官方提供的实 例来快速驱动自己的外设,很多开发板的实例都参考了官方提供的例程源码,这些源码对以后 的学习非常重要。STM32F10x_StdPeriph_Template 文件夹下面存放的是工程模板。 Utilities 文件下就是官方评估板的一些对应源码,这个可以忽略不看。 根目录中还有一个 stm32f10x_stdperiph_lib_um.chm 文件,直接打开可以知道,这是一个固 件库的帮助文档,这个文档非常有用,只可惜是英文的,在开发过程中,这个文档会经常被使 用到。 60 3.1.3.2 关键文件介绍: STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 下面我们要着重介绍 Libraries 目录下面几个重要的文件。 core_cm3.c 和 core_cm3.h 文件位于\Libraries\CMSIS\CM3\CoreSupport 目录下面的,这个就 是 CMSIS 核心文件,提供进入 M3 内核接口,这是 ARM 公司提供,对所有 CM3 内核的芯片 都一样。你永远都不需要修改这个文件,所以这里我们就点到为止。 和 CoreSupport 同一级还有一个 DeviceSupport 文件夹。DeviceSupport\ST\STM32F10xt 文件 夹下面主要存放一些启动文件以及比较基础的寄存器定义以及中断向量定义的文件。 图 3.1.2.5 DeviceSupport\ST\STM32F10x 目录结构 这个目录下面有三个文件:system_stm32f10x.c, system_stm32f10x.h 以及 stm32f10x.h 文件。其 中 system_stm32f10x.c 和对应的头文件 system_stm32f10x.h 文件的功能是设置系统以及总线时 钟,这个里面有一个非常重要的 SystemInit()函数,这个函数在我们系统启动的时候都会调用, 用来设置系统的整个时钟系统。 stm32f10x.h 这个文件就相当重要了,只要你做 STM32 开发,你几乎时刻都要查看这个文 件相关的定义。这个文件打开可以看到,里面非常多的结构体以及宏定义。这个文件里面主要 是系统寄存器定义申明以及包装内存操作,对于这里是怎样申明以及怎样将内存操作封装起来 的,我们在后面的章节“MDK 中寄存器地址名称映射分析”中会讲到。 在 DeviceSupport\ST\STM32F10x 同一级还有一个 startup 文件夹,这个文件夹里面放的文 件顾名思义是启动文件。在\startup\arm 目录下,我们可以看到 8 个 startup 开头的.s 文件。 图 3.1.2.6 startup 文件 这里之所以有 8 个启动文件,是因为对于不同容量的芯片启动文件不一样。对于 103 系列,主 要是用其中 3 个启动文件: startup_stm32f10x_ld.s: 适用于小容量 产品 startup_stm32f10x_md.s : 适用于中等容量产品 startup_stm32f10x_hd.s: 适用于大容量产品 61 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 这里的容量是指 FLASH 的大小.判断方法如下: 小容量:FLASH≤32K 中容量:64K≤FLASH≤128K 大容量:256K≤FLASH 我们 ALIENTEK STM32 战舰板,精英板以及 mini 板采用的 STM32F103ZET6 和 stm32F103RCT6 芯片都属于大容量产品,所以我们的启动文件选择 startup_stm32f10x_hd.s,对于中等容量芯片请 选择 startup_stm32f10x_md.s 启动文件,小容量芯片请选择 startup_stm32f10x_ld.s。 启动文件到底什么作用,其实我们可以打开启动文件进去看看。启动文件主要是进行堆栈 之类的初始化,中断向量表以及中断函数定义。启动文件要引导进入 main 函数。Reset_Handler 中断函数是唯一实现了的中断处理函数,其他的中断函数基本都是死循环。 Reset_handler 在我 们系统启动的时候会调用,下面让我们看看 Reset_handler 这段代码: ; Reset handler Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT __main IMPORT SystemInit LDR R0, =SystemInit BLX R0 LDR R0, =__main BX R0 ENDP 这段代码我也看不懂,反正就知道,这里面要引导进入 main 函数,同时在进入 main 函数之前, 首先要调用 SystemInit 系统初始化函数。 还有其他几个文件 stm32f10x_it.c,stm32f10x_it.h 以及 stm32f10x_conf.h 等文件,这里就不 一一介绍。stm32f10x_it.c 里面是用来编写中断服务函数,中断服务函数也可以随意编写在工程 里面的任意一个文件里面,个人觉得这个文件没太大意义。 stm32f10x_conf.h 文件打开可以看到一堆的#include,这里你建立工程的时候,可以注释掉一 些你不用的外设头文件。这里相信大家一看就明白。 这一节我们就简要介绍到这里,后面我们会介绍怎样建立基于 V3.5 版本固件库的工程模 块。 3.2 MDK5 简介 MDK 源自德国的 KEIL 公司,是 RealView MDK 的简称。在全球 MDK 被超过 10 万的嵌 入式开发工程师使用。目前最新版本为:MDK5.14,该版本使用 uVision5 IDE 集成开发环境, 是目前针对 ARM 处理器,尤其是 Cortex M 内核处理器的最佳开发工具。 MDK5 向后兼容 MDK4 和 MDK3 等,以前的项目同样可以在 MDK5 上进行开发(但是头文 件方面得全部自己添加), MDK5 同时加强了针对 Cortex-M 微控制器开发的支持,并且对传统 的开发模式和界面进行升级,MDK5 由两个部分组成:MDK Core 和 Software Packs。其中, Software Packs 可以独立于工具链进行新芯片支持和中间库的升级。如图 3.1.1 所示: 62 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.2.1 MDK5 组成 从上图可以看出,MDK Core 又分成四个部分:uVision IDE with Editor(编辑器),ARM C/C++ Compiler(编译器),Pack Installer(包安装器),uVision Debugger with Trace(调试跟踪 器)。uVision IDE 从 MDK4.7 版本开始就加入了代码提示功能和语法动态检测等实用功能,相 对于以往的 IDE 改进很大。 Software Packs(包安装器)又分为:Device(芯片支持),CMSIS(ARM Cortex 微控制器 软件接口标准)和 Mdidleware(中间库)三个小部分,通过包安装器,我们可以安装最新的组 件,从而支持新的器件、提供新的设备驱动库以及最新例程等,加速产品开发进度。 同以往的 MDK 不同,以往的 MDK 把所有组件到包含到了一个安装包里面,显得十分“笨 重”,MDK5 则不一样,MDK Core 是一个独立的安装包,它并不包含器件支持和设备驱动等组 件,但是一般都会包括 CMSIS 组件,大小 350M 左右,相对于 MDK4.70A 的 500 多 M,瘦身 不少,MDK5 安装包可以在:http://www.keil.com/demo/eval/arm.htm 下载到。而器件支持、设 备驱动、CMSIS 等组件,则可以点击 MDK5 的 Build Toolbar 的最后一个图标调出 Pack Installer, 来进行各种组件的安装。也可以在 http://www.keil.com/dd2/pack 这个地址下载,然后进行安装。 在 MDK5 安装完成后,要让 MDK5 支持 STM32F103 的开发,我们还需要安装 STM32F1 的器件支持包:Keil.STM32F1xx_DFP.1.0.5.pack(STM32F1 的器件包)。这个包以及 MDK5.14 安装软件,我们都已经在开发板光盘提供了,大家自行安装即可。 3.3 新建基于固件库的 MDK5 工程模板 在前面的章节我们介绍了 STM32 官方库包的一些知识,这些我们将着重讲解建立基于固 件库的工程模板的详细步骤。在此之前,首先我们要准备如下资料: a) V3.5 固件库包:STM32F10x_StdPeriph_Lib_V3.5.0 这是 ST 官网下载的固件库完 整版,我们光盘目录: 软件资料\STM32 固件库使用参考资料\STM32F10x_StdPeriph_Lib_V3.5.0 我们官方论坛下载地址:http://openedv.com/posts/list/6054.htm b) MDK5 开发环境(我们的板子的开发环境目前是使用这个版本)。这在我们光盘 的软件目录下面有安装包:软件资料\软件\MDK5 在建立工程模板之前,大家首先要安装 MDK5 开发环境。对于 MDK5 的详细安装,请参 考光盘的安装文档:“\1,ALIENTEK 战舰 STM32 开发板入门资料\MDK5.14 安装手册.pdf”。 63 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 这里顺便提醒大家,本小节新建的工程模板在我们光盘目录下面有存放,路径为:“\4,程 序源码\2,标准例程-V3.5 库函数版本\实验 0-1 Template 工程模板-新建工程章节使用”。大家 在新建工程过程中有任何疑问,都可以跟这个模板进行比较,找出问题所在。接下来我们将手 把手的教您新建一个基于 V3.5 版本固件库的 STM32F1 工程模板。步骤如下: 1) 在建立工程之前,我们建议用户在电脑的某个目录下面建立一个文件夹,后面所建立的工 程都可以放在这个文件夹下面,这里我们建立一个文件夹为 Template。 2) 点击 MDK 的菜单:Project –>New Uvision Project ,然后将目录定位到刚才建立的文件夹 Template 之下,在这个目录下面建立子文件夹 USER(我们的代码工程文件都是放在 USER 目录,很多人喜欢新建“Project”目录放在下面,这也是可以的,这个就看个人喜好了), 然后定位到 USER 目录下面,我们的工程文件就都保存到 USER 文件夹下面。工程命名为 Template,点击保存。 图 3.3.1 新建工程 64 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.3.2 定义工程名称 接下来会出现一个选择 CPU 的界面,就是选择我们的芯片型号。如图 3.3.3 所示,因为 ALIENTEK 战舰 STM32F103 所使用的 STM32 型号为 STM32F103ZET6,所以在这里我们选择 STMicroelectronicsSTM32F1 SeriesSTM32F103STM32F103ZET6(如果使用的是其他系列 的芯片,选择相应的型号就可以了,特别注意:一定要安装对应的器件 pack 才会显示这些内 容哦!!,如果没得选择,请关闭 MDK,然后安装 光盘:6,软件资料\1,软件\MDK5\ Keil.STM32F1xx_DFP.1.0.5.pack 这个安装包)。 65 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.3.3 选择芯片型号 3) 点击 OK,MDK 会弹出 Manage Run-Time Environment 对话框,如图 3.3.4 所示: 图 3.3.4 Manage Run-Time Environment 界面 这是 MDK5 新增的一个功能,在这个界面,我们可以添加自己需要的组件,从而方便构建 开发环境,不过这里我们不做介绍。所以在图 3.3.4 所示界面,我们直接点击 Cancel,即可, 得到如图 3.3.5 所示界面: 66 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.3.5 工程初步建立 到这里,我们还只是建了一个框架,还需要添加启动代码,以及.c 文件等。 4) 现在我们看看 USER 目录下面包含 2 个文件夹和 2 个文件,如下图 3.3.6 所示: 图 3.3.6 工程 USER 目录文件 这里我们说明一下, Template.uvprojx 是工程文件,非常关键,不能轻易删除。Listings 和 Objects 文件夹是 MDK 自动生成的文件夹,用于存放编译过程产生的中间文件。这里,我们把两个文 件夹删除,我们会在下一步骤中新建一个 OBJ 文件夹,用来存放编译中间文件。当然,我们不 删除这两个文件夹也是没有关系的,只是我们不用它而已。 5) 接下来,我们在 Template 工程目录下面,新建 3 个文件夹 CORE, OBJ 以及 STM32F10x_FWLib。CORE 用来存放核心文件和启动文件,OBJ 是用来存放编译过程文件以及 hex 文件,STM32F10x_FWLib 文件夹顾名思义用来存放 ST 官方提供的库函数源码文件。已有 的 USER 目 录 除 了 用 来 放 工 程 文 件 外 , 还 用 来 存 放 主 函 数 文 件 main.c, 以 及 其 他 包 括 system_stm32f10x.c 等等。 67 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.3.3.5 工程目录预览 6) 下面我们要将官方的固件库包里的源码文件复制到我们的工程目录文件夹下面。 打开官方固件库包,定位到我们之前准备好的固件库包的目录 STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\STM32F10x_StdPeriph_Driver 下面, 将目录下面的 src,inc 文件夹 copy 到我们刚才建立的 STM32F10x_FWLib 文件夹下面。 src 存放的是固件库的.c 文件,inc 存放的是对应的.h 文件,您不妨打开这两个文件目录过目一 下里面的文件,每个外设对应一个.c 文件和一个.h 头文件。 图 3.3.3.6 官方库源码文件夹 7) 下面我们要将固件库包里面相关的启动文件复制到我们的工程目录 CORE 之下。 打开官方固件库包,定位到目录 STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\CoreSupport 下面,将文件 core_cm3.c 和 文 件 core_cm3.h 复 制 到 CORE 下 面 去 。 然 后 定 位 到 目 录 STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\startup\a rm 下面,将里面 startup_stm32f10x_hd.s 文件复制到 CORE 下面。这里我们我之前已经解释了 不同容量的芯片使用不同的启动文件,我们的芯片 STM32F103ZET6 是大容量芯片,所以选择 68 STM32F1 开发指南(库函数版) 这个启动文件。 现在看看我们的 CORE 文件夹下面的文件: ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.3.3.7 启动文件夹 8) 定位到目录: STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x 下面 将里面的三个文件 stm32f10x.h,system_stm32f10x.c,system_stm32f10x.h,复制到我们的 USER 目录之下。然后将 STM32F10x_StdPeriph_Lib_V3.5.0\Project\STM32F10x_StdPeriph_Template 下 面 的 4 个 文 件 main.c,stm32f10x_conf.h,stm32f10x_it.c,stm32f10x_it.h 复制到 USER 目录下面。 图 3.3.3.8 USER 目录文件浏览 9) 前面 8 个步骤,我们将需要的固件库相关文件复制到了我们的工程目录下面,下面我们将 这些文件加入我们的工程中去。右键点击 Target1,选择 Manage Components 69 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.3.3.9 点击 Management Components 10) Project Targets 一栏,我们将 Target 名字修改为 Template,然后在 Groups 一栏删掉一个 Source Group1,建立三个 Groups:USER,CORE,FWLIB。然后点击 OK,可以看到我们的 Target 名字以及 Groups 情况。 图 3.3.3.10 70 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.3.3.11 11) 下面我们往 Group 里面添加我们需要的文件。我们按照步骤 10 的方法, 右键点击点击 Tempate,选择选择 Manage Components.然后选择需要添加文件的 Group,这里第一步我们 选 择 FWLIB , 然 后 点 击 右 边 的 Add Files, 定 位 到 我 们 刚 才 建 立 的 目 录 STM32F10x_FWLib/src 下面,将里面所有的文件选中(Ctrl+A),然后点击 Add,然后 Close. 可以看到 Files 列表下面包含我们添加的文件。 这里需要说明一下,对于我们写代码,如果我们只用到了其中的某个外设,我们就可以不 用添加没有用到的外设的库文件。例如我只用 GPIO,我可以只用添加 stm32f10x_gpio.c 而 其他的可以不用添加。这里我们全部添加进来是为了后面方便,不用每次添加,当然这样 的坏处是工程太大,编译起来速度慢,用户可以自行选择。 71 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.3.3.12 12) 用同样的方法,将 Groups 定位到 CORE 和 USER 下面,添加需要的文件。这里 我们的 CORE 下面需要添加的文件为 core_cm3.c,startup_stm32f10x_hd.s (注意,默认添加的 时候文件类型为.c,也就是添加 startup_stm32f10x_hd.s 启动文件的时候,你需要选择文件类型 为 All files 才能看得到这个文件),USER 目录下面需要添加的文件为 main.c,stm32f10x_it.c, system_stm32f10x.c. 这样我们需要添加的文件已经添加到我们的工程中了,最后点击 OK,回到工程主界面。 72 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.3.3.13 图 3.3.3.14 73 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.3.3.15 13) 接下来我们要编译工程,在编译之前我们首先要选择编译中间文件编译后存放目录。 方法是点击魔术棒,然后选择“Output”选项下面的“Select folder for objects…”,然后选 择目录为我们上面新建的 OBJ 目录。这里大家注意,如果我们不设置 Output 路径,那么 默认的编译中间文件存放目录就是 MDK 自动生成的 Objects 目录和 Listings 目录。 74 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.3.3.16 14) 下面我们点击编译按钮 编译工程,可以看到很多报错,因为找不到头文件。 75 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.3.3.17 15) 下面我们要告诉 MDK,在哪些路径之下搜索需要的头文件,也就是头文件目录。这里大 家要注意,对于任何一个工程,我们都需要把工程中引用到的所有头文件的路径都包含到 进来。回到工程主菜单,点击魔术棒 ,出来一个菜单,然后点击 c/c++选项.然后点击 Include Paths 右边的按钮。弹出一个添加 path 的对话框,然后我们将图上面的 3 个目录添 加进去。记住,keil 只会在一级目录查找,所以如果你的目录下面还有子目录,记得 path 一定要定位到最后一级子目录。然后点击 OK. 76 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.3.3.18 77 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.3.3.219 78 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.3.3.20 16) 接下来,我们再来编译工程,可以看到又报了很多同样的错误。为什么呢?这是因为 3.5 版 本的库函数在配置和选择外设的时候通过宏定义来选择的,所以我们需要配置一个全局的 宏定义变量。按照步骤 16,定位到 c/c++界面,然后填写 “STM32F10X_HD,USE_STDPERIPH_DRIVER”到 Define 输入框里面。 这里解释一下,如果你用的是中容量那么 STM32F10X_HD 修改为 STM32F10X_MD,小容 量修改为 STM32F10X_LD. 然后点击 OK。 79 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.3.3.21 17) 这次在编译之前,我们记得打开工程 USER 下面的 main.c,复制下面代码到 main.c 覆盖已 有代码,然后进行编译。(记得在代码的最后面加上一个回车,否则会有警告),可以看到, 这次编译已经成功了。 #include "stm32f10x.h" void Delay(u32 count) { u32 i=0; for(;iPB.5 端口配置 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //IO 口速度为 50MHz GPIO_Init(GPIOB, &GPIO_InitStructure); //初始化 GPIOB.5 GPIO_SetBits(GPIOB,GPIO_Pin_5); //PB.5 输出高 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //LED1-->PE.5 推挽输出 GPIO_Init(GPIOE, &GPIO_InitStructure); //初始化 GPIO GPIO_SetBits(GPIOE,GPIO_Pin_5); //PE.5 输出高 80 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 while(1) { GPIO_ResetBits(GPIOB,GPIO_Pin_5); GPIO_SetBits(GPIOE,GPIO_Pin_5); Delay(3000000); GPIO_SetBits(GPIOB,GPIO_Pin_5); GPIO_ResetBits(GPIOE,GPIO_Pin_5); Delay(3000000); } } 这里大家注意,上面 main.c 文件的代码,大家可以打开光盘目录的工程模板,从工程的 main.c 文件中复制过来即可。具体工程为“实验 0-1 Template 工程模板-新建工程章节使用”。 图 3.3.3.22 18) 这样一个工程模版建立完毕。下面还需要配置,让编译之后能够生成 hex 文件。同样点击 魔术棒,进入配置菜单,选择 Output。然后勾上下三个选项。 其中 Create HEX file 是编 译生成 hex 文件,Browser Information 是可以查看变量和函数定义。 81 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.3.3.23 19) 重新编译代码,可以看到生成了 hex 文件在 OBJ 目录下面,这个文件我们用 flymcu 下载 到 mcu 即可(参考 3.4.2 小节)。到这里,一个基于固件库 V3.5 的工程模板就建立了。 20) 实际上经过前面 19 个步骤,我们的工程模板已经建立完成。但是在我们 ALIENTEK 提供 的实验中,每个实验都有一个 SYSTEM 文件夹,下面有 3 个子目录分别为 sys,usart,delay, 存放的是我们每个实验都要使用到的共用代码,该代码是由我们 ALIENTEK 编写,该代码 的原理在我们第五章会有详细的讲解,我们这里只是引入到工程中,方便后面的实验建立 工程。 首先,找到我们实验光盘,打开任何一个固件库的实验,可以看到下面有一个 SYSTEM 文 件夹,比如我们打开实验 1 的工程目录如下: 82 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.3.3.24 可以看到有一个 SYSTEM 文件夹,进入 SYSTEM 文件夹,里面有三个子文件夹分别为 delay,sys,usart,每个子文件夹下面都有相应的.c 文件和.h 文件。我们接下来要将这三个目录下 面的代码加入到我们工程中去。 用我们之前讲解步骤 13 的办法,在工程中新建一个组,命名为 SYSTEM,然后加入这三 个文件夹下面的.c 文件分别为 sys.c,delay.c,usart.c,如下图: 图 3.3.3.25 然后点击“OK”之后可以看到工程中多了一个 SYSTEM 组,下面有 3 个.c 文件。 83 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.3.3.26 接下来我们将对应的三个目录(sys,usart,delay)加入到 PATH 中去,因为每个目录下面都有相 应的.h 头文件,这请参考步骤 15 即可,加入后的截图是: 84 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.3.3.27 最后点击 OK。这样我们的工程模板就彻底完成了,这样我们就可以调用 ALIENTEK 提供的 SYSTEM 文件夹里面的函数。我们建立好的工程模板在我们光盘的实验目录里面有,名字为“实 验 0-1 Template 工程模板-新建工程章节使用”大家可以打开对照一下。 3.4 程序下载与调试 上一节,我们学会了如何在 MDK 下创建 STM32 工程。本节,我们将向读者介绍 STM32 的代码下载以及调试。这里的调试包括了软件仿真和硬件调试(在线调试)。通过本章的学习, 你将了解到:1、STM32 的串口程序下载;2、STM32 在 MDK 下的软件仿真;3、利用 JLINK 对 STM32 进行下载和在线调试。 3.4.1 STM32 软件仿真 MDK 的一个强大的功能就是提供软件仿真,通过软件仿真,我们可以发现很多将要出现 的问题,避免了下载到 STM32 里面来查这些错误,这样最大的好处是能很方便的检查程序存 在的问题,因为在 MDK 的仿真下面,你可以查看很多硬件相关的寄存器,通过观察这些寄存 器,你可以知道代码是不是真正有效。另外一个优点是不必频繁的刷机,从而延长了 STM32 的 FLASH 寿命(STM32 的 FLASH 寿命≥1W 次)。当然,软件仿真不是万能的,很多问题还 是要到在线调试才能发现。废话不多说了,接下来我们开始进行软件仿真。 上一章,我们创立了一个工程模板,本节我们将教大家如何在 MDK5 的软件环境下仿真这 个工程,以验证我们代码的正确性。首先工程模板中 main.c 中代码修如下: #include "delay.h" 85 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 #include "usart.h" int main(void) { u8 t=0; delay_init(); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); uart_init(115200); while(1) { printf("t:%d\n",t); delay_ms(500); t++; } } 注意:上面这段代码大家可以打开光盘的工程:“实验 0-2 Template 工程模板-调试章节使 用”,从 main.c 文件中复制过来即可。 在开始软件仿真之前,先检查一下配置是不是正确,在 IDE 里面点击 ,确定 Target 选 项卡内容如图 3.4.1.1 所示(主要检查芯片型号和晶振频率,其他的一般默认就可以): 图 3.4.1.1 Target 选项卡 确认了芯片以及外部晶振频率(8.0Mhz)之后,基本上就确定了 MDK5.14 软件仿真的硬 件环境了,接下来,我们再点击 Debug 选项卡,设置为如图 3.4.1.2 所示: 86 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.4.1.2 Debug 选项卡 在图 3.4.1.2 中,选择:Use Simulator,即使用软件仿真。选择:Run to main(),即跳过汇 编代码,直接跳转到 main 函数开始仿真。设置下方的:Dialog DLL 分别为:DARMSTM.DLL 和 TARMSTM.DLL,Parameter 均为:-pSTM32F103ZE,用于设置支持 STM32F103ZE 的软硬 件仿真(即可以通过 Peripherals 选择对应外设的对话框观察仿真结果)。最后点击 OK,完成设 置。 接下来,我们点击 (开始/停止仿真按钮),开始仿真,出现如图 3.4.1.3 所示界面: 87 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.4.1.3 开始仿真 可以发现,多出了一个工具条,这就是 Debug 工具条,这个工具条在我们仿真的时候是非 常有用的,下面简单介绍一下 Debug 工具条相关按钮的功能。Debug 工具条部分按钮的功能如 图 3.4.1.4 所示: 图 3.4.1.4 Debug 工具条 复位:其功能等同于硬件上按复位按钮。相当于实现了一次硬复位。按下该按钮之后,代 码会重新从头开始执行。 执行到断点处:该按钮用来快速执行到断点处,有时候你并不需要观看每步是怎么执行的, 而是想快速的执行到程序的某个地方看结果,这个按钮就可以实现这样的功能,前提是你在查 看的地方设置了断点。 挂起:此按钮在程序一直执行的时候会变为有效,通过按该按钮,就可以使程序停止下来, 进入到单步调试状态。 执行进去:该按钮用来实现执行到某个函数里面去的功能,在没有函数的情况下,是等同 于执行过去按钮的。 执行过去:在碰到有函数的地方,通过该按钮就可以单步执行过这个函数,而不进入这个 88 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 函数单步执行。 执行出去:该按钮是在进入了函数单步调试的时候,有时候你可能不必再执行该函数的剩 余部分了,通过该按钮就直接一步执行完函数余下的部分,并跳出函数,回到函数被调用的位 置。 执行到光标处:该按钮可以迅速的使程序运行到光标处,其实是挺像执行到断点处按钮功 能,但是两者是有区别的,断点可以有多个,但是光标所在处只有一个。 汇编窗口:通过该按钮,就可以查看汇编代码,这对分析程序很有用。 观看变量/堆栈窗口:该按钮按下,会弹出一个显示变量的窗口,在里面可以查看各种你想 要看的变量值,也是很常用的一个调试窗口。 串口打印窗口:该按钮按下,会弹出一个类似串口调试助手界面的窗口,用来显示从串口 打印出来的内容。 内存查看窗口:该按钮按下,会弹出一个内存查看窗口,可以在里面输入你要查看的内存 地址,然后观察这一片内存的变化情况。是很常用的一个调试窗口 性能分析窗口:按下该按钮,会弹出一个观看各个函数执行时间和所占百分比的窗口,用 来分析函数的性能是比较有用的。 逻辑分析窗口:按下该按钮会弹出一个逻辑分析窗口,通过 SETUP 按钮新建一些 IO 口, 就可以观察这些 IO 口的电平变化情况,以多种形式显示出来,比较直观。 Debug 工具条上的其他几个按钮用的比较少,我们这里就不介绍了。以上介绍的是比较常 用的,当然也不是每次都用得着这么多,具体看你程序调试的时候有没有必要观看这些东西, 来决定要不要看。 这样,我们在上面的仿真界面里面选内存查看窗口、串口打印窗口。然后调节一下这两个 窗口的位置,如图 3.4.1.5 所示: 89 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.4.1.5 调出仿真串口打印窗口 我们把光标放到 main.c 的 12 行的空白处,然后双击鼠标左键,可以看到在 12 行的左边出 现了一个红框,即表示设置了一个断点(也可以通过鼠标右键弹出菜单来加入),再次双击则取 消)。 然后我们点击 ,执行到该断点处,如图 3.4.1.6 所示: 90 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.4.1.6 执行到断点处 我们现在先不忙着往下执行,点击菜单栏的 Peripherals->USARTs->USART 1。可以看到, 有很多外设可以查看,这里我们查看的是串口 1 的情况。如图 3.4.1.7 所示: 91 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.4.1.7 查看串口 1 相关寄存器 单击 USART1 后会在 IDE 之外出现一个如图 3.4.1.8(a)所示的界面: 图 3.4.1.8 串口 1 各寄存器初始化前后对比 图 3.4.8(a)是 STM32 的串口 1 的默认设置状态,从中可以看到所有与串口相关的寄存器 全部在这上面表示出来了,而且有当前串口的波特率等信息的显示。我们接着单击一下 , 执行完串口初始化函数,得到了如图 3.4.8(b)所示的串口信息。大家可以对比一下这两个图 92 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 的区别,就知道在 uart_init(115200)这个函数里面大概执行了哪些操作。 通过图 3.4.8(b),我们可以查看串口 1 的各个寄存器设置状态,从而判断我们写的代码是 否有问题,只有这里的设置正确了之后,才有可能在硬件上正确的执行。同样这样的方法也可 以适用于很多其他外设,这个读者慢慢体会吧!这一方法不论是在排错还是在编写代码的时候, 都是非常有用的。 然后我们继续单击 按钮,一步步执行,最后就会看到在 USART #1 中打印出相关的信 息,如图 3.4.1.9 所示: 图 3.4.1.9 串口 1 输出信息 图中红色方框内的数据是串口 1 打印出来的,证明我们的仿真是通过的,代码运行时会在 串口 1 不停的输出 t 的值,每 0.5s 执行一次。软件仿真的时间可以在 IDE 的最下面(右下角) 观看到,如图 3.4.1.10 所示。并且 t 自增,与我们预期的一致。再次按下 结束仿真。 图 3.4.1.10 仿真持续时间 至此,我们软件仿真就结束了,通过软件仿真,我们在 MDK5 中验证了代码的正确性,接 下来我们下载代码到硬件上来真正验证一下我们的代码是否在硬件上也是可行的。 3.4.2 STM32 串口程序下载 STM32 的程序下载有多种方法:USB、串口、JTAG、SWD 等,这几种方式,都可以用来 93 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 给 STM32 下载代码。不过,我们最常用的,最经济的,就是通过串口给 STM32 下载代码。本 节,我们将向大家介绍,如何利用串口给 STM32 下载代码。 STM32 的串口下载一般是通过串口 1 下载的,本指南的实验平台 ALIENTEK 战舰 STM32 开发板,不是通过 RS232 串口下载的,而是通过自带的 USB 串口来下载。看起来像是 USB 下 载(只需一根 USB 线,并不需要串口线)的,实际上,是通过 USB 转成串口,然后再下载的。 下面,我们就一步步教大家如何在实验平台上利用 USB 串口来下载代码。 首先要在板子上设置一下,在板子上把 RXD 和 PA9(STM32 的 TXD),TXD 和 PA10(STM32 的 RXD)通过跳线帽连接起来,这样我们就把 CH340G 和 MCU 的串口 1 连接上了。这里由于 ALIENTEK 这款开发板自带了一键下载电路,所以我们并不需要去关心 BOOT0 和 BOOT1 的 状态,但是为了让下下载完后可以按复位执行程序,我们建议大家把 BOOT1 和 BOOT0 都设置 为 0。设置完成如下图所示: 图 3.4.2.1 开发板串口下载跳线设置 这里简单说明一下一键下载电路的原理,我们知道,STM32 串口下载的标准方法是 2 个步 骤: 1, 把 B0 接 V3.3(保持 B1 接 GND)。 2, 按一下复位按键。 通过这两个步骤,我们就可以通过串口下载代码了,下载完成之后,如果没有设置从 0X08000000 开始运行,则代码不会立即运行,此时,你还需要把 B0 接回 GND,然后再按一 次复位,才会开始运行你刚刚下载的代码。所以整个过程,你得跳动 2 次跳线帽,还得按 2 次 复位,比较繁琐。而我们的一键下载电路,则利用串口的 DTR 和 RTS 信号,分别控制 STM32 的复位和 B0,配合上位机软件(flymcu),设置:DTR 的低电平复位,RTS 高电平进 BootLoader, 94 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 这样,B0 和 STM32 的复位,完全可以由下载软件自动控制,从而实现一键下载。 接着我们在 USB_232 处插入 USB 线,并接上电脑,如果之前没有安装 CH340G 的驱动(如 果已经安装过了驱动,则应该能在设备管理器里面看到 USB 串口,如果不能则要先卸载之前的 驱动,卸载完后重启电脑,再重新安装我们提供的驱动),则电脑会提示找到新硬件,如图 3.4.2.2 所示: 图 3.4.2.2 找到新硬件 我们不理会这个提示,直接找到光盘软件资料软件 文件夹下的 CH340 驱动,安装该 驱动,如图 3.4.2.3 所示: 图 3.4.2.3 CH340 驱动安装 在驱动安装成功之后,拔掉 USB 线,然后重新插入电脑,此时电脑就会自动给其安装驱动 了。在安装完成之后,可以在电脑的设备管理器里面找到 USB 串口(如果找不到,则重启下电 脑),如图 3.4.2.4 所示: 图 3.4.2.4 USB 串口 在图 4.2.4 中可以看到,我们的 USB 串口被识别为 COM3,这里需要注意的是:不同电脑 可能不一样,你的可能是 COM4、COM5 等,但是 USB-SERIAL CH340,这个一定是一样的。 95 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 如果没找到 USB 串口,则有可能是你安装有误,或者系统不兼容。 在安装了 USB 串口驱动之后,我们就可以开始串口下载代码了,这里我们的串口下载软件 选择的是 flymcu,该软件是 mcuisp 的升级版本(flymcu 新增对 STM32F4 的支持),由 ALIENTEK 提供部分赞助,mcuisp 作者开发,该软件可以在 www.mcuisp.com 免费下载,本手册的光盘也 附带了这个软件,版本为 V0.188。该软件启动界面如图 3.4.2.5 所示: 图 3.4.2.5 flymcu 启动界面 然后我们选择要下载的 Hex 文件,以前面我们新建的工程为例,因为我们前面在工程建立 的时候,就已经设置了生成 Hex 文件,所以编译的时候已经生成了 Hex 文件,我们只需要找到 这个 Hex 文件下载即可。 用 flymcu 软件打开 OBJ 文件夹,找到 Template.hex,打开并进行相应设置后,如图 3.4.2.6 所示: 96 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.4.2.6 flymcu 设置 图 3.4.2.6 中圈中的设置,是我们建议的设置。编程后执行,这个选项在无一键下载功能的 条件下是很有用的,当选中该选项之后,可以在下载完程序之后自动运行代码。否则,还需要 按复位键,才能开始运行刚刚下载的代码。 编程前重装文件,该选项也比较有用,当选中该选项之后,flymcu 会在每次编程之前,将 hex 文件重新装载一遍,这对于代码调试的时候是比较有用的。特别提醒:不要选择使用 RamIsp, 否则,可能没法正常下载。 最后,我们选择的 DTR 的低电平复位,RTS 高电平进 BootLoader,这个选择项选中,flymcu 就会通过 DTR 和 RTS 信号来控制板载的一键下载功能电路,以实现一键下载功能。如果不选 择,则无法实现一键下载功能。这个是必要的选项(在 BOOT0 接 GND 的条件下)。 在装载了 hex 文件之后,我们要下载代码还需要选择串口,这里 flymcu 有智能串口搜索功 能。每次打开 flymcu 软件,软件会自动去搜索当前电脑上可用的串口,然后选中一个作为默认 的串口(一般是你最后一次关闭时所选择的串口)。也可以通过点击菜单栏的搜索串口,来实 现自动搜索当前可用串口。串口波特率则可以通过 bps 那里设置,对于 STM32,该波特率最大 为 460800。然后,找到 CH340 虚拟的串口,如图 3.4.2.7 所示: 图 3.4.2.7 CH340 虚拟串口 从之前 USB 串口的安装可知,开发板的 USB 串口被识别为 COM3 了(如果你的电脑是被 识别为其他的串口,则选择相应的串口即可),所以我们选择 COM3。选择了相应串口之后, 我们就可以通过按开始编程(P)这个按钮,一键下载代码到 STM32 上,下载成功后如图 3.4.2.8 所示: 97 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.4.2.8 下载完成 图 4.2.8 中,我们圈出了 flymcu 对一键下载电路的控制过程,其实就是控制 DTR 和 RTS 电平的变化,控制 BOOT0 和 RESET,从而实现自动下载。 另外,下载成功后,会有“共写入 xxxxKB,耗时 xxxx 毫秒”的提示,并且从 0X80000000 处开始运行了,我们打开串口调试助手(XCOM V2.0,在光盘6,软件资料软件串口调 试助手里面)选择 COM3(得根据你的实际情况选择),设置波特率为:115200,会发现从 ALIENTEK 战舰 STM32F103 发回来的信息,如图 3.4.2.9 所示: 图 3.4.2.9 程序开始运行了 接收到的数据和我们仿真的是一样的,证明程序没有问题。至此,说明我们下载代码成功 了,并且也从硬件上验证了我们代码的正确性。 98 3.4.3 JTAG/SWD 程序下载和调试 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 上一节,我们介绍了如何通过利用串口给 STM32 下载代码,并在 ALIENTEK 战舰 STM32 开发板上验证了我们程序的正确性。这个代码比较简单,所以不需要硬件调试,我们直接就一 次成功了。可是,如果你的代码工程比较大,难免存在一些 bug,这时,就有必要通过硬件调 试来解决问题了。 串口只能下载代码,并不能实时跟踪调试,而利用调试工具,比如 JLINK、ULINK、STLINK 等就可以实时跟踪程序,从而找到你程序中的 bug,使你的开发事半功倍。这里我们以 JLINK V8 为例,说说如何在线调试 STM32。 JLINK V8 支持 JTAG 和 SWD,同时 STM32 也支持 JTAG 和 SWD。所以,我们有 2 种方 式可以用来调试,JTAG 调试的时候,占用的 IO 线比较多,而 SWD 调试的时候占用的 IO 线很 少,只需要两根即可。 JLINK V8 的驱动安装比较简单,我们在这里就不说了。在安装了 JLINK V8 的驱动之后, 我们接上 JLINK V8,并把 JTAG 口插到 ALIENTEK 战舰 STM32 开发板上,打开之前 3.2 节新 建的工程,点击 ,打开 Options for Target 选项卡,在 Debug 栏选择仿真工具为 J-LINK/ J-TRACE Cortex,如图 3.4.3.1 所示: 图 3.4.3.1 Debug 选项卡设置 上图中我们还勾选了 Run to main(),该选项选中后,只要点击仿真就会直接运行到 main 函 数,如果没选择这个选项,则会先执行 startup_stm32f10x_hd.s 文件的 Reset_Handler,再跳到 main 函数。 然后我们点击 Settings 按钮(注意,如果你的 JLINK 固件比较老,此时可能会提示升级固 件,点击确认升级即可),设置 J-LINK 的一些参数,如图 3.4.3.2 所示: 99 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.4.3.2 J-LINK 模式设置 图 4.3.2 中,我们使用 J-LINK V8 的 SW 模式调试,因为我们 JTAG 需要占用比 SW 模式多 很多的 IO 口,而在 ALIENTEK 战舰 STM32 开发板上这些 IO 口可能被其他外设用到,可能造 成部分外设无法使用。所以,我们建议大家在调试的时候,一定要选择 SW 模式。Max Clock, 可以点击 Auto Clk 来自动设置,图 4.3.2 中我们设置 SWD 的调试速度为 10Mhz,这里,如果你 的 USB 数据线比较差,那么可能会出问题,此时,你可以通过降低这里的速率来试试。 单击 OK,完成此部分设置,接下来我们还需要在 Utilities 选项卡里面设置下载时的目标编 程器,如图 3.4.3.3 所示: 100 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.4.3.3 FLASH 编程器选择 图 3.4.3.3 中,我们直接勾选 Use Debug Driver,即和调试一样,选择 JLINK 来给目标器件 的 FLASH 编程,然后点击 Settings 按钮,进入 FLASH 算法设置,设置如图 3.4.3.4 所示: 图 3.4.3.4 编程设置 这里 MDK5 会根据我们新建工程时选择的目标器件,自动设置 flash 算法。我们使用的是 STM32F103ZET6,FLASH 容量为 512K 字节,所以 Programming Algorithm 里面默认会有 512K 型号的 STM32F10x High-density Flash 算法。另外,如果这里没有 flash 算法,大家可以点击 Add 按钮,在弹出的窗口自行添加即可。最后,选中 Reset and Run 选项,以实现在编程后自动运行, 其他默认设置即可。设置完成之后,如图 3.4.3.4 所示。 在设置完之后,点击 OK,回到 IDE 界面,编译一下工程。如果我们这个时候要进行程序 下载,那么只需要点击图标 即可下载程序到 STM32,非常方便实用,参考图 3.4.3.5: 101 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 接下来我们主要讲解通过 JTAG/SWD 实现程序在线调试的方法。这里,我们只需要点击 图标就可以开始对 STM32 进行仿真(特别注意:开发板上的 B0 和 B1 都要设置到 GND,否则 代码下载后不会自动运行的!),如图 3.4.3.5 所示: 102 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.4.3.5 开始仿真 因为我们之前勾选了 Run to main()选项,所以,程序直接就运行到了 main 函数的入口处, 我们在 uart_init()处设置了一个断点,点击 ,程序将会快速执行到该处。如图 3.4.3.6 所示: 103 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.4.3.6 程序运行到断点处 接下来,我们就可以和 3.4.1 小节详解的软件仿真一样的方法开始操作了,不过这是真正的 在硬件上的运行,而不是软件仿真,其结果更可信。JTAG/SWD 硬件调试就给大家介绍到这里。 3.5 MDK5 使用技巧 通过前面的学习,我们已经了解了如何在 MDK5 里面建立属于自己的工程。下面,我们将 向大家介绍 MDK5 软件的一些使用技巧,这些技巧在代码编辑和编写方面会非常有用,希望大 家好好掌握,最好实际操作一下,加深印象。 3.5.1 文本美化 文本美化,主要是设置一些关键字、注释、数字等的颜色和字体。MDK 提供了我们自定 义字体颜色的功能。我们可以在工具条上点击 (配置对话框)弹出如图 3.5.1.1 所示界面: 104 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.5.1.1 置对话框 在该对话框中,先设置 Encoding 为:Chinese GB2312(Simplified),然后设置 Tab size 为:4。 以更好的支持简体中文(否则,拷贝到其他地方的时候,中文可能是一堆的问号),同时 TAB 间隔设置为 4 个单位。然后,选择:Colors&Fonts 选项卡,在该选项卡内,我们就可以设置自 己的代码的子体和颜色了。由于我们使用的是 C 语言,故在 Window 下面选择:C/C++ Editor Files 在右边就可以看到相应的元素了。如图 3.5.1.2 示: 105 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.5.1.2 Colors&Fonts 选项卡 然后点击各个元素修改为你喜欢的颜色(注意双击,且有时候可能需要设置多次才生效, MDK 的 bug),当然也可以在 Font 栏设置你字体的类型,以及字体的大小等。设置成之后,点 击 OK,就可以在主界面看到你所修改后的结果,例如我修改后的代码显示效果如图 3.5.1.3 示, 代码中的数字全部修改为红色: 106 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.5.1.3 设置完后显示效果 这就比开始的效果好看一些了。字体大小,则可以直接按住:ctrl+鼠标滚轮,进行放大或 者缩小,或者也可以在刚刚的配置界面设置字体大小。 细心的读者可能会发现,上面的代码里面有一个 u8,还是黑色的,这是一个用户自定义的 关键字,为什么不显示蓝色(假定刚刚已经设置了用户自定义关键字颜色为蓝色)呢?这就又 要回到我们刚刚的配置对话框了,单这次我们要选择 User Keywords 选项卡,同样选择:C/C++ Editor Files,在右边的 User Keywords 对话框下面输入你自己定义的关键字,如图 3.5.1.4 示: 图 3.5.1.4 用户自定义关键字 图 3.5.1.5 中我定义了 u8、u16、u32 等 3 个关键字,这样在以后的代码编辑里面只要出现 这三个关键字,肯定就会变成蓝色。点击 OK,再回到主界面,可以看到 u8 变成了蓝色了,如 图 3.5.1.5 示: 图 3.5.1.5 设置完后显示效果 其实这个编辑配置对话框里面,还可以对其他很多功能进行设置,比如动态语法检测等, 我们将 3.5.2 节介绍。 107 3.5.2 语法检测&代码提示 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 MDK4.70 以上的版本,新增了代码提示与动态语法检测功能,使得 MDK 的编辑器越来越 好用了,这里我们简单说一下如何设置,同样,点击 ,打开配置对话框,选择 Text Completion 选项卡,如图 3.5.2.1 所示: 图 3.5.2.1 Text Completion 选项卡设置 Strut/Class Members,用于开启结构体/类成员提示功能。 Function Parameters,用于开启函数参数提示功能。 Symbols after xx characters,用于开启代码提示功能,即在输入多少个字符以后,提示匹配 的内容(比如函数名字、结构体名字、变量名字等),这里默认设置 3 个字符以后,就开始提示。 如图 3.5.2.2 所示: 图 3.5.2.2 代码提示 Dynamic Syntax Checking,则用于开启动态语法检测,比如编写的代码存在语法错误的时 候,会在对应行前面出现 图标,如出现警告,则会出现 图标,将鼠标光标放图标上面,则 会提示产生的错误/警告的原因,如图 3.5.2.3 所示: 108 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3.5.2.3 语法动态检测功能 这几个功能,对我们编写代码很有帮助,可以加快代码编写速度,并且及时发现各种问题。 不过这里要提醒大家,语法动态检测这个功能,有的时候会误报(比如 sys.c 里面,就有很多 误报),大家可以不用理会,只要能编译通过(0 错误,0 警告),这样的语法误报,一般直接 忽略即可。 3.5.3 代码编辑技巧 这里给大家介绍几个我常用的技巧,这些小技巧能给我们的代码编辑带来很大的方便,相 信对你的代码编写一定会有所帮助。 1)TAB 键的妙用 首先要介绍的就是 TAB 键的使用,这个键在很多编译器里面都是用来空位的,每按一下移 空几个位。如果你是经常编写程序的对这个键一定再熟悉不过了。但是 MDK 的 TAB 键和一般 编译器的 TAB 键有不同的地方,和 C++的 TAB 键差不多。MDK 的 TAB 键支持块操作。也就 是可以让一片代码整体右移固定的几个位,也可以通过 SHIFT+TAB 键整体左移固定的几个位。 假设我们前面的串口 1 中断响应函数如图 3. 5.3.1 所示: 109 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3. 5.3.1 头大的代码 图 3. 5.3.1 中这样的代码大家肯定不会喜欢,这还只是短短的 30 来行代码,如果你的代码 有几千行,全部是这个样子,不头大才怪。看到这样的代码我们就可以通过 TAB 键的妙用来快 速修改为比较规范的代码格式。 选中一块然后按 TAB 键,你可以看到整块代码都跟着右移了一定距离,如图 3. 5.3.2 所示: 图 3. 5.3.2 代码整体偏移 接下来我们就是要多选几次,然后多按几次 TAB 键就可以达到迅速使代码规范化的目的, 最终效果如图 3. 5.3.3 所示 110 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3. 5.3.3 修改后的代码 图 3. 5.3.3 中的代码相对于图 3. 5.3.1 中的要好看多了,经过这样的整理之后,整个代码一 下就变得有条理多了,看起来很舒服。 2) 快速定位函数/变量被定义的地方 上一节,我们介绍了 TAB 键的功能,接下来我们介绍一下如何快速查看一个函数或者变量 所定义的地方。 大家在调试代码或编写代码的时候,一定有想看看某个函数是在那个地方定义的,具体里 面的内容是怎么样的,也可能想看看某个变量或数组是在哪个地方定义的等。尤其在调试代码 或者看别人代码的时候,如果编译器没有快速定位的功能的时候,你只能慢慢的自己找,代码 量比较少还好,如果代码量一大,那就郁闷了,有时候要花很久的时间来找这个函数到底在哪 里。型号 MDK 提供了这样的快速定位的功能(顺便说一下 CVAVR 的 2.0 以后的版本也有这个 功能)。只要你把光标放到这个函数/变量(xxx)的上面(xxx 为你想要查看的函数或变量的名 字),然后右键,弹出如图 3.5.3.4 所示的菜单栏 : 图 3.5.3.4 快速定位 111 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 在图 3.5.3.4 中,我们找到 Go to Definition Of‘delay_init’ 这个地方,然后单击左键就可 以快速跳到 delay_init 函数的定义处(注意要先在 Options for Target 的 Output 选项卡里面勾选 Browse Information 选项,再编译,再定位,否则无法定位!)。如图 3.5.3.5 所示: 图 3.5.3.5 定位结果 对于变量,我们也可以按这样的操作快速来定位这个变量被定义的地方,大大缩短了你查 找代码的时间。细心的大家会发现上面还有一个类似的选项,就是 Go to Reference To ‘delay_init’,这个是快速跳到该函数被声明的地方,有时候也会用到,但不如前者使用得多。 很多时候,我们利用 Go to Definition/ Reference 看完函数/变量的定义/申明后,又想返回之 前的代码继续看,此时我们可以通过 IDE 上的 之前的位置,这个按钮非常好用! 按钮(Back to previous position)快速的返回 3) 快速注释与快速消注释 接下来,我们介绍一下快速注释与快速消注释的方法。在调试代码的时候,你可能会想注 释某一片的代码,来看看执行的情况,MDK 提供了这样的快速注释/消注释块代码的功能。也 是通过右键实现的。这个操作比较简单,就是先选中你要注释的代码区,然后右键,选择 Advanced->Comment Selection 就可以了。 以 delay_init 函数为例,比如我要注释掉下图中所选中区域的代码,如图 3. 5.3.6 所示: 112 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3. 5.3.6 选中要注释的区域 我们只要在选中了之后,选择右键,再选择 Advanced->Comment Selection 就可以把这段代 码注释掉了。执行这个操作以后的结果如图 3.5.3.7 所示: 图 3. 5.3.7 注释完毕 这样就快速的注释掉了一片代码,而在某些时候,我们又希望这段注释的代码能快速的取 消注释,MDK 也提供了这个功能。与注释类似,先选中被注释掉的地方,然后通过右键 ->Advanced,不过这里选择的是 Uncomment Selection。 3.5.4 其他小技巧 除了前面介绍的几个比较常用的技巧,这里还介绍几个其他的小技巧,希望能让你的代码 编写如虎添翼。 第一个是快速打开头文件。在将光标放到要打开的引用头文件上,然后右键选择 Open Document“XXX”,就可以快速打开这个文件了(XXX 是你要打开的头文件名字)。如图 3. 5.4.1 所示: 113 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3. 5.4.1 快速打开头文件 第二个小技巧是查找替换功能。这个和 WORD 等很多文档操作的替换功能是差不多的, 在 MDK 里面查找替换的快捷键是“CTRL+H”,只要你按下该按钮就会调出如图 3.5.4.2 所示界 面: 图 3. 5.4.2 替换文本 这个替换的功能在有的时候是很有用的,它的用法与其他编辑工具或编译器的差不多,相 信各位都不陌生了,这里就不在啰唆了。 第三个小技巧是跨文件查找功能,先双击你要找的函数/变量名(这里我们还是以系统时钟 初始化函数:delay_init 为例),然后再点击 IDE 上面的 ,弹出如图 3. 5.4.3 所示对话框: 图 3. 5.4.3 跨文件查找 点击 Find,MDK 就会帮你找出所有含有 delay_init 字段的文件并列出其所在位置,如图 3. 5.4.4 所示: 114 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 3. 5.4.4 查找结果 该方法可以很方便的查找各种函数/变量,而且可以限定搜索范围(比如只查找.c 文件和.h 文件等),是非常实用的一个技巧。 115 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第四章 STM32 开发基础知识入门 这一章,我们将着重 STM32 开发的一些基础知识,让大家对 STM32 开发有一个初步的了 解,为后面 STM32 的学习做一个铺垫,方便后面的学习。这一章的内容大家第一次看的时候 可以只了解一个大概,后面需要用到这方面的知识的时候再回过头来仔细看看。这章我们分 7 个小结, ·4.1 MDK 下 C 语言基础复习 ·4.2 STM32 系统架构 ·4.3 STM32 时钟系统 ·4.4 端口复用和重映射 ·4.5 STM32 NVIC 中断管理 ·4.6 MDK 中寄存器地址名称映射分析 ·4.7 MDK 固件库快速开发技巧 4.1 MDK 下 C 语言基础复习 这一节我们主要讲解一下 C 语言基础知识。C 语言知识博大精深,也不是我们三言两语能 讲解清楚,同时我们相信学 STM32 这种级别 MCU 的用户,C 语言基础应该都是没问题的。我们 这里主要是简单的复习一下几个 C 语言基础知识点,引导那些 C 语言基础知识不是很扎实的用 户能够快速开发 STM32 程序。同时希望这些用户能够多去复习一下 C 语言基础知识,C 语言毕 竟是单片机开发中的必备基础知识。对于 C 语言基础比较扎实的用户,这部分知识可以忽略不 看。 4.1.1 位操作 C 语言位操作相信学过 C 语言的人都不陌生了,简而言之,就是对基本类型变量可以在位级 别进行操作。这节的内容很多朋友都应该很熟练了,我这里也就点到为止,不深入探讨。下面 我们先讲解几种位操作符,然后讲解位操作使用技巧。 C 语言支持如下 6 中位操作 运算符 含义 运算符 含义 & 按位与 ~ 取反 | 按位或 << 左移 ^ 按位异或 >> 右移 表 4.1.1.16 种位操作 这些与或非,取反,异或,右移,左移这些到底怎么回事,这里我们就不多做详细,相信 大家学 C 语言的时候都学习过了。如果不懂的话,可以百度一下,非常多的知识讲解这些操作 符。下面我们想着重讲解位操作在单片机开发中的一些实用技巧。 1) 不改变其他位的值的状况下,对某几个位进行设值。 这个场景单片机开发中经常使用,方法就是先对需要设置的位用&操作符进行清零操作, 然后用|操作符设值。比如我要改变 GPIOA 的状态,可以先对寄存器的值进行&清零操作 GPIOA->CRL&=0XFFFFFF0F; //将第 4-7 位清 0 然后再与需要设置的值进行|或运算 GPIOA->CRL|=0X00000040; //设置相应位的值,不改变其他位的值 116 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 2) 移位操作提高代码的可读性。 移位操作在单片机开发中也非常重要,下面让我们看看固件库的 GPIO 初始化的函数里 面的一行代码 GPIOx->BSRR = (((uint32_t)0x01) << pinpos); 这个操作就是将 BSRR 寄存器的第 pinpos 位设置为 1,为什么要通过左移而不是直接设 置一个固定的值呢?其实,这是为了提高代码的可读性以及可重用性。这行代码可以 很直观明了的知道,是将第 pinpos 位设置为 1。如果你写成 GPIOx->BSRR =0x0030; 这样的代码就不好看也不好重用了。 类似这样的代码很多: GPIOA->ODR|=1<<5; //PA.5 输出高,不改变其他位 这样我们一目了然,5 告诉我们是第 5 位也就是第 6 个端口,1 告诉我们是设置为 1 了。 3) ~取反操作使用技巧 SR 寄存器的每一位都代表一个状态,某个时刻我们希望去设置某一位的值为 0,同时 其他位都保留为 1,简单的作法是直接给寄存器设置一个值: TIMx->SR=0xFFF7; 这样的作法设置第 3 位为 0,但是这样的作法同样不好看,并且可读性很差。看看库函数 代码中怎样使用的: TIMx->SR = (uint16_t)~TIM_FLAG; 而 TIM_FLAG 是通过宏定义定义的值: #define TIM_FLAG_Update ((uint16_t)0x0001) #define TIM_FLAG_CC1 ((uint16_t)0x0002) 看这个应该很容易明白,可以直接从宏定义中看出 TIM_FLAG_Update 就是设置的第 0 位了, 可读性非常强。 4.1.2 define 宏定义 define 是 C 语言中的预处理命令,它用于宏定义,可以提高源代码的可读性,为编程提供 方便。常见的格式: #define 标识符 字符串 “标识符”为所定义的宏名。“字符串”可以是常数、表达式、格式串等。例如: #define SYSCLK_FREQ_72MHz 72000000 定义标识符 SYSCLK_FREQ_72MHz 的值为 72000000。 至于 define 宏定义的其他一些知识,比如宏定义带参数这里我们就不多讲解。 4.1.3 ifdef 条件编译 单片机程序开发过程中,经常会遇到一种情况,当满足某条件时对一组语句进行编译,而 当条件不满足时则编译另一组语句。条件编译命令最常见的形式为: #ifdef 标识符 程序段 1 #else 程序段 2 #endif 117 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 它的作用是:当标识符已经被定义过(一般是用#define 命令定义),则对程序段 1 进行编译, 否则编译程序段 2。 其中#else 部分也可以没有,即: #ifdef 程序段 1 #endif 这个条件编译在 MDK 里面是用得很多的,在 stm32f10x.h 这个头文件中经常会看到这样的语句: #ifdef STM32F10X_HD 大容量芯片需要的一些变量定义 #end 而 STM32F10X_HD 则是我们通过#define 来定义的。条件编译也是 c 语言的基础知识,这里也就 点到为止吧。 4.1.4 extern 变量申明 C 语言中 extern 可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编 译器遇到此变量和函数时在其他模块中寻找其定义。这里面要注意,对于 extern 申明变量可以多 次,但定义只有一次。在我们的代码中你会看到看到这样的语句: extern u16 USART_RX_STA; 这个语句是申明 USART_RX_STA 变量在其他文件中已经定义了,在这里要使用到。所以,你肯定 可以找到在某个地方有变量定义的语句: u16 USART_RX_STA; 的出现。下面通过一个例子说明一下使用方法。 在 Main.c 定义的全局变量 id,id 的初始化都是在 Main.c 里面进行的。 Main.c 文件 u8 id;//定义只允许一次 main() { id=1; printf("d%",id);//id=1 test(); printf("d%",id);//id=2 } 但是我们希望在 test.c 的 changeId(void)函数中使用变量 id,这个时候我们就需要在 test.c 里面去申明变量 id 是外部定义的了,因为如果不申明,变量 id 的作用域是到不了 test.c 文件 中。看下面 test.c 中的代码: extern u8 id;//申明变量 id 是在外部定义的,申明可以在很多个文件中进行 void test(void){ id=2; } 在 test.c 中申明变量 id 在外部定义,然后在 test.c 中就可以使用变量 id 了。 对于 extern 申明函数在外部定义的应用,这里我们就不多讲解了。 118 4.1.5 typedef 类型别名 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 typedef 用于为现有类型创建一个新的名字,或称为类型别名,用来简化变量的定义。 typedef 在 MDK 用得最多的就是定义结构体的类型别名和枚举类型了。 struct _GPIO { __IO uint32_t CRL; __IO uint32_t CRH; … }; 定义了一个结构体 GPIO,这样我们定义变量的方式为: struct _GPIO GPIOA;//定义结构体变量 GPIOA 但是这样很繁琐,MDK 中有很多这样的结构体变量需要定义。这里我们可以为结体定义一个别 名 GPIO_TypeDef,这样我们就可以在其他地方通过别名 GPIO_TypeDef 来定义结构体变量了。 方法如下: typedef struct { __IO uint32_t CRL; __IO uint32_t CRH; … } GPIO_TypeDef; Typedef 为结构体定义一个别名 GPIO_TypeDef,这样我们可以通过 GPIO_TypeDef 来定义结构体 变量: GPIO_TypeDef _GPIOA,_GPIOB; 这里的 GPIO_TypeDef 就跟 struct _GPIO 是等同的作用了。 这样是不是方便很多? 4.1.6 结构体 经常很多用户提到,他们对结构体使用不是很熟悉,但是 MDK 中太多地方使用结构体以及 结构体指针,这让他们一下子摸不着头脑,学习 STM32 的积极性大大降低,其实结构体并不是 那么复杂,这里我们稍微提一下结构体的一些知识,还有一些知识我们会在下一节的“寄存器 地址名称映射分析”中讲到一些。 声明结构体类型: Struct 结构体名{ 成员列表; }变量名列表; 例如: Struct U_TYPE { Int BaudRate Int WordLength; }usart1,usart2; 在结构体申明的时候可以定义变量,也可以申明之后定义,方法是: Struct 结构体名字 结构体变量列表 ; 例如:struct U_TYPE usart1,usart2; 119 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 结构体成员变量的引用方法是: 结构体变量名字.成员名 比如要引用 usart1 的成员 BaudRate,方法是:usart1.BaudRate; 结构体指针变量定义也是一样的,跟其他变量没有啥区别。 例如:struct U_TYPE *usart3;//定义结构体指针变量 usart1; 结构体指针成员变量引用方法是通过“->”符号实现,比如要访问 usart3 结构体指针指向的结 构体的成员变量 BaudRate,方法是: Usart3->BaudRate; 上面讲解了结构体和结构体指针的一些知识,其他的什么初始化这里就不多讲解了。讲到这里, 有人会问,结构体到底有什么作用呢?为什么要使用结构体呢?下面我们将简单的通过一个实 例回答一下这个问题。 在我们单片机程序开发过程中,经常会遇到要初始化一个外设比如串口,它的初始化状态 是由几个属性来决定的,比如串口号,波特率,极性,以及模式。对于这种情况,在我们没有 学习结构体的时候,我们一般的方法是: void USART_Init(u8 usartx,u32 u32 BaudRate,u8 parity,u8 mode); 这种方式是有效的同时在一定场合是可取的。但是试想,如果有一天,我们希望往这个函数里 面再传入一个参数,那么势必我们需要修改这个函数的定义,重新加入字长这个入口参数。于 是我们的定义被修改为: void USART_Init (u8 usartx,u32 BaudRate, u8 parity,u8 mode,u8 wordlength ); 但是如果我们这个函数的入口参数是随着开发不断的增多,那么是不是我们就要不断的修改函 数的定义呢?这是不是给我们开发带来很多的麻烦?那又怎样解决这种情况呢? 这样如果我们使用到结构体就能解决这个问题了。我们可以在不改变入口参数的情况下, 只需要改变结构体的成员变量,就可以达到上面改变入口参数的目的。 结构体就是将多个变量组合为一个有机的整体。上面的函数,BaudRate,wordlength, Parity,mode,wordlength 这些参数,他们对于串口而言,是一个有机整体,都是来设置串口参 数的,所以我们可以将他们通过定义一个结构体来组合在一个。MDK 中是这样定义的: typedef struct { uint32_t USART_BaudRate; uint16_t USART_WordLength; uint16_t USART_StopBits; uint16_t USART_Parity; uint16_t USART_Mode; uint16_t USART_HardwareFlowControl; } USART_InitTypeDef; 于是,我们在初始化串口的时候入口参数就可以是 USART_InitTypeDef 类型的变量或者指针变 量了,MDK 中是这样做的: void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct); 这样,任何时候,我们只需要修改结构体成员变量,往结构体中间加入新的成员变量,而不需 要修改函数定义就可以达到修改入口参数同样的目的了。这样的好处是不用修改任何函数定义 就可以达到增加变量的目的。 120 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 理解了结构体在这个例子中间的作用吗?在以后的开发过程中,如果你的变量定义过多, 如果某几个变量是用来描述某一个对象,你可以考虑将这些变量定义在结构体中,这样也许可 以提高你的代码的可读性。 使用结构体组合参数,可以提高代码的可读性,不会觉得变量定义混乱。当然结构体的作 用就远远不止这个了,同时,MDK 中用结构体来定义外设也不仅仅只是这个作用,这里我们只 是举一个例子,通过最常用的场景,让大家理解结构体的一个作用而已。后面一节我们还会讲 解结构体的一些其他知识。 4.2 STM32 系统架构 STM32 的系统架构比 51 单片机就要强大很多了。STM32 系统架构的知识可以在《STM32 中文参考手册 V10》的 P25~28 有讲解,这里我们也把这一部分知识抽取出来讲解,是为了大 家在学习 STM32 之前对系统架构有一个初步的了解。这里的内容基本也是从中文参考手册中 参考过来的,让大家能通过我们手册也了解到,免除了到处找资料的麻烦吧。如果需要详细深 入的了解 STM32 的系统架构,还需要在网上搜索其他资料学习学习。 我们这里所讲的 STM32 系统架构主要针对的 STM32F103 这些非互联型芯片。首先我们看 看 STM32 的系统架构图: 图 4.2.1STM32 系统架构图 STM32 主系统主要由四个驱动单元和四个被动单元构成。 四个驱动单元是: 内核 DCode 总线; 系统总线; 121 STM32F1 开发指南(库函数版) 通用 DMA1; 通用 DMA2; 四被动单元是: AHB 到 APB 的桥:连接所有的 APB 设备; 内部 FlASH 闪存; 内部 SRAM; ALIENTEK 战舰 STM32F103 V3 开发板教程 FSMC; 下面我们具体讲解一下图中几个总线的知识: ① ICode 总线:该总线将 M3 内核指令总线和闪存指令接口相连,指令的预取在该总线上 面完成。 ② DCode 总线:该总线将 M3 内核的 DCode 总线与闪存存储器的数据接口相连接,常量 加载和调试访问在该总线上面完成。 ③ 系统总线:该总线连接 M3 内核的系统总线到总线矩阵,总线矩阵协调内核和 DMA 间 访问。 ④ DMA 总线:该总线将 DMA 的 AHB 主控接口与总线矩阵相连,总线矩阵协调 CPU 的 DCode 和 DMA 到 SRAM,闪存和外设的访问。 ⑤ 总线矩阵:总线矩阵协调内核系统总线和 DMA 主控总线之间的访问仲裁,仲裁利用 轮换算法。 ⑥ AHB/APB 桥:这两个桥在 AHB 和 2 个 APB 总线间提供同步连接,APB1 操作速度限于 36MHz,APB2 操作速度全速。 对于系统架构的知识,在刚开始学习 STM32 的时候只需要一个大概的了解,大致知道是个 什么情况即可。对于寻址之类的知识,这里就不做深入的讲解,中文参考手册都有很详细的讲 解。 4.3 STM32 时钟系统 STM32 时钟系统的知识在《STM32 中文参考手册 V10》中的 P55~P73 有非常详细的讲解, 网上关于时钟系统的讲解也基本都是参考的这里,讲不出啥特色,不过作为一个完整的参考手 册,我们必然要提到时钟系统的知识。这些知识也不是什么原创,纯粹的是看网友发的帖子和 手册来总结的,有一些直接是 copy 过来的,望大家谅解。 众所周知,时钟系统是 CPU 的脉搏,就像人的心跳一样。所以时钟系统的重要性就不言而 喻了。 STM32 的时钟系统比较复杂,不像简单的 51 单片机一个系统时钟就可以解决一切。于 是有人要问,采用一个系统时钟不是很简单吗?为什么 STM32 要有多个时钟源呢? 因为首先 STM32 本身非常复杂,外设非常的多,但是并不是所有外设都需要系统时钟这么高的频率,比 如看门狗以及 RTC 只需要几十 k 的时钟即可。同一个电路,时钟越快功耗越大,同时抗电磁干 扰能力也会越弱,所以对于较为复杂的 MCU 一般都是采取多时钟源的方法来解决这些问题。 首先让我们来看看 STM32 的时钟系统图吧: 122 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 4.3.1STM32 时钟系统图 在 STM32 中,有五个时钟源,为 HSI、HSE、LSI、LSE、PLL。从时钟频率来分可以分为 高速时钟源和低速时钟源,在这 5 个中 HIS,HSE 以及 PLL 是高速时钟,LSI 和 LSE 是低速时 钟。从来源可分为外部时钟源和内部时钟源,外部时钟源就是从外部通过接晶振的方式获取时 钟源,其中 HSE 和 LSE 是外部时钟源,其他的是内部时钟源。下面我们看看 STM32 的 5 个时 钟源,我们讲解顺序是按图中红圈标示的顺序: ①、HSI 是高速内部时钟,RC 振荡器,频率为 8MHz。 ② 、 HSE 是 高 速 外 部 时 钟 , 可 接 石 英 / 陶 瓷 谐 振 器 , 或 者 接 外 部 时 钟 源 , 频 率 范 围 为 4MHz~16MHz。我们的开发板接的是 8M 的晶振。 ③、LSI 是低速内部时钟,RC 振荡器,频率为 40kHz。独立看门狗的时钟源只能是 LSI,同 时 LSI 还可以作为 RTC 的时钟源。 ④、LSE 是低速外部时钟,接频率为 32.768kHz 的石英晶体。这个主要是 RTC 的时钟源。 ⑤、PLL 为锁相环倍频输出,其时钟输入源可选择为 HSI/2、HSE 或者 HSE/2。倍频可选择为 123 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 2~16 倍,但是其输出频率最大不得超过 72MHz。 上面我们简要概括了 STM32 的时钟源,那么这 5 个时钟源是怎么给各个外设以及系统提 供时钟的呢?这里我们将一一讲解。我们还是从图的下方讲解起吧,因为下方比较简单。 图中我们用 A~E 标示我们要讲解的地方。 A. MCO 是 STM32 的一个时钟输出 IO(PA8),它可以选择一个时钟信号输出,可以 选择为 PLL 输出的 2 分频、HSI、HSE、或者系统时钟。这个时钟可以用来给外 部其他系统提供时钟源。 B. 这里是 RTC 时钟源,从图上可以看出,RTC 的时钟源可以选择 LSI,LSE,以及 HSE 的 128 分频。 C. 从图中可以看出 C 处 USB 的时钟是来自 PLL 时钟源。STM32 中有一个全速功能 的 USB 模块,其串行接口引擎需要一个频率为 48MHz 的时钟源。该时钟源只能 从 PLL 输出端获取,可以选择为 1.5 分频或者 1 分频,也就是,当需要使用 USB 模块时,PLL 必须使能,并且时钟频率配置为 48MHz 或 72MHz。 D. D 处就是 STM32 的系统时钟 SYSCLK,它是供 STM32 中绝大部分部件工作的时 钟源。系统时钟可选择为 PLL 输出、HSI 或者 HSE。系统时钟最大频率为 72MHz, 当然你也可以超频,不过一般情况为了系统稳定性是没有必要冒风险去超频的。 E. 这里的 E 处是指其他所有外设了。从时钟图上可以看出,其他所有外设的时钟最 终来源都是 SYSCLK。SYSCLK 通过 AHB 分频器分频后送给各模块使用。这些 模块包括: ①、AHB 总线、内核、内存和 DMA 使用的 HCLK 时钟。 ②、通过 8 分频后送给 Cortex 的系统定时器时钟,也就是 systick 了。 ③、直接送给 Cortex 的空闲运行时钟 FCLK。 ④、送给 APB1 分频器。APB1 分频器输出一路供 APB1 外设使用(PCLK1,最大 频率 36MHz),另一路送给定时器(Timer)2、3、4 倍频器使用。 ⑤、送给 APB2 分频器。APB2 分频器分频输出一路供 APB2 外设使用(PCLK2, 最大频率 72MHz),另一路送给定时器(Timer)1 倍频器使用。 其中需要理解的是 APB1 和 APB2 的区别,APB1 上面连接的是低速外设,包括电源接口、 备份接口、CAN、USB、I2C1、I2C2、UART2、UART3 等等,APB2 上面连接的是高速外设包 括 UART1、SPI1、Timer1、ADC1、ADC2、所有普通 IO 口(PA~PE)、第二功能 IO 口等。居宁 老师的《稀里糊涂玩 STM32》资料里面教大家的记忆方法是 2>1, APB2 下面所挂的外设的时 钟要比 APB1 的高。 在以上的时钟输出中,有很多是带使能控制的,例如 AHB 总线时钟、内核时钟、各种 APB1 外设、APB2 外设等等。当需要使用某模块时,记得一定要先使能对应的时钟。后面我们讲解 实例的时候回讲解到时钟使能的方法。 STM32 时钟系统的配置除了初始化的时候在 system_stm32f10x.c 中的 SystemInit()函数中 外,其他的配置主要在 stm32f10x_rcc.c 文件中,里面有很多时钟设置函数,大家可以打开这个 文件浏览一下,基本上看看函数的名称就知道这个函数的作用。在大家设置时钟的时候,一定 要仔细参考 STM32 的时钟图,做到心中有数。这里需要指明一下,对于系统时钟,默认情况 下是在 SystemInit 函数的 SetSysClock()函数中间判断的,而设置是通过宏定义设置的。我们可 以看看 SetSysClock()函数体: static void SetSysClock(void) { #ifdef SYSCLK_FREQ_HSE 124 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 SetSysClockToHSE(); #elif defined SYSCLK_FREQ_24MHz SetSysClockTo24(); #elif defined SYSCLK_FREQ_36MHz SetSysClockTo36(); #elif defined SYSCLK_FREQ_48MHz SetSysClockTo48(); #elif defined SYSCLK_FREQ_56MHz SetSysClockTo56(); #elif defined SYSCLK_FREQ_72MHz SetSysClockTo72(); #endif } 这段代码非常简单,就是判断系统宏定义的时钟是多少,然后设置相应值。我们系统默认宏定 义是 72MHz: #define SYSCLK_FREQ_72MHz 72000000 如果你要设置为 36MHz,只需要注释掉上面代码,然后加入下面代码即可: #define SYSCLK_FREQ_36MHz 36000000 同时还要注意的是,当我们设置好系统时钟后,可以通过变量 SystemCoreClock 获取系统时钟 值,如果系统是 72M 时钟,那么 SystemCoreClock=72000000。这是在 system_stm32f10x.c 文件 中设置的: #ifdef SYSCLK_FREQ_HSE uint32_t SystemCoreClock = SYSCLK_FREQ_HSE; #elif defined SYSCLK_FREQ_36MHz uint32_t SystemCoreClock = SYSCLK_FREQ_36MHz; #elif defined SYSCLK_FREQ_48MHz uint32_t SystemCoreClock = SYSCLK_FREQ_48MHz; #elif defined SYSCLK_FREQ_56MHz uint32_t SystemCoreClock = SYSCLK_FREQ_56MHz; #elif defined SYSCLK_FREQ_72MHz uint32_t SystemCoreClock = SYSCLK_FREQ_72MHz; #else uint32_t SystemCoreClock = HSI_VALUE; #endif 这里总结一下 SystemInit()函数中设置的系统时钟大小: SYSCLK(系统时钟) =72MHz AHB 总线时钟(使用 SYSCLK) =72MHz APB1 总线时钟(PCLK1) =36MHz APB2 总线时钟(PCLK2) =72MHz PLL 时钟 =72MHz 125 4.4 端口复用和重映射 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 4.4.1 端口复用功能 STM32 有很多的内置外设,这些外设的外部引脚都是与 GPIO 复用的。也就是说,一个 GPIO 如果可以复用为内置外设的功能引脚,那么当这个 GPIO 作为内置外设使用的时候,就叫做复用。 这部分知识在《STM32 中文参考手册 V10》的 P109,P116~P121 有详细的讲解哪些 GPIO 管脚是 可以复用为哪些内置外设的。这里我们就不一一讲解。 大家都知道,MCU 都有串口,STM32 有好几个串口。比如说 STM32F103ZET6 有 5 个串口,我 们可以查手册知道,串口 1 的引脚对应的 IO 为 PA9,PA10.PA9,PA10 默认功能是 GPIO,所以当 PA9,PA10 引脚作为串口 1 的 TX,RX 引脚使用的时候,那就是端口复用。 图 4.4.1.1 串口 1 复用管脚 复用端口初始化有几个步骤: 1) GPIO 端口时钟使能。要使用到端口复用,当然要使能端口的时钟了。 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); 2) 复用的外设时钟使能。比如你要将端口 PA9,PA10 复用为串口,所以要使能串口时钟。 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); 3) 端口模式配置。 在 IO 复用位内置外设功能引脚的时候,必须设置 GPIO 端口的模式,至于 在复用功能下 GPIO 的模式是怎么对应的,这个可以查看手册《STM32 中文参考手册 V10》 P110 的表格“8.1.11 外设的 GPIO 配置”。这里我们拿 Usart1 举例: 图 4.4.1.2 串口复用 GPIO 配置 从表格中可以看出,我们要配置全双工的串口 1,那么 TX 管脚需要配置为推挽复用输出, RX 管脚配置为浮空输入或者带上拉输入。 //USART1_TX PA.9 复用推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出 GPIO_Init(GPIOA, &GPIO_InitStructure); //USART1_RX PA.10 浮空输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入 GPIO_Init(GPIOA, &GPIO_InitStructure); 上面代码的含义在我们的第一个实验学完之后大家自然会了解,这里只是做个概括。 所以,我们在使用复用功能的是时候,最少要使能 2 个时钟: 1) GPIO 时钟使能 2) 复用的外设时钟使能 126 同时要初始化 GPIO 以及复用外设功能 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 4.4.2 端口重映射 为了使不同器件封装的外设 IO 功能数量达到最优,可以把一些复用功能重新映射到其他一 些引脚上。STM32 中有很多内置外设的输入输出引脚都具有重映射(remap)的功能。我们知道每 个内置外设都有若干个输入输出引脚,一般这些引脚的输出端口都是固定不变的,为了让设计 工程师可以更好地安排引脚的走向和功能,在 STM32 中引入了外设引脚重映射的概念,即一个 外设的引脚除了具有默认的端口外,还可以通过设置重映射寄存器的方式,把这个外设的引脚 映射到其它的端口。 简单的讲就是把管脚的外设功能映射到另一个管脚,但不是可以随便映射的,具体对应关 系《STM32 中文参考手册 V10》的 P116 页“8.3 复用功能和调试配置”有讲解。这里我们同样 拿串口 1 为例来讲解。 图 4.4.2.1 串口重映射管脚表 上图是截取的中文参考手册中的重映射表,从表中可以看出,默认情况下,串口 1 复用的时候 的引脚位 PA9,PA10,同时我们可以将 TX 和 RX 重新映射到管脚 PB6 和 PB7 上面去。 所以重映射我们同样要使能复用功能的时候讲解的 2 个时钟外,还要使能 AFIO 功能时钟,然后 要调用重映射函数。详细步骤为: 1)使能 GPIOB 时钟: RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); 2)使能串口 1 时钟: RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); 3)使能 AFIO 时钟: RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); 4)开启重映射: GPIO_PinRemapConfig(GPIO_Remap_USART1, ENABLE); 这样就将串口的 TX 和 RX 重映射到管脚 PB6 和 PB7 上面了。至于有哪些功能可以重映射,大家 除了查看中文参考手册之外,还可以从 GPIO_PinRemapConfig 函数入手查看第一个入口参数的 取值范围可以得知。在 stm32f10x_gpio.h 文件中定义了取值范围为下面宏定义的标识符,这里 我们贴一小部分: #define GPIO_Remap_SPI1 ((uint32_t)0x00000001) #define GPIO_Remap_I2C1 #define GPIO_Remap_USART1 ((uint32_t)0x00000002) ((uint32_t)0x00000004) #define GPIO_Remap_USART2 ((uint32_t)0x00000008) #define GPIO_PartialRemap_USART3 ((uint32_t)0x00140010) #define GPIO_FullRemap_USART3 ((uint32_t)0x00140030) 127 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 从上面可以看出,USART1 只有一种重映射,而对于 USART3,存在部分重映射和完全重映射。所 谓部分重映射就是部分管脚和默认的是一样的,而部分管脚是重新映射到其他管脚。而完全重 映射就是所有管脚都重新映射到其他管脚。看看手册中的 USART3 重映射表: 图 4.4.2.2 USART3 重映射管脚对应表 部分重映射就是 PB10,PB11,PB12 重映射到 PC10,PC11,PC12 上。而 PB13 和 PB14 和没有重映射 情况是一样的,都是 USART3_CTS 和 USART3_RTS 对应管脚。完全重映射就是将这两个脚重新映 射到 PD11 和 PD12 上去。我们要使用 USART3 的部分重映射,我们调用函数方法为: GPIO_PinRemapConfig(GPIO_PartialRemap_USART3, ENABLE); 这些知识我们后面在使用的过程中间还会讲解,这里只是对重映射概念做个简要的描述。 4.5 STM32 NVIC 中断优先级管理 CM3 内核支持 256 个中断,其中包含了 16 个内核中断和 240 个外部中断,并且具有 256 级的可编程中断设置。但 STM32 并没有使用 CM3 内核的全部东西,而是只用了它的一部分。 STM32 有 84 个中断,包括 16 个内核中断和 68 个可屏蔽中断,具有 16 级可编程的中断优先级。 而我们常用的就是这 68 个可屏蔽中断,但是 STM32 的 68 个可屏蔽中断,在 STM32F103 系列 上面,又只有 60 个(在 107 系列才有 68 个)。因为我们开发板选择的芯片是 STM32F103 系列 的所以我们就只针对 STM32F103 系列这 60 个可屏蔽中断进行介绍。 在 MDK 内,与 NVIC 相关的寄存器,MDK 为其定义了如下的结构体: typedef struct { __IO uint32_t ISER[8]; /*!< Interrupt Set Enable Register */ uint32_t RESERVED0[24]; __IO uint32_t ICER[8]; /*!< Interrupt Clear Enable Register */ uint32_t RSERVED1[24]; __IO uint32_t ISPR[8]; /*!< Interrupt Set Pending Register */ uint32_t RESERVED2[24]; __IO uint32_t ICPR[8]; /*!< Interrupt Clear Pending Register */ uint32_t RESERVED3[24]; __IO uint32_t IABR[8]; /*!< Interrupt Active bit Register */ uint32_t RESERVED4[56]; __IO uint8_t IP[240]; /*!< Interrupt Priority Register, 8Bit wide */ uint32_t RESERVED5[644]; __O uint32_t STIR; /*!< Software Trigger Interrupt Register */ } NVIC_Type; 128 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 STM32 的中断在这些寄存器的控制下有序的执行的。只有了解这些中断寄存器,才能方便 的使用 STM32 的中断。下面重点介绍这几个寄存器: ISER[8]:ISER 全称是:Interrupt Set-Enable Registers,这是一个中断使能寄存器组。上面 说了 CM3 内核支持 256 个中断,这里用 8 个 32 位寄存器来控制,每个位控制一个中断。但是 STM32F103 的可屏蔽中断只有 60 个,所以对我们来说,有用的就是两个(ISER[0]和 ISER[1]), 总共可以表示 64 个中断。而 STM32F103 只用了其中的前 60 位。ISER[0]的 bit0~bit31 分别对 应中断 0~31。ISER[1]的 bit0~27 对应中断 32~59;这样总共 60 个中断就分别对应上了。你要 使能某个中断,必须设置相应的 ISER 位为 1,使该中断被使能(这里仅仅是使能,还要配合中 断分组、屏蔽、IO 口映射等设置才算是一个完整的中断设置)。具体每一位对应哪个中断,请 参考 stm32f10x.h 里面的第 140 行处(针对编译器 MDK5 来说)。 ICER[8]:全称是:Interrupt Clear-Enable Registers,是一个中断除能寄存器组。该寄存器组 与 ISER 的作用恰好相反,是用来清除某个中断的使能的。其对应位的功能,也和 ICER 一样。 这里要专门设置一个 ICER 来清除中断位,而不是向 ISER 写 0 来清除,是因为 NVIC 的这些寄 存器都是写 1 有效的,写 0 是无效的。具体为什么这么设计,请看《CM3 权威指南》第 125 页, NVIC 概览一章。 ISPR[8]:全称是:Interrupt Set-Pending Registers,是一个中断挂起控制寄存器组。每个位 对应的中断和 ISER 是一样的。通过置 1,可以将正在进行的中断挂起,而执行同级或更高级别 的中断。写 0 是无效的。 ICPR[8]:全称是:Interrupt Clear-Pending Registers,是一个中断解挂控制寄存器组。其作 用与 ISPR 相反,对应位也和 ISER 是一样的。通过设置 1,可以将挂起的中断接挂。写 0 无效。 IABR[8]:全称是:Interrupt Active Bit Registers,是一个中断激活标志位寄存器组。对应位 所代表的中断和 ISER 一样,如果为 1,则表示该位所对应的中断正在被执行。这是一个只读寄 存器,通过它可以知道当前在执行的中断是哪一个。在中断执行完了由硬件自动清零。 IP[240]:全称是:Interrupt Priority Registers,是一个中断优先级控制的寄存器组。这个寄 存器组相当重要!STM32 的中断分组与这个寄存器组密切相关。IP 寄存器组由 240 个 8bit 的寄 存器组成,每个可屏蔽中断占用 8bit,这样总共可以表示 240 个可屏蔽中断。而 STM32 只用到 了其中的前 60 个。IP[59]~IP[0]分别对应中断 59~0。而每个可屏蔽中断占用的 8bit 并没有全部 使用,而是 只用了高 4 位。这 4 位,又分为抢占优先级和子优先级。抢占优先级在前,子优先 级在后。而这两个优先级各占几个位又要根据 SCB->AIRCR 中的中断分组设置来决定。 这里简单介绍一下 STM32 的中断分组:STM32 将中断分为 5 个组,组 0~4。该分组的设 置是由 SCB->AIRCR 寄存器的 bit10~8 来定义的。具体的分配关系如表 4.5.1 所示: 组 AIRCR[10:8] bit[7:4]分配情况 分配结果 0 111 0:4 0 位抢占优先级,4 位响应优先级 1 110 1:3 1 位抢占优先级,3 位响应优先级 2 101 2:2 2 位抢占优先级,2 位响应优先级 3 100 3:1 3 位抢占优先级,1 位响应优先级 4 011 4:0 4 位抢占优先级,0 位响应优先级 表 4.5.1 AIRCR 中断分组设置表 通过这个表,我们就可以清楚的看到组 0~4 对应的配置关系,例如组设置为 3,那么此时 所有的 60 个中断,每个中断的中断优先寄存器的高四位中的最高 3 位是抢占优先级,低 1 位是 响应优先级。每个中断,你可以设置抢占优先级为 0~7,响应优先级为 1 或 0。抢占优先级的 级别高于响应优先级。而数值越小所代表的优先级就越高。 这里需要注意两点:第一,如果两个中断的抢占优先级和响应优先级都是一样的话,则看 129 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 哪个中断先发生就先执行;第二,高优先级的抢占优先级是可以打断正在进行的低抢占优先级 中断的。而抢占优先级相同的中断,高优先级的响应优先级不可以打断低响应优先级的中断。 结合实例说明一下:假定设置中断优先级组为 2,然后设置中断 3(RTC 中断)的抢占优先级 为 2,响应优先级为 1。中断 6(外部中断 0)的抢占优先级为 3,响应优先级为 0。中断 7(外 部中断 1)的抢占优先级为 2,响应优先级为 0。那么这 3 个中断的优先级顺序为:中断 7>中 断 3>中断 6。 上面例子中的中断 3 和中断 7 都可以打断中断 6 的中断。而中断 7 和中断 3 却不可以相互 打断! 通过以上介绍,我们熟悉了 STM32 中断设置的大致过程。接下来我们介绍如何使用库函 数实现以上中断分组设置以及中断优先级管理,使得我们以后的中断设置简单化。NVIC 中断 管理函数主要在 misc.c 文件里面。 首先要讲解的是中断优先级分组函数 NVIC_PriorityGroupConfig,其函数申明如下: void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup); 这个函数的作用是对中断的优先级进行分组,这个函数在系统中只能被调用一次,一旦分 组确定就最好不要更改。这个函数我们可以找到其实现: void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup) { assert_param(IS_NVIC_PRIORITY_GROUP(NVIC_PriorityGroup)); SCB->AIRCR = AIRCR_VECTKEY_MASK | NVIC_PriorityGroup; } 从函数体可以看出,这个函数唯一目的就是通过设置 SCB->AIRCR 寄存器来设置中断优先级分 组,这在前面寄存器讲解的过程中已经讲到。而其入口参数通过双击选中函数体里面的 “IS_NVIC_PRIORITY_GROUP”然后右键“Go to defition of …”可以查看到为: #define IS_NVIC_PRIORITY_GROUP(GROUP) (((GROUP) == NVIC_PriorityGroup_0) || ((GROUP) == NVIC_PriorityGroup_1) || \ ((GROUP) == NVIC_PriorityGroup_2) || \ ((GROUP) == NVIC_PriorityGroup_3) || \ ((GROUP) == NVIC_PriorityGroup_4)) 这也是我们上面表 4.5.1 讲解的,分组范围为 0-4。比如我们设置整个系统的中断优先级分组值 为 2,那么方法是: NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 这样就确定了一共为“2 位抢占优先级,2 位响应优先级”。 设置好了系统中断分组,那么对于每个中断我们又怎么确定他的抢占优先级和响应优先级 呢?下面我们讲解一个重要的函数为中断初始化函数 NVIC_Init,其函数申明为: void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct) 其中 NVIC_InitTypeDef 是一个结构体,我们可以看看结构体的成员变量: typedef struct { uint8_t NVIC_IRQChannel; uint8_t NVIC_IRQChannelPreemptionPriority; uint8_t NVIC_IRQChannelSubPriority; FunctionalState NVIC_IRQChannelCmd; 130 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 } NVIC_InitTypeDef; NVIC_InitTypeDef 结构体中间有三个成员变量,这三个成员变量的作用是: NVIC_IRQChannel:定义初始化的是哪个中断,这个我们可以在 stm32f10x.h 中找到 每个中断对应的名字。例如 USART1_IRQn。 NVIC_IRQChannelPreemptionPriority:定义这个中断的抢占优先级别。 NVIC_IRQChannelSubPriority:定义这个中断的子优先级别。 NVIC_IRQChannelCmd:该中断是否使能。 比如我们要使能串口 1 的中断,同时设置抢占优先级为 1,子优先级位 2,初始化的方法是: NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;//串口 1 中断 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1 ;// 抢占优先级为 1 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;// 子优先级位 2 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道使能 NVIC_Init(&NVIC_InitStructure); //根据上面指定的参数初始化 NVIC 寄存器 这里我们讲解了中断的分组的概念以及设定优先级值的方法,至于每种优先级还有一些关于清 除中断,查看中断状态,这在后面我们讲解每个中断的时候会详细讲解到。最后我们总结一下 中断优先级设置的步骤: 1. 系统运行开始的时候设置中断分组。确定组号,也就是确定抢占优先级和子优先级的 分配位数。调用函数为 NVIC_PriorityGroupConfig(); 2. 设置所用到的中断的中断优先级别。对每个中断调用函数为 NVIC_Init(); 4.6 MDK 中寄存器地址名称映射分析 之所以要讲解这部分知识,是因为经常会遇到客户提到不明白 MDK 中那些结构体是怎么与 寄存器地址对应起来的。这里我们就做一个简要的分析吧。 首先我们看看 51 中是怎么做的。51 单片机开发中经常会引用一个 reg51.h 的头文件,下 面我们看看他是怎么把名字和寄存器联系起来的: sfr P0 =0x80; sfr 也是一种扩充数据类型,点用一个内存单元,值域为 0~255。利用它可以访问 51 单片 机内部的所有特殊功能寄存器。如用 sfr P1 = 0x90 这一句定义 P1 为 P1 端口在片内的寄存 器。然后我们往地址为 0x80 的寄存器设值的方法是:P0=value; 那么在 STM32 中,是否也可以这样做呢??答案是肯定的。肯定也可以通过同样的方 式来做,但是 STM32 因为寄存器太多太多,如果一一以这样的方式列出来,那要好大的篇 幅,既不方便开发,也显得太杂乱无序的感觉。所以 MDK 采用的方式是通过结构体来将 寄存器组织在一起。下面我们就讲解 MDK 是怎么把结构体和地址对应起来的,为什么我 们修改结构体成员变量的值就可以达到操作对应寄存器的值。这些事情都是在 stm32f10x.h 文件中完成的。我们通过 GPIOA 的几个寄存器的地址来讲解吧。 首先我们可以查看《STM32 中文参考手册 V10》中的寄存器地址映射表(P159): 131 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 4.6.1 GPIO 寄存器地址映像 从这个表我们可以看出,GPIOA 的 7 个寄存器都是 32 位的,所以每个寄存器占有 4 个地址,一共占用 28 个地址,地址偏移范围为(000h~01Bh)。这个地址偏移是相对 GPIOA 的基地址而言的。GPIOA 的基地址是怎么算出来的呢?因为 GPIO 都是挂载在 APB2 总线 之上,所以它的基地址是由 APB2 总线的基地址+GPIOA 在 APB2 总线上的偏移地址决定 的。同理依次类推,我们便可以算出 GPIOA 基地址了。这里设计到总线的一些知识,我们 在后面会讲到。下面我们打开 stm32f10x.h 定位到 GPIO_TypeDef 定义处: typedef struct { __IO uint32_t CRL; __IO uint32_t CRH; __IO uint32_t IDR; __IO uint32_t ODR; __IO uint32_t BSRR; __IO uint32_t BRR; __IO uint32_t LCKR; } GPIO_TypeDef; 然后定位到: #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE) 可以看出,GPIOA 是将 GPIOA_BASE 强制转换为 GPIO_TypeDef 指针,这句话的意思是, GPIOA 指向地址 GPIOA_BASE,GPIOA_BASE 存放的数据类型为 GPIO_TypeDef。然后双 击“GPIOA_BASE”选中之后右键选中“Go to definition of ”,便可一查看 GPIOA_BASE 的宏定义: #define GPIOA_BASE 依次类推,可以找到最顶层: (APB2PERIPH_BASE + 0x0800) #define APB2PERIPH_BASE (PERIPH_BASE + 0x10000) #define PERIPH_BASE ((uint32_t)0x40000000) 所以我们便可以算出 GPIOA 的基地址位: 132 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 GPIOA_BASE= 0x40000000+0x10000+0x0800=0x40010800 下面我们再跟《STM32 中文参考手册 V10》比较一下看看 GPIOA 的基地址是不是 0x40010800。截图 P28 存储器映射表我们可以看到,GPIOA 的起始地址也就是基地址确实 是 0x40010800: 图 4.6.2 GPIO 存储器地址映射表 同样的道理,我们可以推算出其他外设的基地址。 上面我们已经知道 GPIOA 的基地址,那么那些 GPIOA 的 7 个寄存器的地址又是怎么 算出来的呢??在上面我们讲过 GPIOA 的各个寄存器对于 GPIOA 基地址的偏移地址,所 以我们自然可以算出来每个寄存器的地址。 GPIOA 的寄存器的地址=GPIOA 基地址+寄存器相对 GPIOA 基地址的偏移值 这个偏移值在上面的寄存器地址映像表中可以查到。 那么在结构体里面这些寄存器又是怎么与地址一一对应的呢?这里就涉及到结构体的 一个特征,那就是结构体存储的成员他们的地址是连续的。上面讲到 GPIOA 是指向 GPIO_TypeDef 类型的指针,又由于 GPIO_TypeDef 是结构体,所以自然而然我们就可以算 出 GPIOA 指向的结构体成员变量对应地址了。 寄存器 偏移地址 实际地址=基地址+偏移地址 GPIOA->CRL 0x00 0x40010800+0x00 GPIOA->CRH; 0x04 0x40010800+0x04 GPIOA->IDR; 0x08 0x40010800+0x08 GPIOA->ODR 0x0c 0x40010800+0x0c GPIOA->BSRR 0x10 0x40010800+0x10 GPIOA->BRR 0x14 0x40010800+0x14 GPIOA->LCKR 0x18 0x40010800+0x18 表 4.6.3 GPIOA 各寄存器实际地址表 我们可以把 GPIO_TypeDef 的定义中的成员变量的顺序和 GPIOx 寄存器地址映像对比 可以发现,他们的顺序是一致的,如果不一致,就会导致地址混乱了。 这就是为什么固件库里面:GPIOA->BRR=value;就是设置地址为 0x40010800 +0x014(BRR 偏移量)=0x40010814 的寄存器 BRR 的值了。它和 51 里面 P0=value 是设置地 址为 0x80 的 P0 寄存器的值是一样的道理。 看到这里你是否会学起来踏实一点呢??STM32 使用的方式虽然跟 51 单片机不一样, 但是原理都是一致的。 4.7 MDK 固件库快速组织代码技巧 这一节主要讲解在使用 MDK 固件库开发的时候的一些小技巧,仅供初学者参考。这节的 知识大家可以在学习第一个跑马灯实验的时候参考一下,对初学者应该很有帮助。我们就用最 简单的 GPIO 初始化函数为例。 133 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 现在我们要初始化某个 GPIO 端口,我们要怎样快速操作呢?在头文件 stm32f10x_gpio.h 头文件中,定义 GPIO 初始化函数为: void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct); 现在我们想写初始化函数,那么我们在不参考其他代码的前提下,怎么组织代码呢? 首先,我们可以看出,函数的入口参数是 GPIO_TypeDef 类型指针和 GPIO_InitTypeDef 类 型 指 针 , 因 为 GPIO_TypeDef 入 口 参 数 比 较 简 单 , 所 以 我 们 通 过 第 二 个 入 口 参 数 GPIO_InitTypeDef 类型指针来讲解。双击 GPIO_InitTypeDef 后右键选择“Go to definition…”,如 下图 4.7.1: 图 4.7.1 查看类型定义方法 于是定位到 stm32f10x_gpio.h 中 GPIO_InitTypeDef 的定义处: typedef struct { uint16_t GPIO_Pin; GPIOSpeed_TypeDef GPIO_Speed; GPIOMode_TypeDef GPIO_Mode; GPIOMode_TypeDef */ }GPIO_InitTypeDef; 可以看到这个结构体有 3 个成员变量,这也告诉我们一个信息,一个 GPIO 口的状态是由速度 (Speed)和模式(Mode)来决定的。我们首先要定义一个结构体变量,下面我们定义: GPIO_InitTypeDef GPIO_InitStructure; 接着我们要初始化结构体变量 GPIO_InitStructure。首先我们要初始化成员变量 GPIO_Pin,这个 时候我们就有点迷糊了,这个变量到底可以设置哪些值呢?这些值的范围有什么规定吗? 这里我们就要找到 GPIO_Init()函数的实现处,同样,双击 GPIO_Init,右键点击“Go to definition of …”,这样光标定位到 stm32f10x_gpio.c 文件中的 GPIO_Init 函数体开始处,我们可以 看到在函数的开始处有如下几行: 134 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct) { …… /* Check the parameters */ assert_param(IS_GPIO_ALL_PERIPH(GPIOx)); assert_param(IS_GPIO_MODE(GPIO_InitStruct->GPIO_Mode)); assert_param(IS_GPIO_PIN(GPIO_InitStruct->GPIO_Pin)); …… assert_param(IS_GPIO_SPEED(GPIO_InitStruct->GPIO_Speed)); …… } 顾名思义,assert_param 函数式对入口参数的有效性进行判断,所以我们可以从这个函数入手, 确 定 我 们 的 入 口 参 数 的 范 围 。 第 一 行 是 对 第 一 个 参 数 GPIOx 进 行 有 效 性 判 断 , 双 击 “IS_GPIO_ALL_PERIPH”右键点击“go to defition of…” 定位到了下面的定义: #define IS_GPIO_ALL_PERIPH(PERIPH) (((PERIPH) == GPIOA) || \ ((PERIPH) == GPIOB) || \ ((PERIPH) == GPIOC) || \ ((PERIPH) == GPIOD) || \ ((PERIPH) == GPIOE) || \ ((PERIPH) == GPIOF) || \ ((PERIPH) == GPIOG)) 很明显可以看出,GPIOx 的取值规定只允许是 GPIOA~GPIOG。 同样的办法,我们双击“IS_GPIO_MODE” 右键点击“go to defition of…”,定位到下面的定义: typedef enum { GPIO_Mode_AIN = 0x0, GPIO_Mode_IN_FLOATING = 0x04, GPIO_Mode_IPD = 0x28, GPIO_Mode_IPU = 0x48, GPIO_Mode_Out_OD = 0x14, GPIO_Mode_Out_PP = 0x10, GPIO_Mode_AF_OD = 0x1C, GPIO_Mode_AF_PP = 0x18 }GPIOMode_TypeDef; #define IS_GPIO_MODE(MODE) (((MODE) == GPIO_Mode_AIN) || \ ((MODE) == GPIO_Mode_IN_FLOATING) || \ ((MODE) == GPIO_Mode_IPD) || \ ((MODE) == GPIO_Mode_IPU) || \ ((MODE) == GPIO_Mode_Out_OD) || \ ((MODE) == GPIO_Mode_Out_PP) || \ ((MODE) == GPIO_Mode_AF_OD) || \ ((MODE) == GPIO_Mode_AF_PP)) 135 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 所以 GPIO_InitStruct->GPIO_Mode 成员的取值范围只能是上面定义的 8 种。这 8 中模式是通过 一个枚举类型组织在一起的。 同样的方法可以找出 GPIO_Speed 的参数限制: typedef enum { GPIO_Speed_10MHz = 1, GPIO_Speed_2MHz, GPIO_Speed_50MHz }GPIOSpeed_TypeDef; #define IS_GPIO_SPEED(SPEED) (((SPEED) == GPIO_Speed_10MHz) || \ ((SPEED) == GPIO_Speed_2MHz) || \ ((SPEED) == GPIO_Speed_50MHz)) 同样的方法我们双击“IS_GPIO_PIN” 右键点击“go to defition of…”,定位到下面的定义: #define IS_GPIO_PIN(PIN) ((((PIN) & (uint16_t)0x00) == 0x00) && ((PIN) != (uint16_t)0x00)) 可以看出,GPIO_Pin 成员变量的取值范围为 0x0000 到 0xffff,那么是不是我们写代码初始化就 是直接给一个 16 位的数字呢?这也是可以的,但是大多数情况下,MDK 不会让你直接在入口 参数处设置一个简单的数字,因为这样代码的可读性太差,MDK 会将这些数字的意思通过宏 定义定义出来,这样可读性大大增强。我们可以看到在 IS_GPIO_PIN(PIN)宏定义的上面还有数 行宏定义: #define GPIO_Pin_0 #define GPIO_Pin_1 #define GPIO_Pin_2 #define GPIO_Pin_3 #define GPIO_Pin_4 …… #define GPIO_Pin_14 #define GPIO_Pin_15 #define GPIO_Pin_All ((uint16_t)0x0001) ((uint16_t)0x0002) ((uint16_t)0x0004) ((uint16_t)0x0008) ((uint16_t)0x0010) /*!< Pin 0 selected */ /*!< Pin 1 selected */ /*!< Pin 2 selected */ /*!< Pin 3 selected */ /*!< Pin 4 selected */ ((uint16_t)0x4000) /*!< Pin 14 selected */ ((uint16_t)0x8000) /*!< Pin 15 selected */ ((uint16_t)0xFFFF) /*!< All pins selected */ #define IS_GPIO_PIN(PIN) ((((PIN) & (uint16_t)0x00) == 0x00) && ((PIN) != (uint16_t)0x00)) 这些宏定义 GPIO_Pin_0~GPIO_Pin_ All 就是 MDK 事先定义好的,我们写代码的时候初始化 GPIO_Pin 的时候入口参数可以是这些宏定义。对于这种情况,MDK 一般把取值范围的宏定义 放在判断有效性语句的上方,这样是为了方便大家查找。 讲到这里,我们基本对 GPIO_Init 的入口参数有比较详细的了解了。于是我们可以组织起 来下面的代码: GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; // GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 136 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); 接着又有一个问题会被提出来,这个初始化函数一次只能初始化一个 IO 口吗?我要同时 初始化很多个 IO 口,是不是要复制很多次这样的初始化代码呢? 这里又有一个小技巧了。从上面的 GPIO_Pin_x 的宏定义我们可以看出,这些值是 0,1,2,4 这样的数字,所以每个 IO 口选定都是对应着一个位,16 位的数据一共对应 16 个 IO 口。这个 位为 0 那么这个对应的 IO 口不选定,这个位为 1 对应的 IO 口选定。如果多个 IO 口,他们都 是对应同一个 GPIOx,那么我们可以通过|(或)的方式同时初始化多个 IO 口。这样操作的前 提是,他们的 Mode 和 Speed 参数相同,因为 Mode 和 Speed 参数并不能一次定义多种。所以 初始化多个 IO 口的方式可以是如下: GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5| GPIO_Pin_6| GPIO_Pin_7; //指定端口 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //端口模式:推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度 GPIO_Init(GPIOB, &GPIO_InitStructure); //初始化 对于那些参数可以通过|(或)的方式连接,这既有章可循,同时也靠大家在开发过程中不断积累。 有客户经常问到,我每次使能时钟的时候都要去查看时钟树看那些外设是挂载在那个总线 之下的,这好麻烦。学到这里我相信大家就可以很快速的解决这个问题了。 在 stm32f10x.h 文件里面我们可以看到如下的宏定义: #define RCC_APB2Periph_GPIOA ((uint32_t)0x00000004) #define RCC_APB2Periph_GPIOB ((uint32_t)0x00000008) #define RCC_APB2Periph_GPIOC ((uint32_t)0x00000010) #define RCC_APB1Periph_TIM2 #define RCC_APB1Periph_TIM3 #define RCC_APB1Periph_TIM4 ((uint32_t)0x00000001) ((uint32_t)0x00000002) ((uint32_t)0x00000004) #define RCC_AHBPeriph_DMA1 ((uint32_t)0x00000001) #define RCC_AHBPeriph_DMA2 ((uint32_t)0x00000002) 从上图可以很明显的看出 GPIOA~GPIOC 是挂载在 APB2 下面,TIM2~TIM4 是挂载在 APB1 下 面 , DMA 是 挂 载 在 AHB 下 面 。 所 以 在 使 能 DMA 的 时 候 记 住 要 调 用 的 是 RCC_AHBPeriphClock()函数使能,在使能 GPIO 的时候调用的是 RCC_APB2PeriphResetCmd() 函数使能。 大家会觉得上面讲解有点麻烦,每次要去查找 assert_param()这个函数去寻找,那么有没有 更好的办法呢?大家可以打开 GPIO_InitTypeDef 结构体定义: typedef struct { uint16_t GPIO_Pin; /*!< Specifies the GPIO pins to be configured. This parameter can be any value of @ref GPIO_pins_define */ GPIOSpeed_TypeDef GPIO_Speed; /*!< Specifies the speed for the selected pins. 137 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 This parameter can be a value of @ref GPIOSpeed_TypeDef */ GPIOMode_TypeDef GPIO_Mode; /*!< Specifies the operating mode for the selected pins.This parameter can be a value of @ref GPIOMode_TypeDef */ }GPIO_InitTypeDef; 从上图的结构体成员后面的注释我们可以看出 GPIO_Mode 的意思是 “Specifies the operating mode for the selected pins.This parameter can be a value of @ref GPIOMode_TypeDef”。从这段注释可以看出 GPIO_Mode 的取值为 GPIOMode_TypeDef 枚举类 型的枚举值,大家同样可以用之前讲解的方法右键双击“GPIOMode_TypeDef”选择“Go to definition of …”即可查看其取值范围。如果要确定详细的信息呢我们就得去查看手册了。对于 去查看手册的哪个地方,你可以在函数 GPIO_Init()的函数体中搜索 GPIO_Mode 关键字,然后 查看库函数设置 GPIO_Mode 是设置的哪个寄存器的哪个位,然后去中文参考手册查看该寄存 器相应位的定义以及前后文的描述。 这一节我们就讲解到这里,希望能对大家的开发有帮助。 138 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第五章 SYSTEM 文件夹介绍 上一章,我们介绍了如何在 MDK5.14 下建立 STM32F1 工程,在这个新建的工程之中,我 们用到了一个 SYSTEM 文件夹里面的代码,此文件夹里面的代码由 ALIENTEK 提供,是 STM32F10x 系列的底层核心驱动函数,可以用在 STM32F10x 系列的各个型号上面,方便大家 快速构建自己的工程。 SYSTEM 文件夹下包含了 delay、sys、usart 等三个文件夹。分别包含了 delay.c、sys.c、usart.c 及其头文件。通过这 3 个 c 文件,可以快速的给任何一款 STM32F1 构建最基本的框架。使用 起来是很方便的。 本章,我们将向大家介绍这些代码,通过这章的学习,大家将了解到这些代码的由来,也 希望大家可以灵活使用 SYSTEM 文件夹提供的函数,来快速构建工程,并实际应用到自己的项 目中去。 本章包括如下 3 个小结: 5.1,delay 文件夹代码介绍; 5.2,sys 文件夹代码介绍; 5.3,usart 文件夹代码介绍; 5.1 delay 文件夹代码介绍 delay 文件夹内包含了 delay.c 和 delay.h 两个文件,这两个文件用来实现系统的延时功能, 其中包含 7 个函数: void delay_osschedlock(void); void delay_osschedunlock(void); void delay_ostimedly(u32 ticks); void SysTick_Handler(void); void delay_init(void); void delay_ms(u16 nms); void delay_us(u32 nus); 前面 4 个函数,仅在支持操作系统(OS)的时候,需要用到,而后面三个函数,则不论是 否支持 OS 都需要用到。 在介绍这些函数之前,我们先了解一下 delay 延时的编程思想:CM3 内核的处理器,内部 包含了一个 SysTick 定时器,SysTick 是一个 24 位的倒计数定时器,当计数到 0 时,将从 RELOAD 寄存器中自动重装载定时初值,开始新一轮计数。只要不把它在 SysTick 控制及状 态寄存器中的使能位清除,就永不停息。SysTick 在《STM32 中文参考手册》(这里是指 V10.0 版本,下同)里面介绍的很简单,其详细介绍,请参阅《Cortex-M3 权威指南》第 133 页。我 们就是利用 STM32 的内部 SysTick 来实现延时的,这样既不占用中断,也不占用系统定时器。 这里我们将介绍的是 ALIENTEK 提供的最新版本的延时函数,该版本的延时函数支持在任 意操作系统(OS)下面使用,它可以和操作系统共用 SysTick 定时器。 这里,我们以 UCOSII 为例,介绍如何实现操作系统和我们的 delay 函数共用 SysTick 定时 器。首先,我们简单介绍下 UCOSII 的时钟:ucos 运行需要一个系统时钟节拍(类似 “心跳”), 而这个节拍是固定的(由 OS_TICKS_PER_SEC 宏定义设置),比如要求 5ms 一次(即可设置: OS_TICKS_PER_SEC=200),在 STM32 上面,一般是由 SysTick 来提供这个节拍,也就是 SysTick 要设置为 5ms 中断一次,为 ucos 提供时钟节拍,而且这个时钟一般是不能被打断的(否则就不 准了)。 139 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 因为在 ucos 下 SysTick 不能再被随意更改,如果我们还想利用 SysTick 来做 delay_us 或者 delay_ms 的延时,就必须想点办法了,这里我们利用的是时钟摘取法。以 delay_us 为例,比如 delay_us(50),在刚进入 delay_us 的时候先计算好这段延时需要等待的 SysTick 计数次数,这里 为 50*9(假设系统时钟为 72Mhz,那么 SysTick 每增加 1,就是 1/9us),然后我们就一直统计 SysTick 的计数变化,直到这个值变化了 50*9,一旦检测到变化达到或者超过这个值,就说明 延时 50us 时间到了。这样,我们只是抓取 SysTick 计数器的变化,并不需要修改 SysTick 的任 何状态,完全不影响 SysTick 作为 UCOS 时钟节拍的功能,这就是实现 delay 和操作系统共用 SysTick 定时器的原理。 下面我们开始介绍这几个函数。 5.1.1 操作系统支持宏定义及相关函数 当需要 delay_ms 和 delay_us 支持操作系统(OS)的时候,我们需要用到 3 个宏定义和 4 个函数,宏定义及函数代码如下: //本例程仅作 UCOSII 和 UCOSIII 的支持,其他 OS,请自行参考着移植 //支持 UCOSII #ifdef OS_CRITICAL_METHOD //OS_CRITICAL_METHOD 定义了,说明要支持 UCOSII #define delay_osrunning OSRunning //OS 是否运行标记,0,不运行;1,在运行 #define delay_ostickspersec OS_TICKS_PER_SEC //OS 时钟节拍,即每秒调度次数 #define delay_osintnesting OSIntNesting //中断嵌套级别,即中断嵌套次数 #endif //支持 UCOSIII #ifdef CPU_CFG_CRITICAL_METHOD //CPU_CFG_CRITICAL_METHOD 定义了,说明要支持 UCOSIII #define delay_osrunning OSRunning //OS 是否运行标记,0,不运行;1,在运行 #define delay_tickspersec OSCfg_TickRate_Hz //OS 时钟节拍,即每秒调度次数 #define delay_intnesting OSIntNestingCtr //中断嵌套级别,即中断嵌套次数 #endif //us 级延时时,关闭任务调度(防止打断 us 级延迟) void delay_osschedlock(void) { #ifdef CPU_CFG_CRITICAL_METHOD //使用 UCOSIII OS_ERR err; OSSchedLock(&err); #else //UCOSIII 的方式,禁止调度,防止打断 us 延时 //否则 UCOSII OSSchedLock(); //UCOSII 的方式,禁止调度,防止打断 us 延时 #endif } //us 级延时时,恢复任务调度 void delay_osschedunlock(void) { 140 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 #ifdef CPU_CFG_CRITICAL_METHOD //使用 UCOSIII OS_ERR err; OSSchedUnlock(&err); #else OSSchedUnlock(); //UCOSIII 的方式,恢复调度 //否则 UCOSII //UCOSII 的方式,恢复调度 #endif } //调用 OS 自带的延时函数延时 //ticks:延时的节拍数 void delay_ostimedly(u32 ticks) { #ifdef CPU_CFG_CRITICAL_METHOD //使用 UCOSIII 时 OS_ERR err; OSTimeDly(ticks,OS_OPT_TIME_PERIODIC,&err);//UCOSIII 延时采用周期模式 #else OSTimeDly(ticks); //UCOSII 延时 #endif } //systick 中断服务函数,使用 ucos 时用到 void SysTick_Handler(void) { if(delay_osrunning==1) //OS 开始跑了,才执行正常的调度处理 { OSIntEnter(); OSTimeTick(); OSIntExit(); //进入中断 //调用 ucos 的时钟服务程序 //触发任务切换软中断 } } 以上代码,仅支持 UCOSII 和 UCOSIII,不过,对于其他 OS 的支持,也只需要对以上代 码进行简单修改即可实现。 支持 OS 需要用到的三个宏定义(以 UCOSII 为例)即: #define delay_osrunning OSRunning //OS 是否运行标记,0,不运行;1,在运行 #define delay_ostickspersec OS_TICKS_PER_SEC //OS 时钟节拍,即每秒调度次数 #define delay_osintnesting OSIntNesting //中断嵌套级别,即中断嵌套次数 宏定义:delay_osrunning,用于标记 OS 是否正在运行,当 OS 已经开始运行时,该宏定义 值为 1,当 OS 还未运行时,该宏定义值为 0。 宏定义:delay_ ostickspersec,用于表示 OS 的时钟节拍,即 OS 每秒钟任务调度次数。 宏定义:delay_ osintnesting,用于表示 OS 中断嵌套级别,即中断嵌套次数,每进入一个 中断,该值加 1,每退出一个中断,该值减 1。 支持 OS 需要用到的 4 个函数,即: 函数:delay_osschedlock,用于 delay_us 延时,作用是禁止 OS 进行调度,以防打断 us 级 延时,导致延时时间不准。 函数:delay_osschedunlock,同样用于 delay_us 延时,作用是在延时结束后恢复 OS 的调度, 141 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 继续正常的 OS 任务调度。 函数:delay_ostimedly,则是调用 OS 自带的延时函数,实现延时。该函数的参数为时钟节 拍数。 函数:SysTick_Handler,则是 systick 的中断服务函数,该函数为 OS 提供时钟节拍,同时 可以引起任务调度。 以上就是 delay_ms 和 delay_us 支持操作系统时,需要实现的 3 个宏定义和 4 个函数。 5.1.2delay_init 函数 该函数用来初始化 2 个重要参数:fac_us 以及 fac_ms;同时把 SysTick 的时钟源选择为外部时 钟,如果需要支持操作系统(OS),只需要在 sys.h 里面,设置 SYSTEM_SUPPORT_OS 宏的值 为 1 即可,然后,该函数会根据 delay_ostickspersec 宏的设置,来配置 SysTick 的中断时间,并 开启 SysTick 中断。具体代码如下: //初始化延迟函数 //当使用 OS 的时候,此函数会初始化 OS 的时钟节拍 //SYSTICK 的时钟固定为 HCLK 时钟的 1/8 void delay_init() { #if SYSTEM_SUPPORT_OS //如果需要支持 OS. u32 reload; #endif SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); //选择外部时钟 HCLK/8 fac_us=SystemCoreClock/8000000; //为系统时钟的 1/8 #if SYSTEM_SUPPORT_OS //如果需要支持 OS. reload=SystemCoreClock/8000000; //每秒钟的计数次数 单位为 K reload*=1000000/delay_ostickspersec; //根据 delay_tickspersec 设定溢出时间 //reload 为 24 位寄存器,最大值:16777216,在 72M 下,约合 1.86s 左右 fac_ms=1000/delay_ostickspersec; //代表 OS 可以延时的最少单位 SysTick->CTRL|=SysTick_CTRL_TICKINT_Msk; //开启 SYSTICK 中断 SysTick->LOAD=reload; //每 1/os_delay_tickspersec 秒中断一次 SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk; //开启 SYSTICK #else fac_ms=(u16)fac_us*1000; //非 OS 下,代表每个 ms 需要的 systick 时钟数 #endif } 可以看到,delay_init 函数使用了条件编译,来选择不同的初始化过程,如果不使用 OS 的 时候,只是设置一下 SysTick 的时钟源以及确定 fac_us 和 fac_ms 的值。而如果使用 OS 的时候, 则会进行一些不同的配置,这里的条件编译是根据 SYSTEM_SUPPORT_OS 这个宏来确定的, 该宏在 sys.h 里面定义。 SysTick 是 MDK 定义了的一个结构体(在 core_m3.h 里面),里面包含 CTRL、LOAD、VAL、 CALIB 等 4 个寄存器, SysTick->CTRL 的各位定义如图 5.1.2.1 所示: 142 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 5.1.2.1 SysTick->CTRL 寄存器各位定义 SysTick-> LOAD 的定义如图 5.1.2.2 所示: 图 5.1.2.2 SysTick->LOAD 寄存器各位定义 SysTick-> VAL 的定义如图 5.1.2.3 所示: 图 5.1.2.3 SysTick->VAL 寄存器各位定义 SysTick-> CALIB 不常用,在这里我们也用不到,故不介绍了。 SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);这一句把 SysTick 的时钟选择 外部时钟,这里需要注意的是:SysTick 的时钟源自 HCLK 的 8 分频,假设我们外部晶振为 8M, 然后倍频到 72M,那么 SysTick 的时钟即为 9Mhz,也就是 SysTick 的计数器 VAL 每减 1,就代 表时间过了 1/9us。所以 fac_us=SystemCoreClock/8000000;这句话就是计算在 SystemCoreClock 时钟频率下延时 1us 需要多少个 SysTick 时钟周期。同理,fac_ms=(u16)fac_us*1000;就是计算 延时 1ms 需要多少个 SysTick 时钟周期,它自然是 1us 的 1000 倍。初始化将计算出 fac_us 和 fac_ms 的值。 在不使用 OS 的时候:fac_us,为 us 延时的基数,也就是延时 1us,SysTick->LOAD 所应 设置的值。fac_ms 为 ms 延时的基数,也就是延时 1ms,SysTick->LOAD 所应设置的值。fac_us 为 8 位整形数据,fac_ms 为 16 位整形数据。Systick 的时钟来自系统时钟 8 分频,正因为如此, 系统时钟如果不是 8 的倍数(不能被 8 整除),则会导致延时函数不准确,这也是我们推荐外部 时钟选择 8M 的原因。这点大家要特别留意。 当使用 OS 的时候,fac_us,还是 us 延时的基数,不过这个值不会被写到 SysTick->LOAD 寄存器来实现延时,而是通过时钟摘取的办法实现的(前面已经介绍了)。而 fac_ms 则代表 ucos 自带的延时函数所能实现的最小延时时间(如 delay_ostickspersec=200,那么 fac_ms 就是 5ms)。 5.1.3 delay_us 函数 该函数用来延时指定的 us,其参数 nus 为要延时的微秒数。该函数有使用 OS 和不使用 OS 两个版本,这里我们分别介绍,首先是不使用 OS 的时候,实现函数如下: //延时 nus 143 STM32F1 开发指南(库函数版) //nus 为要延时的 us 数. ALIENTEK 战舰 STM32F103 V3 开发板教程 void delay_us(u32 nus) { u32 temp; SysTick->LOAD=nus*fac_us; //时间加载 SysTick->VAL=0x00; //清空计数器 SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ; //开始倒数 do { temp=SysTick->CTRL; }while((temp&0x01)&&!(temp&(1<<16)));//等待时间到达 SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk; //关闭计数器 SysTick->VAL =0X00; //清空计数器 } 有了上面对 SysTick 寄存器的描述,这段代码不难理解。其实就是先把要延时的 us 数换算 成 SysTick 的时钟数,然后写入 LOAD 寄存器。然后清空当前寄存器 VAL 的内容,再开启倒数 功能。等到倒数结束,即延时了 nus。最后关闭 SysTick,清空 VAL 的值。实现一次延时 nus 的操作,但是这里要注意 nus 的值,不能太大,必须保证 nus<=(2^24)/fac_us,否则将导致 延时时间不准确。这里特别说明一下:temp&0x01,这一句是用来判断 systick 定时器是否还处 于开启状态,可以防止 systick 被意外关闭导致的死循环。 再来看看使用 OS 的时候,delay_us 的实现函数如下: //延时 nus //nus 为要延时的 us 数. void delay_us(u32 nus) { u32 ticks; u32 told,tnow,tcnt=0; u32 reload=SysTick->LOAD; ticks=nus*fac_us; delay_osschedlock(); told=SysTick->VAL; //LOAD 的值 //需要的节拍数 //阻止 OS 调度,防止打断 us 延时 //刚进入时的计数器值 while(1) { tnow=SysTick->VAL; if(tnow!=told) { if(tnow=ticks)break; //时间超过/等于要延迟的时间,则退出. } }; delay_osschedunlock(); //恢复 OS 调度 144 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 } 这里就正是利用了我们前面提到的时钟摘取法,ticks 是延时 nus 需要等待的 SysTick 计数 次数(也就是延时时间),told 用于记录最近一次的 SysTick->VAL 值,然后 tnow 则是当前的 SysTick->VAL 值,通过他们的对比累加,实现 SysTick 计数次数的统计,统计值存放在 tcnt 里 面,然后通过对比 tcnt 和 ticks,来判断延时是否到达,从而达到不修改 SysTick 实现 nus 的延 时,从而可以和 OS 共用一个 SysTick。 上面的 delay_osschedlock 和 delay_osschedunlock 是 OS 提供的两个函数,用于调度上锁和 解锁,这里为了防止 OS 在 delay_us 的时候打断延时,可能导致的延时不准,所以我们利用这 两个函数来实现免打断,从而保证延时精度!同时,此时的 delay_us,可以实现最长 2^32us 的 延时,大概是 4294 秒。 5.1.4 delay_ms 函数 该函数用来延时指定的 ms,其参数 nms 为要延时的毫秒数。该函数同样有使用 OS 和不使 用 OS 两个版本,这里我们分别介绍,首先是不使用 OS 的时候,实现函数如下: //延时 nms //注意 nms 的范围 //SysTick->LOAD 为 24 位寄存器,所以,最大延时为: //nms<=0xffffff*8*1000/SYSCLK //SYSCLK 单位为 Hz,nms 单位为 ms //对 72M 条件下,nms<=1864 void delay_ms(u16 nms) { u32 temp; SysTick->LOAD=(u32)nms*fac_ms;//时间加载(SysTick->LOAD 为 24bit) SysTick->VAL =0x00; //清空计数器 SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ; //开始倒数 do { temp=SysTick->CTRL; }while((temp&0x01)&&!(temp&(1<<16)));//等待时间到达 SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk; //关闭计数器 SysTick->VAL =0X00; //清空计数器 } 此部分代码和 5.1.3 节的 delay_us(非 OS 版本)大致一样,但是要注意因为 LOAD 仅仅是 一个 24bit 的寄存器,延时的 ms 数不能太长。否则超出了 LOAD 的范围,高位会被舍去,导致 延时不准。最大延迟 ms 数可以通过公式:nms<=0xffffff*8*1000/SYSCLK 计算。SYSCLK 单 位为 Hz,nms 的单位为 ms。如果时钟为 72M,那么 nms 的最大值为 1864ms。超过这个值,建 议通过多次调用 delay_ms 实现,否则就会导致延时不准确。 再来看看使用 OS 的时候,delay_ms 的实现函数如下: //延时 nms //nms:要延时的 ms 数 void delay_ms(u16 nms) { 145 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 if(delay_osrunning&&delay_osintnesting==0) //如果 OS 已经在跑了,并且不是在中断里面(中断里面不能任务调度) { if(nms>=fac_ms) //延时的时间大于 OS 的最少时间周期 { delay_ostimedly(nms/fac_ms);//OS 延时 } nms%=fac_ms; //OS 已经无法提供这么小的延时了,采用普通方式延时 } delay_us((u32)(nms*1000)); //普通方式延时 } 该函数中,delay_osrunning 是 OS 正在运行的标志,delay_osintnesting 则是 OS 中断嵌套次 数,必须 delay_osrunning 为真,且 delay_osintnesting 为 0 的时候,才可以调用 OS 自带的延时 函数进行延时(可以进行任务调度),delay_ostimedly 函数就是利用 OS 自带的延时函数,实现 任 务 级 延 时 的 , 其 参 数 代 表 延 时 的 时 钟 节 拍 数 ( 假 设 delay_ostickspersec=200 , 那 么 delay_ostimedly (1),就代表延时 5ms)。 当 OS 还未运行的时候,我们的 delay_ms 就是直接由 delay_us 实现的,OS 下的 delay_us 可以实现很长的延时而不溢出!,所以放心的使用 delay_us 来实现 delay_ms,不过由于 delay_us 的时候,任务调度被上锁了,所以还是建议不要用 delay_us 来延时很长的时间,否则影响整个 系统的性能。 当 OS 运行的时候,我们的 delay_ms 函数将先判断延时时长是否大于等于 1 个 OS 时钟节 拍(fac_ms),当大于这个值的时候,我们就通过调用 OS 的延时函数来实现(此时任务可以调 度),不足 1 个时钟节拍的时候,直接调用 delay_us 函数实现(此时任务无法调度)。 5.2 sys 文件夹代码介绍 sys 文件夹内包含了 sys.c 和 sys.h 两个文件。在 sys.h 里面定义了 STM32 的 IO 口输入读取 宏定义和输出宏定义。sys.c 里面只定义了一个中断分组函数。下面我们将分别向大家介绍。 5.2.1 IO 口的位操作实现 该部分代码在 sys.h 文件中,实现对 STM32 各个 IO 口的位操作,包括读入和输出。当然 在这些函数调用之前,必须先进行 IO 口时钟的使能和 IO 口功能定义。此部分仅仅对 IO 口进 行输入输出读取和控制。 位带操作简单的说,就是把每个比特膨胀为一个 32 位的字,当访问这些字的时候就达到了 访问比特的目的,比如说 BSRR 寄存器有 32 个位,那么可以映射到 32 个地址上,我们去访问 这 32 个地址就达到访问 32 个比特的目的。这样我们往某个地址写 1 就达到往对应比特位写 1 的目的,同样往某个地址写 0 就达到往对应的比特位写 0 的目的。 146 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 寄存器32个位 Bit31 Bit30 Bit29 32个对应地址 Address31 Address30 Address29 Bit2 Bit1 Bit0 Address2 Address1 Address 0 图 5.2.2.1 位带映射图 对于上图,我们往 Address0 地址写入 1,那么就可以达到往寄存器的第 0 位 Bit0 赋值 1 的 目的。这里我们不想讲得过于复杂,因为位带操作在实际开发中可能只是用来 IO 口的输入输 出还比较方便,其他操作在日常开发中也基本很少用。下面我们看看 sys.h 中位带操作的定义。 代码如下: #define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+(( addr &0xFFFFF)<<5)+(bitnum<<2)) #define MEM_ADDR(addr) *((volatile unsigned long *)(addr)) #define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum)) //IO 口地址映射 #define GPIOA_ODR_Addr (GPIOA_BASE+12) //0x4001080C #define GPIOB_ODR_Addr (GPIOB_BASE+12) //0x40010C0C #define GPIOC_ODR_Addr (GPIOC_BASE+12) //0x4001100C #define GPIOD_ODR_Addr (GPIOD_BASE+12) //0x4001140C #define GPIOE_ODR_Addr (GPIOE_BASE+12) //0x4001180C #define GPIOF_ODR_Addr (GPIOF_BASE+12) //0x40011A0C #define GPIOG_ODR_Addr (GPIOG_BASE+12) //0x40011E0C #define GPIOA_IDR_Addr (GPIOA_BASE+8) //0x40010808 #define GPIOB_IDR_Addr (GPIOB_BASE+8) //0x40010C08 #define GPIOC_IDR_Addr (GPIOC_BASE+8) //0x40011008 #define GPIOD_IDR_Addr (GPIOD_BASE+8) //0x40011408 #define GPIOE_IDR_Addr (GPIOE_BASE+8) //0x40011808 #define GPIOF_IDR_Addr (GPIOF_BASE+8) //0x40011A08 #define GPIOG_IDR_Addr (GPIOG_BASE+8) //0x40011E08 //IO 口操作,只对单一的 IO 口! //确保 n 的值小于 16! #define PAout(n) BIT_ADDR(GPIOA_ODR_Addr,n) //输出 #define PAin(n) BIT_ADDR(GPIOA_IDR_Addr,n) //输入 #define PBout(n) #define PBin(n) #define PCout(n) BIT_ADDR(GPIOB_ODR_Addr,n) //输出 BIT_ADDR(GPIOB_IDR_Addr,n) //输入 BIT_ADDR(GPIOC_ODR_Addr,n) //输出 147 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 #define PCin(n) BIT_ADDR(GPIOC_IDR_Addr,n) //输入 #define PDout(n) BIT_ADDR(GPIOD_ODR_Addr,n) //输出 #define PDin(n) BIT_ADDR(GPIOD_IDR_Addr,n) //输入 #define PEout(n) BIT_ADDR(GPIOE_ODR_Addr,n) //输出 #define PEin(n) BIT_ADDR(GPIOE_IDR_Addr,n) //输入 #define PFout(n) BIT_ADDR(GPIOF_ODR_Addr,n) //输出 #define PFin(n) BIT_ADDR(GPIOF_IDR_Addr,n) //输入 #define PGout(n) BIT_ADDR(GPIOG_ODR_Addr,n) //输出 #define PGin(n) BIT_ADDR(GPIOG_IDR_Addr,n) //输入 以上代码的便是 GPIO 位带操作的具体实现,位带操作的详细说明,在权威指南中有详细 讲解,请参考<>第五章(87 页~92 页)。比如说,我们调用 PAout(1)=1 是设置了 GPIOA 的第一个管脚 GPIOA.1 为 1,实际是设置了寄存器 ODR 的某个位,但是我们的定义中 可以跟踪过去看到却是通过计算访问了一个地址。上面一系列公式也就是计算 GPIO 的某个 io 口对应的位带区的地址了。 有了上面的代码,我们就可以像 51/AVR 一样操作 STM32 的 IO 口了。比如,我要 PORTA 的第七个 IO 口输出 1,则可以使用 PAout(6)=1;即可实现。我要判断 PORTA 的第 15 个位 是否等于 1,则可以使用 if(PAin(14)==1)…;就可以了。 这里顺便说一下,在 sys.h 中的还有个全局宏定义: //0,不支持 ucos 1,支持 ucos #define SYSTEM_SUPPORT_UCOS 0 //定义系统文件夹是否支持 UCOS SYSTEM_SUPPORT_UCOS,这个宏定义用来定义 SYSTEM 文件夹是否支持 ucos,如果在 ucos 下面使用 SYSTEM 文件夹,那么设置这个值为 1 即可,否则设置为 0(默认)。 5.3 usart 文件夹介绍 usart 文件夹内包含了 usart.c 和 usart.h 两个文件。这两个文件用于串口的初始化和中断接 收。这里只是针对串口 1,比如你要用串口 2 或者其他的串口,只要对代码稍作修改就可以了。 usart.c 里面包含了 2 个函数一个是 void USART1_IRQHandler(void);另外一个是 void uart_init(u32 bound);里面还有一段对串口 printf 的支持代码,如果去掉,则会导致 printf 无法使用,虽然软 件编译不会报错,但是硬件上 STM32 是无法启动的,这段代码不要去修改。 5.3.1 printf 函数支持 这段引入 printf 函数支持的代码在 usart.h 头文件的最上方,这段代码加入之后便可以通过 printf 函数向串口发送我们需要的内容,方便开发过程中查看代码执行情况以及一些变量值。这 段代码不需要修改,引入到 usart.h 即可。 这段代码为: //加入以下代码,支持 printf 函数,而不需要选择 use MicroLIB #if 1 #pragma import(__use_no_semihosting) //标准库需要的支持函数 struct __FILE { int handle; }; 148 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 FILE __stdout; //定义_sys_exit()以避免使用半主机模式 _sys_exit(int x) { x = x; } //重定义 fputc 函数 int fputc(int ch, FILE *f) { while(USART_GetFlagStatus(USART1,USART_FLAG_TC)==RESET); USART_SendData(USART1,(uint8_t)ch); return ch; } #endif 5.3.2 uart_init 函数 void uart_init(u32 pclk2,u32 bound)函数是串口 1 初始化函数。该函数有 1 个参数为波特率, 波特率这个参数对于大家来说应该不陌生,这里就不多说了。 void uart_init(u32 bound){ //GPIO 端口设置 GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA , ENABLE); //使能 USART1,GPIOA 时钟 //USART1_TX PA.9 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9 复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出 GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 GPIOA.0 发送端 //USART1_RX PA.10 浮空输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入 GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 GPIOA.10 接收端 //Usart1 NVIC 中断配置 配置 NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //对应中断通道 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级 3 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //子优先级 3 149 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道使能 NVIC_Init(&NVIC_InitStructure); //中断优先级配置 //USART 初始化设置 USART_InitStructure.USART_BaudRate = bound;//波特率设置; USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为 8 位 USART_InitStructure.USART_StopBits = USART_StopBits_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_Init(USART1, &USART_InitStructure); //初始化串口 USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启中断 USART_Cmd(USART1, ENABLE); //使能串口 } 下面我们一一分析一下这段初始化代码。首先是一行时钟使能代码: RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1| RCC_APB2Periph_GPIOA, ENABLE); //使能 USART1,GPIOA 时钟 这个时钟使能我们在端口复用的时候已经讲解过,大家可以翻到端口复用那一章节,有详细的 讲解。在使用一个内置外设的时候,我们首先要使能相应的 GPIO 时钟,然后使能复用功能时 钟和内置外设时钟。 接下来我们要初始化相应的 GPIO 端口为特定的状态,我们在复用内置外设的时候到底 GPIO 要设置成什么模式呢?这个在我们的端口复用一节也有讲解,那就是在《STM32 中文参 考手册 V10》的 P110“8.1.11 外设的 GPIO 配置”中有讲解,我们就继续截图下来: 所以接下来的两段代码就是将 TX(PA9)设置为推挽复用输出模式,将 RX(PA10)设置为浮空输入 模式: //USART1_TX PA.9 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9 复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出 GPIO_Init(GPIOA, &GPIO_InitStructure); //USART1_RX PA.10 浮空输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入 GPIO_Init(GPIOA, &GPIO_InitStructure); 对于 GPIO 的知识我们在跑马灯实例会讲解到,这里暂时不做深入的讲解。 150 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 紧接着,我们要进行 usart1 的中断初始化,设置抢占优先级值和子优先级的值: //Usart1 NVIC 中断配置 配置 NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级 3 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //子优先级 3 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道使能 NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化 VIC 寄存器 这段代码在我们的中断管理函数章节 4.5 有讲解中断管理相关的知识,大家可以翻阅一下。 在设置完中断优先级之后,接下来我们要设置串口 1 的初始化参数: //USART 初始化设置 USART_InitStructure.USART_BaudRate = bound;//波特率设置; USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为 8 位 USART_InitStructure.USART_StopBits = USART_StopBits_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_Init(USART1, &USART_InitStructure); //初始化串口 从上面的源码我们可以看出,串口的初始化是通过调用 USART_Init()函数实现,而这个函 数重要的参数就是就是结构体指针变量 USART_InitStructure,下面我们看看结构体定义: typedef struct { uint32_t USART_BaudRate; uint16_t USART_WordLength; uint16_t USART_StopBits; uint16_t USART_Parity; uint16_t USART_Mode; uint16_t USART_HardwareFlowControl; } USART_InitTypeDef; 这个结构体有 6 个成员变量,所以我们有 6 个参数需要初始化。 第一个参数 USART_BaudRate 为串口波特率,波特率可以说是串口最重要的参数了,我们 这里通过初始化传入参数 baund 来设定。第二个参数 USART_WordLength 为字长,这里我们设 置为 8 位字长数据格式。第三个参数 USART_StopBits 为停止位设置,我们设置为 1 位停止位。 第四个参数 USART_Parity 设定是否需要奇偶校验,我们设定为无奇偶校验位。第五个参数 USART_Mode 为串口模式,我们设置为全双工收发模式。第六个参数为是否支持硬件流控制, 我们设置为无硬件流控制。 在设置完成串口中断优先级以及串口初始化之后,接下来就是开启串口中断以及使能串口 了: USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启中断 USART_Cmd(USART1, ENABLE); //使能串口 在开启串口中断和使能串口之后接下来就是写中断处理函数了,下面一节我们将着重讲解 中断处理函数。 151 5.3.3 USART1_IRQHandler 函数 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 void USART1_IRQHandler(void)函数是串口 1 的中断响应函数,当串口 1 发生了相应的中 断后,就会跳到该函数执行。中断相应函数的名字是不能随便定义的,一般我们都遵循 MDK 定义的函数名。这些函数名字在启动文件 startup_stm32f10x_hd.s 文件中可以找到。 函数体里面通过函数: if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) 判断是否接受中断,如果是串口接受中断,则读取串口接受到的数据: Res =USART_ReceiveData(USART1);//(USART1->DR); //读取接收到的数据 读到数据后接下来就对数据进行分析。 这里我们设计了一个小小的接收协议:通过这个函数,配合一个数组 USART_RX_BUF[], 一个接收状态寄存器 USART_RX_STA(此寄存器其实就是一个全局变量,由作者自行添加。 由于它起到类似寄存器的功能,这里暂且称之为寄存器)实现对串口数据的接收管理。 USART_RX_BUF 的大小由 USART_REC_LEN 定义,也就是一次接收的数据最大不能超过 USART_REC_LEN 个字节。USART_RX_STA 是一个接收状态寄存器其各的定义如表 5.3.1.1 所 示: USART_RX_STA bit15 接收完成 标志 bit14 接收到 0X0D 标志 bit13~0 接收到的有效数据个数 表 5.3.1.1 接收状态寄存器位定义表 设计思路如下: 当接收到从电脑发过来的数据,把接收到的数据保存在 USART_RX_BUF 中,同时在接收 状态寄存器(USART_RX_STA)中计数接收到的有效数据个数,当收到回车(回车的表示由 2 个字节组成:0X0D 和 0X0A)的第一个字节 0X0D 时,计数器将不再增加,等待 0X0A 的到来, 而如果 0X0A 没有来到,则认为这次接收失败,重新开始下一次接收。如果顺利接收到 0X0A, 则标记 USART_RX_STA 的第 15 位,这样完成一次接收,并等待该位被其他程序清除,从而开 始下一次的接收,而如果迟迟没有收到 0X0D,那么在接收数据超过 USART_REC_LEN 的时候, 则会丢弃前面的数据,重新接收。中断相应函数代码如下: void USART1_IRQHandler(void) //串口 1 中断服务程序 { u8 Res; #ifdef OS_TICKS_PER_SEC //如果时钟节拍数定义了,说明要使用 ucosII 了. OSIntEnter(); #endif if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断(接收到的数据必须是 0x0d 0x0a 结尾) { Res =USART_ReceiveData(USART1);//(USART1->DR); //读取接收到的数据 if((USART_RX_STA&0x8000)==0)//接收未完成 { if(USART_RX_STA&0x4000)//接收到了 0x0d 152 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 { if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始 else USART_RX_STA|=0x8000; //接收完成了 } else //还没收到 0X0D { if(Res==0x0d)USART_RX_STA|=0x4000; else { USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ; USART_RX_STA++; if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0; //接收数据错误,重新开始接收 } } } } #ifdef OS_TICKS_PER_SEC //如果时钟节拍数定义了,说明要使用 ucosII 了. OSIntExit(); #endif } EN_USART1_RX 和 USART_REC_LEN 都是在 usart.h 文件里面定义的,当需要使用串口接 收的时候,我们只要在 usart.h 里面设置 EN_USART1_RX 为 1 就可以了。不使用的时候,设置, EN_USART1_RX 为 0 即可,这样可以省出部分 sram 和 flash,我们默认是设置 EN_USART1_RX 为 1,也就是开启串口接收的。 OS_CRITICAL_METHOD,则是用来判断是否使用 ucos,如果使用了 ucos,则调用 OSIntEnter 和 OSIntExit 函数,如果没有使用 ucos,则不调用这两个函数(这两个函数用于实现 中断嵌套处理,这里我们先不理会)。 153 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第三篇 实战篇 经过前两篇的学习,我们对 STM32 开发的软件和硬件平台都有了个比较深入的了解了, 接下来我们将通过实例,由浅入深,带大家一步步的学习 STM32。 STM32 的内部资源非常丰富,对于初学者来说,一般不知道从何开始。本篇将从 STM32 最简单的外设说起,然后一步步深入。每一个实例都配有详细的代码及解释,手把手教你如何 入手 STM32 的各种外设,通过本篇的学习,希望大家能学会 STM32 绝大部分外设的使用。 本篇总共分为 56 章,每一章即一个实例,下面就让我们开始精彩的 STM32 之旅。 我们固件库版本源码在光盘目录:程序源码\标准例程-V3.5 库函数版本\ 之下。 154 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第六章 跑马灯实验 STM32 最简单的外设莫过于 IO 口的高低电平控制了,本章将通过一个经典的跑马灯程序, 带大家开启 STM32F1 之旅,通过本章的学习,你将了解到 STM32F1 的 IO 口作为输出使用的 方法。在本章中,我们将通过代码控制 ALIENTEK 战舰 STM32 开发板上的两个 LED:DS0 和 DS1 交替闪烁,实现类似跑马灯的效果。 本章分为如下四个小节: 6.1, STM32 IO 口简介 6.2, 硬件设计 6.3, 软件设计 6.4, 仿真与下载 155 6.1 STM32 IO 简介 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 本章将要实现的是控制 ALIENTEK 战舰 STM32 开发板上的两个 LED 实现一个类似跑马灯 的效果,该实验的关键在于如何控制 STM32 的 IO 口输出。了解了 STM32 的 IO 口如何输出的, 就可以实现跑马灯了。通过这一章的学习,你将初步掌握 STM32 基本 IO 口的使用,而这是迈 向 STM32 的第一步。 这一章节因为是第一个实验章节,所以我们在这一章将讲解一些知识为后面的实验做铺垫。 为了小节标号与后面实验章节一样,这里我们不另起一节来讲。 在讲解 STM32 的 GPIO 之前,首先打开我们光盘的第一个固件库版本实验工程跑马灯实验 工程(光盘目录为“: 4,程序源码\标准例程-V3.5 库函数版本\实验 1 跑马灯/USER/LED.uvprojx”), 可以看到我们的实验工程目录: 图 6.1.1 跑马灯实验目录结构 接下来我们逐一讲解一下我们的工程目录下面的组以及重要文件。 ① 组 USER 下面存放的主要是用户代码。system_stm32f10x.c 里面主要是系统时钟初始化函 数 SystemInit 相关的定义,一般情况下文件用户不需要修改。stm32f10x_it.c 里面存放的 是部分中断服务函数,这两个文件的作用在 3.1 节有讲解,大家可以翻过去看看。main.c 156 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 函数主要存放的是主函数了,这个大家应该很清楚。 ② 组 HARDWARE 下面存放的是每个实验的外设驱动代码,他的实现是通过调用 FWLib 下面的固件库文件实现的,比如 led.c 里面调用 stm32f10x_gpio.c 里面的函数对 led 进行 初始化,这里面的函数是讲解的重点。后面的实验中可以看到会引入多个源文件。 ③ 组 SYSTEM 是 ALIENTEK 提供的共用代码,包含 Systick 延时函数,IO 口位带操作以 及串口相关函数。代码的作用和讲解在第五章都有详细讲解,大家可以翻过去看下。 ④ 组 CORE 下面存放的是固件库必须的核心文件和启动文件。这里面的文件用户不需要修 改。 ⑤ 组 FWLib 下面存放的是 ST 官方提供的外设驱动固件库文件,这些文件大家可以根据工 程需要来添加和删除。每个 stm32f10x_ppp.c 源文件对应一个 stm32f10x_ppp.h 头文件。 ⑥ README 分组主要就是添加了 README.TXT 说明文件,对实验操作进行相关说明。 最后我们讲解一下这些组之间的层次结构: 组USER main.c文件 用户代码 调用HARDWARE组下 面的设备驱动代码以 及直接操作FWLib下 面的固件库函数。 组HARDWARE xxx.c/xxx.h文件 设备初始化代码 直接操作FWLib下面 的固件库函数实 现。ALIENTEK提供。 组FWLib stm32f10x_ppp.c stm32f10x_ppp.h 固件库驱动代码 直接操作寄存器实 现。 寄存器 图 6.1.3 代码层次结构图 从层次图中可以看出,我们的用户代码和 HARDWARE 下面的外设驱动代码再不需要直接 操作寄存器,而是直接或间接操作官方提供的固件库函数。但是后面我们的为了让大家更全面 方便的了解外设,我们会增加重要的外设寄存器的讲解,这样对底层知识更加了解,方便我们 深入学习固件库。 准备内容我们就讲解到这里,接下来我们就要进入我们跑马灯实验的讲解部分了。这里需 要说明一下,我们在讲解固件库之前会首先对重要寄存器进行一个讲解,这样是为了大家对寄 存器有个初步的了解。大家学习固件库,并不需要记住每个寄存器的作用,而只是通过了解寄 存器来对外设一些功能有个大致的了解,这样对以后的学习也很有帮助。 首先要提一下,在固件库中,GPIO 端口操作对应的库函数函数以及相关定义在文件 stm32f10x_gpio.h 和 stm32f10x_gpio.c 中。 STM32 的 IO 口相比 51 而言要复杂得多,所以使用起来也困难很多。首先 STM32 的 IO 口 可以由软件配置成如下 8 种模式: 1、输入浮空 2、输入上拉 3、输入下拉 4、模拟输入 5、开漏输出 6、推挽输出 7、推挽式复用功能 8、开漏复用功能 157 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 每个 IO 口可以自由编程,但 IO 口寄存器必须要按 32 位字被访问。STM32 的很多 IO 口都 是 5V 兼容的,这些 IO 口在与 5V 电平的外设连接的时候很有优势,具体哪些 IO 口是 5V 兼容 的,可以从该芯片的数据手册管脚描述章节查到(I/O Level 标 FT 的就是 5V 电平兼容的)。 STM32 的每个 IO 端口都有 7 个寄存器来控制。他们分别是:配置模式的 2 个 32 位的端口 配置寄存器 CRL 和 CRH;2 个 32 位的数据寄存器 IDR 和 ODR;1 个 32 位的置位/复位寄存器 BSRR;一个 16 位的复位寄存器 BRR;1 个 32 位的锁存寄存器 LCKR。大家如果想要了解每个 寄存器的详细使用方法,可以参考《STM32 中文参考手册 V10》P105~P129。 CRL 和 CRH 控制着每个 IO 口的模式及输出速率。 STM32 的 IO 口位配置表如表 6.1.4 所示: 表 6.1.4 STM32 的 IO 口位配置表 STM32 输出模式配置如表 6.1.5 所示: 表 6.1.5 STM32 输出模式配置表 接下来我们看看端口低配置寄存器 CRL 的描述,如图 6.1.6 所示: 158 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 6.1.6 端口低配置寄存器 CRL 各位描述 该寄存器的复位值为 0X4444 4444,从图 6.1.4 可以看到,复位值其实就是配置端口为浮空 输入模式。从上图还可以得出:STM32 的 CRL 控制着每组 IO 端口(A~G)的低 8 位的模式。 每个 IO 端口的位占用 CRL 的 4 个位,高两位为 CNF,低两位为 MODE。这里我们可以记住几 个常用的配置,比如 0X0 表示模拟输入模式(ADC 用)、0X3 表示推挽输出模式(做输出口用, 50M 速率)、0X8 表示上/下拉输入模式(做输入口用)、0XB 表示复用输出(使用 IO 口的第二 功能,50M 速率)。 CRH 的作用和 CRL 完全一样,只是 CRL 控制的是低 8 位输出口,而 CRH 控制的是高 8 位输出口。这里我们对 CRH 就不做详细介绍了。下面我们讲解一下怎样通过固件库设置 GPIO 的相关参数和输出。 GPIO 相关的函数和定义分布在固件库文件 stm32f10x_gpio.c 和头文件 stm32f10x_gpio.h 文 件中。 在固件库开发中,操作寄存器 CRH 和 CRL 来配置 IO 口的模式和速度是通过 GPIO 初始化 函数完成: void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct); 这个函数有两个参数,第一个参数是用来指定 GPIO,取值范围为 GPIOA~GPIOG。 第二个参数为初始化参数结构体指针,结构体类型为 GPIO_InitTypeDef。下面我们看看这个结 构体的定义。首先我们打开我们光盘的跑马灯实验,然后找到 FWLib 组下面的 stm32f10x_gpio.c 文件,定位到 GPIO_Init 函数体处,双击入口参数类型 GPIO_InitTypeDef 后右键选择“Go to definition of …”可以查看结构体的定义: typedef struct 159 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 { uint16_t GPIO_Pin; GPIOSpeed_TypeDef GPIO_Speed; GPIOMode_TypeDef GPIO_Mode; }GPIO_InitTypeDef; 下面我们通过一个 GPIO 初始化实例来讲解这个结构体的成员变量的含义。 通过初始化结构体初始化 GPIO 的常用格式是: GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //LED0-->PB.5 端口配置 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//速度 50MHz GPIO_Init(GPIOB, &GPIO_InitStructure);//根据设定参数配置 GPIO 上面代码的意思是设置 GPIOB 的第 5 个端口为推挽输出模式,同时速度为 50M。从上面初始 化代码可以看出,结构体 GPIO_InitStructure 的第一个成员变量 GPIO_Pin 用来设置是要初始化 哪个或者哪些 IO 口;第二个成员变量 GPIO_Mode 是用来设置对应 IO 端口的输出输入模式, 这些模式是上面我们讲解的 8 个模式,在 MDK 中是通过一个枚举类型定义的: typedef enum { GPIO_Mode_AIN = 0x0, GPIO_Mode_IN_FLOATING = 0x04, GPIO_Mode_IPD = 0x28, GPIO_Mode_IPU = 0x48, GPIO_Mode_Out_OD = 0x14, GPIO_Mode_Out_PP = 0x10, GPIO_Mode_AF_OD = 0x1C, GPIO_Mode_AF_PP = 0x18 //模拟输入 //浮空输入 //下拉输入 //上拉输入 //开漏输出 //通用推挽输出 //复用开漏输出 //复用推挽 }GPIOMode_TypeDef; 第三个参数是 IO 口速度设置,有三个可选值,在 MDK 中同样是通过枚举类型定义: typedef enum { GPIO_Speed_10MHz = 1, GPIO_Speed_2MHz, GPIO_Speed_50MHz }GPIOSpeed_TypeDef; 这些入口参数的取值范围怎么定位,怎么快速定位到这些入口参数取值范围的枚举类型, 在我们上面章节 4.7 的“快速组织代码”章节有讲解,不明白的朋友可以翻回去看一下,这里 我们就不重复讲解,在后面的实验中,我们也不再去重复讲解怎么定位每个参数的取值范围的 方法。 IDR 是一个端口输入数据寄存器,只用了低 16 位。该寄存器为只读寄存器,并且只能以 16 位的形式读出。该寄存器各位的描述如图 6.1.7 所示: 160 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 6.1.7 端口输入数据寄存器 IDR 各位描述 要想知道某个 IO 口的电平状态,你只要读这个寄存器,再看某个位的状态就可以了。使 用起来是比较简单的。 在固件库中操作 IDR 寄存器读取 IO 端口数据是通过 GPIO_ReadInputDataBit 函数实现的: uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) 比如我要读 GPIOA.5 的电平状态,那么方法是: GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_5); 返回值是 1(Bit_SET)或者 0(Bit_RESET); ODR 是一个端口输出数据寄存器,也只用了低 16 位。该寄存器为可读写,从该寄存器读 出来的数据可以用于判断当前 IO 口的输出状态。而向该寄存器写数据,则可以控制某个 IO 口 的输出电平。该寄存器的各位描述如图 6.1.8 所示: 图 6.1.8 端口输出数据寄存器 ODR 各位描述 在固件库中设置 ODR 寄存器的值来控制 IO 口的输出状态是通过函数 GPIO_Write 来实现 的: void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal); 该函数一般用来往一次性一个 GPIO 的多个端口设值。 BSRR 寄存器是端口位设置/清除寄存器。该寄存器和 ODR 寄存器具有类似的作用,都可 以用来设置 GPIO 端口的输出位是 1 还是 0。下面我们看看该寄存器的描述如下图: 161 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 6.1.9 端口位设置/清除寄存器 BSRR 各位描述 该寄存器通过举例子可以很清楚了解它的使用方法。例如你要设置 GPIOA 的第 1 个端口 值为 1,那么你只需要往寄存器 BSRR 的低 16 位对应位写 1 即可: GPIOA->BSRR=1<<1; 如果你要设置 GPIOA 的第 1 个端口值为 0,你只需要往寄存器高 16 位对应为写 1 即可: GPIOA->BSRR=1<<(16+1) 该寄存器往相应位写 0 是无影响的,所以我们要设置某些位,我们不用管其他位的值。 BRR 寄存器是端口位清除寄存器。该寄存器的作用跟 BSRR 的高 16 位雷同,这里就不做 详细讲解。在 STM32 固件库中,通过 BSRR 和 BRR 寄存器设置 GPIO 端口输出是通过函数 GPIO_SetBits()和函数 GPIO_ResetBits()来完成的。 void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) 在多数情况下,我们都是采用这两个函数来设置 GPIO 端口的输入和输出状态。比如我们要设 置 GPIOB.5 输出 1,那么方法为: GPIO_SetBits(GPIOB, GPIO_Pin_5); 反之如果要设置 GPIOB.5 输出位 0,方法为: GPIO_ResetBits (GPIOB, GPIO_Pin_5); GPIO 相关的函数我们先讲解到这里。虽然 IO 操作步骤很简单,这里我们还是做个概括性 的总结,操作步骤为: 1) 使能 IO 口时钟。调用函数为 RCC_APB2PeriphClockCmd()。 2) 初始化 IO 参数。调用函数 GPIO_Init(); 3) 操作 IO。操作 IO 的方法就是上面我们讲解的方法。 上面我们讲解了 STM32 IO 口的基本知识以及固件库操作 GPIO 的一些函数方法,下面我 们来讲解我们的跑马灯实验的硬件和软件设计。 6.2 硬件设计 本章用到的硬件只有 LED(DS0 和 DS1)。其电路在 ALIENTEK 战舰 STM32F103 开发板 上默认是已经连接好了的。DS0 接 PB5,DS1 接 PE5。所以在硬件上不需要动任何东西。其连 162 接原理图如图 6.2.1 下: STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 6.2.1 LED 与 STM32F1 连接原理图 6.3 软件设计 跑马灯实验我们主要用到的固件库文件是: stm32f10x_gpio.c /stm32f10x_gpio.h stm32f10x_rcc.c/stm32f10x_rcc.h misc.c/ misc.h stm32f10x_usart /stm32f10x_usart.h 其中 stm32f10x_rcc.h 头文件在每个实验中都要引入,因为系统时钟配置函数以及相关的外设时 钟使能函数都在这个其源文件 stm32f10x_rcc.c 中。stm32f10x_usart.h 和 misc.h 头文件在我们 SYSTEM 文件夹中都需要使用到,所以每个实验都会引用。 首先,找到之前 3.3 节新建的 Template 工程,在该文件夹下面新建一个 HARDWARE 的文 件夹,用来存储以后与硬件相关的代码,然后在 HARDWARE 文件夹下新建一个 LED 文件夹, 用来存放与 LED 相关的代码。如图 6.3.1 所示: 图 6.3.1 新建 HARDWARE 文件夹 然后我们打开 USER 文件夹下的 LED.uvprojx 工程(如果是使用的上面新建的工程模板,那 么就是 Template. uvprojx,大家可以将其重命名为 LED. uvprojx),按 按钮新建一个文件,然 后保存在 HARDWARE->LED 文件夹下面,保存为 led.c。在该文件中输入如下代码: #include "led.h" //初始化 PB5 和 PE5 为输出口.并使能这两个口的时钟 //LED IO 初始化 163 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 void LED_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB| RCC_APB2Periph_GPIOE, ENABLE); //使能 PB,PE 端口时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_SetBits(GPIOB,GPIO_Pin_5); //LED0-->PB.5 推挽输出 //推挽输出 //PB.5 输出高 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //LED1-->PE.5 推挽输出 GPIO_Init(GPIOE, &GPIO_InitStructure); GPIO_SetBits(GPIOE,GPIO_Pin_5); /PE.5 输出高 } 该代码里面就包含了一个函数 void LED_Init(void),该函数的功能就是用来实现配置 PB5 和 PE5 为推挽输出。这里需要注意的是:在配置 STM32 外设的时候,任何时候都要先使能该 外设的时钟。GPIO 是挂载在 APB2 总线上的外设,在固件库中对挂载在 APB2 总线上的外设时 钟使能是通过函数 RCC_APB2PeriphClockCmd()来实现的。对于这个入口参数设置,在我们前 面的“快速组织代码”章节已经讲解很清楚了。看看我们的代码: RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB| RCC_APB2Periph_GPIOE, ENABLE); //使能 GPIOB,GPIOE 端口时钟 这行代码的作用是使能 APB2 总线上的 GPIOB 和 GPIOE 的时钟。 在配置完时钟之后,LED_Init 配置了 GPIOB.5 和 GPIOE.5 的模式为推挽输出,并且默认 输出 1。这样就完成了对这两个 IO 口的初始化。函数代码是: GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //LED0-->GPIOB.5 端口配置 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //IO 口速度为 50MHz GPIO_Init(GPIOB, &GPIO_InitStructure); //初始化 GPIOB.5 GPIO_SetBits(GPIOB,GPIO_Pin_5); //GPIOB.5 输出高 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; GPIO_Init(GPIOE, &GPIO_InitStructure); GPIO_SetBits(GPIOE,GPIO_Pin_5); //LED1-->GPIOE.5 推挽输出 //初始化 GPIOE.5 //GPIOE.5 输出高 这里需要说明的是,因为 GPIOB 和 GPIOE 的 IO 口的初始化参数都是设置在结构体变量 GPIO_InitStructure 中,因为两个 IO 口的模式和速度都一样,所以我们只用初始化一次,在 GPIOE.5 的初始化的时候就不需要再重复初始化速度和模式了。最后一行代码: GPIO_SetBits(GPIOE,GPIO_Pin_5); 的作用是在初始化中将 GPIOE.5 输出设置为高。 保存 led.c 代码,然后我们按同样的方法,新建一个 led.h 文件,也保存在 LED 文件夹下面。 在 led.h 中输入如下代码: 164 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 #ifndef __LED_H #define __LED_H #include "sys.h" //LED 端口定义 #define LED0 PBout(5)// DS0 #define LED1 PEout(5)// DS1 void LED_Init(void);//初始化 #endif 这段代码里面最关键就是 2 个宏定义: #define LED0 PBout(5)// DS0 #define LED1 PEout(5)// DS1 这里使用的是位带操作来实现操作某个 IO 口的 1 个位的,关于位带操作前面第五章 5.2.1 已经有介绍,这里不再多说。这里我们来讲解一下,操作 IO 口输出高低电平的三种方法。 通过位带操作 PB5 输出高低电平从而控制 LED0 的方法如下: LED0=1; //通过位带操作控制 LED0 的引脚 PB5 输出高电平 LED0=0; //通过位带操作控制 LED0 的引脚 PB5 输出低电平 关于位带操作的基本原理请参考 5.2.1 小节。同样我们也可以使用固件库操作和寄存器操作 来实现 IO 口操作。库函数操作方法如下: GPIO_SetBits(GPIOB, GPIO_Pin_5); //设置 GPIOB.5 输出 1,等同 LED0=1; GPIO_ResetBits (GPIOB, GPIO_Pin_5); //设置 GPIOB.5 输出 0,等同 LED0=0; 库函数操作就直接调用两个函数即可控制 IO 输出高低电平。我们也通过直接操作寄存器 BRR 和 BSRR 的方式来操作 IO 口输出高低电平,方法如下: GPIOB->BRR=GPIO_Pin_5; //设置 GPIOB.5 输出 1,等同 LED0=1; GPIOE->BSRR=GPIO_Pin_5; //设置 GPIOB.5 输出 0,等同 LED0=0; 对于上面三种方法,大家根据自己喜好来选择一种即可。在 IO 口速度没有太大要求的情 况下效果都是一样的。 接下来我们将 led.h 也保存一下。接着,我们在 Manage Components 管理里面新建一个 HARDWARE 的组,并把 led.c 加入到这个组里面,如图 6.3.2 所示: 165 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 6.3.2 给工程新增 HARDWARE 组 单击 OK,回到工程,然后你会发现在 Project Workspace 里面多了一个 HARDWARE 的组, 在改组下面有一个 led.c 的文件。如图 6.3.3 所示: 图 6.3.3 新增 HARDWARE 组 166 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 然后用之前介绍的方法(在 3.3.3 节介绍的)将 led.h 头文件的路径加入到工程里面。 回到主界面,在 main 函数里面编写如下代码: #include "led.h" #include "delay.h" #include "sys.h" //ALIENTEK 战舰 STM32 开发板实验 1 //跑马灯实验 int main(void) { delay_init(); LED_Init(); //延时函数初始化 //初始化与 LED 连接的硬件接口 while(1) { LED0=0; LED1=1; delay_ms(300); //延时 300ms LED0=1; LED1=0; delay_ms(300); //延时 300ms } } 代码包含了#include "led.h"这句,使得 LED0、LED1、LED_Init 等能在 main()函数里被调用。 这里我们需要重申的是,在固件库 V3.5 中,系统在启动的时候会调用 system_stm32f10x.c 中的 函数 SystemInit()对系统时钟进行初始化,在时钟初始化完毕之后会调用 main()函数。 所以我 们不需要再在 main()函数中调用 SystemInit()函数。当然如果有需要重新设置时钟系统,可以写 自己的时钟设置代码,SystemInit()只是将时钟系统初始化为默认状态。 main()函数非常简单,先调用 delay_init()初始化延时,接着就是调用 LED_Init()来初始化 GPIOB.5 和 GPIOE.5 为输出。最后在死循环里面实现 LED0 和 LED1 交替闪烁,间隔为 300ms。 上面是通过位带操作实现的 IO 操作,对于通过调用库函数以及直接操作寄存器来实现 LED 控制的方法,在我们 main.c 文件中我们已经注释掉,大家可以取消注释分别来测试这两种方法, 实际效果是一模一样的。 最后我们按 ,编译工程,得到结果如图 6.3.4 所示: 图 6.3.4 编译结果 可以看到没有错误,也没有警告。从编译信息可以看出,我们的代码占用 FLASH 大小为: 1892 字节(1556+336),所用的 SRAM 大小为:1864 个字节(32+1832)。 167 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 这里我们解释一下,编译结果里面的几个数据的意义: Code:表示程序所占用 FLASH 的大小(FLASH)。 RO-data:即 Read Only-data,表示程序定义的常量,如 const 类型(FLASH)。 RW-data:即 Read Write-data,表示已被初始化的全局变量(SRAM) ZI-data:即 Zero Init-data,表示未被初始化的全局变量(SRAM) 有了这个就可以知道你当前使用的 flash 和 sram 大小了,所以,一定要注意的是程序的大 小不是.hex 文件的大小,而是编译后的 Code 和 RO-data 之和。 接下来,我们就先进行软件仿真,验证一下是否有错误的地方,然后下载到战舰 STM32 开发板看看实际运行的结果。 6.4 仿真与下载 此代码,我们先进行软件仿真,看看结果对不对,根据软件仿真的结果,然后再下载到 ALIENTEK 战舰 STM32 板子上面看运行是否正确。 首先,我们进行软件仿真(请先确保 Options for Target Debug 选项卡里面已经设置为 Use Simulator,参考 3.4.1 小节)。先按 开始仿真,接着按 ,显示逻辑分析窗口,点击 Setup, 新建两个信号 PORTB.5 和 PORTE.5,如图 6.4.1 所示: 图 6.4.1 逻辑分析设置 Display Type 选择 bit,然后单击 Close 关闭该对话框,可以看到逻辑分析窗口出来了两个 信号,如图 6.4.2 所示: 168 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 6.4.2 设置后的逻辑分析窗口 接着,点击 ,开始运行。运行一段时间之后,按 可以看到如图 6.4.3 所示的波形: 按钮,暂停仿真回到逻辑分析窗口, 图 6.4.3 仿真波形 这里注意 Gird 要调节到 0.2s 左右比较合适,可以通过 Zoom 里面的 In 按钮来放大波形, 通过 Out 按钮来缩小波形,或者按 All 显示全部波形。从上图中可以看到 PORTB.5 和 PORTE.5 交替输出,周期可以通过中间那根红线来测量。至此,我们的软件仿真已经顺利通过。 在软件仿真没有问题了之后,我们就可以把代码下载到开发板上,看看运行结果是否与我 们仿真的一致。运行结果如图 6.4.4 所示: 169 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 6.4.4 执行结果 至此,我们的第一章的学习就结束了,本章作为 STM32 的入门第一个例子,详细介绍了 STM32 的 IO 口操作,同时巩固了前面的学习,并进一步介绍了 MDK 的软件仿真功能。希望 大家好好理解一下。 170 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第七章 蜂鸣器实验 上一章,我们介绍了 STM32F1 的 IO 口作为输出的使用,这一章,我们将通过另外一个例 子讲述 STM32F1 的 IO 口作为输出的使用。在本章中,我们将利用一个 IO 口来控制板载的有 源蜂鸣器,实现蜂鸣器控制。通过本章的学习,你将进一步了解 STM32F1 的 IO 口作为输出口 使用的方法。本章分为如下几个小节: 7.1 蜂鸣器简介 7.2 硬件设计 7.3 软件设计 7.4 仿真与下载 171 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 7.1 蜂鸣器简介 蜂鸣器是一种一体化结构的电子讯响器,采用直流电压供电,广泛应用于计算机、打印机、 复印机、报警器、电子玩具、汽车电子设备、电话机、定时器等电子产品中作发声器件。蜂鸣 器主要分为压电式蜂鸣器和电磁式蜂鸣器两种类型。 战舰 STM32 开发板板载的蜂鸣器是电磁式的有源蜂鸣器,如图 7.1.1 所示: 图 7.1.1 有源蜂鸣器 这里的有源不是指电源的“源”,而是指有没有自带震荡电路,有源蜂鸣器自带了震荡电路, 一通电就会发声;无源蜂鸣器则没有自带震荡电路,必须外部提供 2~5Khz 左右的方波驱动, 才能发声。 前面我们已经对 STM32 的 IO 做了简单介绍,上一章,我们就是利用 STM32 的 IO 口直接 驱动 LED 的,本章的蜂鸣器,我们能否直接用 STM32 的 IO 口驱动呢?让我们来分析下:STM32 的单个 IO 最大可以提供 25mA 电流(来自数据手册),而蜂鸣器的驱动电流是 30mA 左右,两 者十分相近,但是全盘考虑,STM32 整个芯片的电流,最大也就 150mA,如果用 IO 口直接驱 动蜂鸣器,其他地方用电就得省着点了…所以,我们不用 STM32 的 IO 直接驱动蜂鸣器,而是 通过三极管扩流后再驱动蜂鸣器,这样 STM32 的 IO 只需要提供不到 1mA 的电流就足够了。 IO 口使用虽然简单,但是和外部电路的匹配设计,还是要十分讲究的,考虑越多,设计就 越可靠,可能出现的问题也就越少。 本章将要实现的是控制 ALIENTEK 战舰 STM32 开发板上的蜂鸣器发出:“嘀”…“ 嘀”… 的间隔声,进一步熟悉 STM32 IO 口的使用。 7.2 硬件设计 本章需要用到的硬件有: 1)指示灯 DS0 2)蜂鸣器 DS0 在上一章已有介绍,而蜂鸣器在硬件上也是直接连接好了的,不需要经过任何设置, 直接编写代码就可以了。蜂鸣器的驱动信号连接在 STM32 的 PB8 上。如图 7.2.1 所示: 图 7.2.1 蜂鸣器与 STM32 连接原理图 172 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图中我们用到一个 NPN 三极管(S8050)来驱动蜂鸣器,R60 主要用于防止蜂鸣器的误发 声。当 PB.8 输出高电平的时候,蜂鸣器将发声,当 PB.8 输出低电平的时候,蜂鸣器停止发声。 7.3 软件设计 大家可以直接打开本实验工程。也可以按下面的步骤在实验 1 的基础上新建蜂鸣器实验工 程。复制上一章的 LED 实验工程,然后打开 USER 目录,把目录下面工程 LED.uvprojx 重命名 为 BEEP.uvprojx。,然后在 HARDWARE 文件夹下新建一个 BEEP 文件夹,用来存放与蜂鸣器 相关的代码。如图 7.3.1 所示: 图 7.3.1 在 HARDWARE 下新增 BEEP 文件夹 然后我们打开 USER 文件夹下的 BEEP.uvprojx 工程,按 按钮新建一个文件,然后保存在 HARDWAREBEEP 文件夹下面,保存为 beep.c。在该文件中输入如下代码: #include "beep.h" //初始化 PB8 为输出口.并使能这个口的时钟 //LED IO 初始化 void BEEP_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //使能 GPIOB 端口时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; //BEEP-->GPIOB.8 端口配置 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); //推挽输出 //速度为 50MHz //根据参数初始化 GPIOB.8 GPIO_ResetBits(GPIOB,GPIO_Pin_8); //输出 0,关闭蜂鸣器输出 } 这段代码 仅包含 1 个函数:void BEEP_Init(void),该函数的作用就是使能 PORTB 的时钟, 同时配置 PB8 为推挽输出。这里的初始化内容跟实验 6 跑马灯实验几乎是一样的,这里就不做 深入的讲解。 保存 beep.c 代码,然后我们按同样的方法,新建一个 beep.h 文件,也保存在 BEEP 文件夹 173 STM32F1 开发指南(库函数版) 下面。在 beep.h 中输入如下代码: ALIENTEK 战舰 STM32F103 V3 开发板教程 #ifndef __BEEP_H #define __BEEP_H #include "sys.h" //蜂鸣器端口定义 #define BEEP PBout(8) void BEEP_Init(void); // BEEP,蜂鸣器接口 //初始化 #endif 和上一章一样,我们这里还是通过位带操作来实现某个 IO 口的输出控制,BEEP 就直接代 表了 PB8 的输出状态。我们只需要令 BEEP=1,就可以让蜂鸣器发声。 将 beep.h 也保存。接着,我们把 beep.c 加入到 HARDWARE 这个组里面,这一次我们通过 双击的方式来增加新的.c 文件,双击 HARDWARE,找到 beep.c,加入到 HARDWARE 里面, 如图 7.3.2 所示: 图 7.3.2 将 beep.c 加入 HARDWARE 组下 可以看到 HARDWARE 文件夹里面多了一个 beep.c 的文件,然后还是用老办法把 beep.h 头文件所在的路径加入到工程里面。回到主界面,在 main.c 里面编写如下代码: #include "sys.h" #include "delay.h" #include "led.h" #include "beep.h" 174 STM32F1 开发指南(库函数版) //ALIENTEK 战舰 STM32 开发板实验 2 //蜂鸣器实验 ALIENTEK 战舰 STM32F103 V3 开发板教程 int main(void) { delay_init(); LED_Init(); BEEP_Init(); //延时函数初始化 //初始化与 LED 连接的硬件接口 //初始化蜂鸣器端口 while(1) { LED0=0; BEEP=0; delay_ms(300); LED0=1; BEEP=1; delay_ms(300); } } 注意要将 BEEP 文件夹加入头文件包含路径,不能少,否则编译的时候会报错。这段代码 就实现前面 7.1 节所阐述的功能,同时加入了 DS0(LED0)的闪烁来提示程序运行(后面的代 码,我们基本都会加入这个),整个代码比较简单。 然后按 ,编译工程,得到结果如图 7.3.3 所示: 图 7.3.3 编译结果 可以看到没有错误,也没有警告。接下来,大家就可以下载验证了。如果有 JLINK,则可 以用 jlink 进行在线调试(需要先下载代码),单步查看代码的运行,STM32F1 的在线调试方法 介绍,参见:3.4.3 节。 7.4 仿真与下载 我们可以先用软件仿真,看看结果对不对,根据软件仿真的结果,然后再下载到战舰 STM32 开发板上面看运行是否正确。 首先,我们进行软件仿真。先按 开始仿真,接着按 ,显示逻辑分析窗口,点击 Setup, 新建 2 个信号:PORTB.5 和 PORTB.8,如图 7.4.1 所示: 175 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 7.4.1 新建仿真信号 接着,点击 ,开始运行。运行一段时间之后,按 可以看到如图 7.4.2 所示的波形: 按钮,暂停仿真回到逻辑分析窗口, 图 7.4.2 仿真波形 从图中我们可以看出,PB.5 和 PB.8 同时输出高和低电平,和我们预计的效果是一样的, 周期是 0.6s,符合预期设计。接下来可以把代码下载到战舰 STM32 开发板上看看运行结果是否 正确。 下载完代码,可以看到 DS0 亮的时候蜂鸣器不叫,而 DS0 灭的时候,蜂鸣器叫(因为他 们的有效信号相反)。间隔为 0.3 秒左右,符合预期设计。 至此,我们的本章的学习就结束了。本章,作为 STM32 的入门第二个例子,进一步介绍 了 STM32 的 IO 作为输出口的使用方法,同时巩固了前面的学习。希望大家在开发板上实际验 证一下,从而加深印象。 176 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第八章 按键输入实验 上两章,我们介绍了 STM32F1 的 IO 口作为输出的使用,这一章,我们将向大家介绍如何 使用 STM32F1 的 IO 口作为输入用。在本章中,我们将利用板载的 4 个按键,来控制板载的两 个 LED 的亮灭和蜂鸣器的开关。通过本章的学习,你将了解到 STM32F1 的 IO 口作为输入口 的使用方法。本章分为如下几个小节: 8.1 STM32 IO 口简介 8.2 硬件设计 8.3 软件设计 8.4 仿真与下载 177 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 8.1 STM32 IO 口简介 STM32F1 的 IO 口在上一章已经有了比较详细的介绍,这里我们不再多说。STM32F1 的 IO 口做输入使用的时候,是通过调用函数 GPIO_ReadInputDataBit()来读取 IO 口的状态的。了解 了这点,就可以开始我们的代码编写了。 这一章,我们将通过 ALIENTEK 战舰 STM32 开发板上载有的 4 个按钮(WK_UP、KEY0、 KEY1 和 KEY2),来控制板上的 2 个 LED(DS0 和 DS1)和蜂鸣器,其中 WK_UP 控制蜂鸣器, 按一次叫,再按一次停;KEY2 控制 DS0,按一次亮,再按一次灭;KEY1 控制 DS1,效果同 KEY2;KEY0 则同时控制 DS0 和 DS1,按一次,他们的状态就翻转一次。 8.2 硬件设计 本实验用到的硬件资源有: 1) 指示灯 DS0、DS1 2) 蜂鸣器 3) 4 个按键:KEY0、KEY1、KEY2、和 WK_UP。 DS0、DS1 以及蜂鸣器和 STM32 的连接在上两章都已经分别介绍了,在战舰 STM32 开发 板上的按键 KEY0 连接在 PE4 上、KEY1 连接在 PE3 上、KEY2 连接在 PE2 上、WK_UP 连接 在 PA0 上。如图 8.2.1 所示: 图 8.2.1 按键与 STM32 连接原理图 这里需要注意的是:KEY0、KEY1 和 KEY2 是低电平有效的,而 WK_UP 是高电平有效的, 并且外部都没有上下拉电阻,所以,需要在 STM32 内部设置上下拉。 8.3 软件设计 从这章开始,我们的软件设计主要是通过直接打开我们光盘的实验工程,而不再讲解怎么 加入文件和头文件目录。 打开我们的按键实验工程可以看到,我们引入了 key.c 文件以及头文件 key.h。下面我们首 先打开 key.c 文件,代码如下: #include "key.h" #include "sys.h" #include "delay.h" //按键初始化函数 void KEY_Init(void) //IO 初始化 { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA| RCC_APB2Periph_GPIOE,ENABLE); //使能 PORTA,PORTE 时钟 178 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4;//GPIOE.2~4 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //设置成上拉输入 GPIO_Init(GPIOE, &GPIO_InitStructure); //初始化 GPIOE2,3,4 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //初始化 WK_UP-->GPIOA.0 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //PA0 设置成输入,下拉 GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 GPIOA.0 } //按键处理函数 //返回按键值 //mode:0,不支持连续按;1,支持连续按; //0,没有任何按键按下;1,KEY0 按下;2,KEY1 按下;3,KEY2 按下 ;4,KEY3 按下 WK_UP //注意此函数有响应优先级,KEY0>KEY1>KEY2>KEY3!! u8 KEY_Scan(u8 mode) { static u8 key_up=1; if(mode)key_up=1; //按键按松开标志 //支持连按 if(key_up&&(KEY0==0||KEY1==0||KEY2==0||KEY3==1)) { delay_ms(10); //去抖动 key_up=0; if(KEY0==0)return KEY0_PRES; else if(KEY1==0)return KEY1_PRES; else if(KEY2==0)return KEY2_PRES; else if(KEY3==1)return WKUP_PRES; }else if(KEY0==1&&KEY1==1&&KEY2==1&&KEY3==0)key_up=1; return 0; // 无按键按下 } 这段代码包含 2 个函数,void KEY_Init(void)和 u8 KEY_Scan(u8 mode),KEY_Init()是用来 初始化按键输入的 IO 口的。首先使能 GPIOA 和 GPIOE 时钟,然后实现 PA0、PE2~4 的输入设 置,这里和第六章的输出配置差不多,只是这里用来设置成的是输入而第六章是输出。 KEY_Scan()函数,则是用来扫描这 4 个 IO 口是否有按键按下。KEY_Scan()函数,支持两 种扫描方式,通过 mode 参数来设置。 当 mode 为 0 的时候,KEY_Scan()函数将不支持连续按,扫描某个按键,该按键按下之后 必须要松开,才能第二次触发,否则不会再响应这个按键,这样的好处就是可以防止按一次多 次触发,而坏处就是在需要长按的时候比较不合适。 当 mode 为 1 的时候,KEY_Scan()函数将支持连续按,如果某个按键一直按下,则会一直 返回这个按键的键值,这样可以方便的实现长按检测。 有了 mode 这个参数,大家就可以根据自己的需要,选择不同的方式。这里要提醒大家, 因为该函数里面有 static 变量,所以该函数不是一个可重入函数,在有 OS 的情况下,这个大家 要留意下。同时还有一点要注意的就是,该函数的按键扫描是有优先级的,最优先的是 KEY0, 第二优先的是 KEY1,接着 KEY2,最后是 WK_UP 按键。该函数有返回值,如果有按键按下, 则返回非 0 值,如果没有或者按键不正确,则返回 0。 179 STM32F1 开发指南(库函数版) 接下来我们看看头文件 key.h 里面的代码: ALIENTEK 战舰 STM32F103 V3 开发板教程 #ifndef __KEY_H #define __KEY_H #include "sys.h" #define KEY0 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_4)//读取按键 0 #define KEY1 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3)//读取按键 1 #define KEY2 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_2)//读取按键 2 #define WK_UP GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)//读取按键 3(WK_UP) #define KEY0_PRES 1 //KEY0 按下 #define KEY1_PRES 2 //KEY1 按下 #define KEY2_PRES 3 //KEY2 按下 #define WKUP_PRES 4 //WK_UP 按下(即 WK_UP/WK_UP) void KEY_Init(void); //IO 初始化 u8 KEY_Scan(u8); //按键扫描函数 #endif 这段代码里面最关键就是 4 个宏定义: #define KEY0 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_4) //读取按键 0 #define KEY1 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3) //读取按键 1 #define KEY2 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_2) //读取按键 2 #define WK_UP GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0) //读取按键 3(WK_UP) 前面两个实验用的是位带操作实现设定某个 IO 口的位。这里我们采取的是库函数的读取 IO 口的值。当然,上面的功能也同样可以通过位带操作来简单的实现: #define KEY0 PEin(4) //PE4 #define KEY1 PEin(3) //PE3 #define KEY2 PEin(2) //PE2 #define WK_UP PAin(0) //PA0 WK_UP 用库函数实现的好处是在各个 STM32 芯片上面的移植性非常好,不需要修改任何代码。 用位带操作的好处是简洁,至于使用哪种方法,看各位的爱好了。 在 key.h 中,我们还定义了 KEY0_PRES/KEY1_PRES /KEY2_PRES /WKUP_PRES 等 4 个 宏定义,分别对应开发板上下左右(KEY0/KEY1/KEY2/WKUP)按键按下时 KEY_Scan()返回 的值。这些宏定义的方向直接和开发板的按键排列方式相同,方便大家使用。 最后,我们看看 main.c 里面编写的主函数代码如下: #include "led.h" #include "delay.h" #include "key.h" #include "sys.h" #include "beep.h" //ALIENTEK 战舰 STM32 开发板实验 3 //按键输入实验 int main(void) { 180 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 u8 key; delay_init(); LED_Init(); KEY_Init(); BEEP_Init(); LED0=0; //延时函数初始化 //LED 端口初始化 //初始化与按键连接的硬件接口 //初始化蜂鸣器端口 //先点亮红灯 while(1) { key =KEY_Scan(0); //得到键值 if(key) { switch(t) { case WKUP_PRES: //控制蜂鸣器 BEEP=!BEEP;break; case KEY2_PRES: //控制 LED0 翻转 LED0=!LED0;break; case KEY1_PRES: //控制 LED1 翻转 LED1=!LED1;break; case KEY0_PRES: //同时控制 LED0,LED1 翻转 LED0=!LED0; LED1=!LED1;break; } }else delay_ms(10); } } 主函数代码比较简单,先进行一系列的初始化操作,然后在死循环中调用按键扫描函数 KEY_Scan()扫描按键值,最后根据按键值控制 LED 和蜂鸣器的翻转。 8.4 仿真与下载 我们可以先用软件仿真,看看结果对不对,根据软件仿真的结果,然后再下载到战舰 STM32 板子上面看运行是否正确。 首先,我们进行软件仿真。先按 开始仿真,接着按 ,显示逻辑分析窗口,点击 Setup, 新建 7 个信号 PORTB.5、PORTE.5、PORTB.8、PORTA.0、PORTE.2、PORTE.3、PORTE.4,如 图 8.4.1 所示 181 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 8.4.1 新建仿真信号 然后再点击 PeripheralsGeneral Purpose I/OGPIOE,弹出 GPIOE 的查看窗口,如图 8.4.2 所示: 图 8.4.2 查看 GPIOE 寄存器 然后在 key=KEY_Scan();这里设置一个断点,按 直接执行到这里,然后在 General Purpose 182 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 I/O E 窗口内的 Pins 里面勾选 2、3、4 位,这是虽然我们已经设置了这几个 IO 口为上拉输入, 但是 MDK 不会考虑 STM32 自带的上拉和下拉,所以我们得自己手动设置一下,来使得其初始 状态和外部硬件的状态一摸一样。如图 8.4.3 所示: 图 8.4.3 执行到断点处 本来我们还需要设置 PORTA.0 的,但是 GPIOA.0 是高电平有效,刚好默认的就满足要求, 不需要再去勾选 PORTA.0 了。所以这里我们可以省略一个 GPIOA.0 的设置。接着我们执行过 这句,可以看到 key 的值依旧为 0,也就是没有任何按键按下。接着我们再按 ,再次执行到 key=KEY_Scan();我们此次把 Pins 的 PE2 取消勾选,再次执行过这句,得到 key 的值为 3,如 图 8.4.4 所示: 183 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 8.4.4 按键扫描结果 然后按相似的方法,分别取消勾选 PE3 和 PE4,以及勾选 PA0,然后再把它们还原,可以 看到逻辑分析窗口的波形如图 8.4.5 所示: 图 8.4.5 仿真波形 184 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 从图 8.4.5 可以看出,当 PE2 按下的时候 PB5 翻转,PE3 按下的时候 PE5 翻转,PE4 按下 的时候 PB5 和 PE5 一起翻转,PA0 按下的时候 PB8 翻转,使我们想要得到的结果。因此,可 以确定软件仿真基本没有问题了。接下来可以把代码下载到战 STM32 开发板上看看运行结果 是否正确。 在下载完之后,我们可以按 KEY0、KEY1、KEY2 和 WK_UP 来看看 DS0 和 DS1 以及蜂 鸣器的变化,是否和我们仿真的结果一致(结果肯定是一致的)。 至此,我们的本章的学习就结束了。本章,作为 STM32 的入门第三个例子,介绍了 STM32 的 IO 作为输入的使用方法,同时巩固了前面的学习。希望大家在开发板上实际验证一下,从 而加深印象。 185 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第九章 串口实验 前面几章介绍了 STM32F1 的 IO 口操作。这一章我们将学习 STM32F 的串口,教大家如何 使用 STM32F1 的串口来发送和接收数据。本章将实现如下功能:STM32F1 通过串口和上位机 的对话,STM32F1 在收到上位机发过来的字符串后,原原本本的返回给上位机。本章分为如下 几个小节: 9.1 STM32 串口简介 9.2 硬件设计 9.3 软件设计 9.4 下载验证 186 9.1 STM32 串口简介 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 串口作为 MCU 的重要外部接口,同时也是软件开发重要的调试手段,其重要性不言而喻。 现在基本上所有的 MCU 都会带有串口,STM32 自然也不例外。 STM32 的串口资源相当丰富的,功能也相当强劲。ALIENTEK 战舰 STM32 开发板所使用 的 STM32F103ZET6 最多可提供 5 路串口,有分数波特率发生器、支持同步单线通信和半双工 单线通讯、支持 LIN、支持调制解调器操作、智能卡协议和 IrDA SIR ENDEC 规范、具有 DMA 等。 5.3 节对串口有过简单的介绍,大家看这个实验的时候记得翻过去看看。接下来我们将主要 从库函数操作层面结合寄存器的描述,告诉你如何设置串口,以达到我们最基本的通信功能。 本章,我们将实现利用串口 1 不停的打印信息到电脑上,同时接收从串口发过来的数据,把发 送过来的数据直接送回给电脑。战舰 STM32 开发板板载了 1 个 USB 串口和 1 个 RS232 串口, 我们本章介绍的是通过 USB 串口和电脑通信。 在 4.4.1 章节端口复用功能已经讲解过,对于复用功能的 IO,我们首先要使能 GPIO 时钟, 然后使能复用功能时钟,同时要把 GPIO 模式设置为复用功能对应的模式(这个可以查看手册 《STM32 中文参考手册 V10》P110 的表格“8.1.11 外设的 GPIO 配置”)。这些准备工作做完之后, 剩下的当然是串口参数的初始化设置,包括波特率,停止位等等参数。在设置完成只能接下来 就是使能串口,这很容易理解。同时,如果我们开启了串口的中断,当然要初始化 NVIC 设置中 断优先级别,最后编写中断服务函数。 串口设置的一般步骤可以总结为如下几个步骤: 1) 串口时钟使能,GPIO 时钟使能 2) 串口复位 3) GPIO 端口模式设置 4) 串口参数初始化 5) 开启中断并且初始化 NVIC(如果需要开启中断才需要这个步骤) 6) 使能串口 7) 编写中断处理函数 下面,我们就简单介绍下这几个与串口基本配置直接相关的几个固件库函数。这些函数和 定义主要分布在 stm32f10x_usart.h 和 stm32f10x_usart.c 文件中。 1.串口时钟使能。串口是挂载在 APB2 下面的外设,所以使能函数为: RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1); 2.串口复位。当外设出现异常的时候可以通过复位设置,实现该外设的复位,然后重新配置 这个外设达到让其重新工作的目的。一般在系统刚开始配置外设的时候,都会先执行复位该外 设的操作。复位的是在函数 USART_DeInit()中完成: void USART_DeInit(USART_TypeDef* USARTx);//串口复位 比如我们要复位串口 1,方法为: USART_DeInit(USART1); //复位串口 1 3.串口参数初始化。串口初始化是通过 USART_Init()函数实现的, void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct); 这个函数的第一个入口参数是指定初始化的串口标号,这里选择 USART1。 第二个入口参数是一个 USART_InitTypeDef 类型的结构体指针,这个结构体指针的成员变量用 来设置串口的一些参数。一般的实现格式为: USART_InitStructure.USART_BaudRate = bound; //波特率设置; USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为 8 位数据格式 187 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 USART_InitStructure.USART_StopBits = USART_StopBits_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_Init(USART1, &USART_InitStructure); //初始化串口 从上面的初始化格式可以看出初始化需要设置的参数为:波特率,字长,停止位,奇偶校验位, 硬件数据流控制,模式(收,发)。我们可以根据需要设置这些参数。 4.数据发送与接收。STM32 的发送与接收是通过数据寄存器 USART_DR 来实现的,这是 一个双寄存器,包含了 TDR 和 RDR。当向该寄存器写数据的时候,串口就会自动发送,当收 到数据的时候,也是存在该寄存器内。 STM32 库函数操作 USART_DR 寄存器发送数据的函数是: void USART_SendData(USART_TypeDef* USARTx, uint16_t Data); 通过该函数向串口寄存器 USART_DR 写入一个数据。 STM32 库函数操作 USART_DR 寄存器读取串口接收到的数据的函数是: uint16_t USART_ReceiveData(USART_TypeDef* USARTx); 通过该函数可以读取串口接受到的数据。 5.串口状态。串口的状态可以通过状态寄存器 USART_SR 读取。USART_SR 的各位描述如 图 9.1.1 所示: 图 9.1.1 USART_SR 寄存器各位描述 这里我们关注一下两个位,第 5、6 位 RXNE 和 TC。 RXNE(读数据寄存器非空),当该位被置 1 的时候,就是提示已经有数据被接收到了,并 且可以读出来了。这时候我们要做的就是尽快去读取 USART_DR,通过读 USART_DR 可以将 该位清零,也可以向该位写 0,直接清除。 TC(发送完成),当该位被置位的时候,表示 USART_DR 内的数据已经被发送完成了。如 果设置了这个位的中断,则会产生中断。该位也有两种清零方式:1)读 USART_SR,写 USART_DR。2)直接向该位写 0。 状态寄存器的其他位我们这里就不做过多讲解,大家需要可以查看中文参考手册。 在我们固件库函数里面,读取串口状态的函数是: FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG); 这个函数的第二个入口参数非常关键,它是标示我们要查看串口的哪种状态,比如上面讲解的 RXNE(读数据寄存器非空)以及 TC(发送完成)。例如我们要判断读寄存器是否非空(RXNE),操 作库函数的方法是: USART_GetFlagStatus(USART1, USART_FLAG_RXNE); 我们要判断发送是否完成(TC),操作库函数的方法是: USART_GetFlagStatus(USART1, USART_FLAG_TC); 这些标识号在 MDK 里面是通过宏定义定义的: 188 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 #define USART_IT_PE ((uint16_t)0x0028) #define USART_IT_TXE ((uint16_t)0x0727) #define USART_IT_TC ((uint16_t)0x0626) #define USART_IT_RXNE ((uint16_t)0x0525) #define USART_IT_IDLE ((uint16_t)0x0424) #define USART_IT_LBD ((uint16_t)0x0846) #define USART_IT_CTS ((uint16_t)0x096A) #define USART_IT_ERR ((uint16_t)0x0060) #define USART_IT_ORE ((uint16_t)0x0360) #define USART_IT_NE ((uint16_t)0x0260) #define USART_IT_FE ((uint16_t)0x0160) 6.串口使能。串口使能是通过函数 USART_Cmd()来实现的,这个很容易理解,使用方法 是: USART_Cmd(USART1, ENABLE); //使能串口 7.开启串口响应中断。有些时候当我们还需要开启串口中断,那么我们还需要使能串口中 断,使能串口中断的函数是: void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT, FunctionalState NewState) 这个函数的第二个入口参数是标示使能串口的类型,也就是使能哪种中断,因为串口的中断类 型有很多种。比如在接收到数据的时候(RXNE 读数据寄存器非空),我们要产生中断,那么我 们开启中断的方法是: USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启中断,接收到数据中断 我们在发送数据结束的时候(TC,发送完成)要产生中断,那么方法是: USART_ITConfig(USART1,USART_IT_TC,ENABLE); 8.获取相应中断状态。当我们使能了某个中断的时候,当该中断发生了,就会设置状态寄 存器中的某个标志位。经常我们在中断处理函数中,要判断该中断是哪种中断,使用的函数是: ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT) 比如我们使能了串口发送完成中断,那么当中断发生了, 我们便可以在中断处理函数中调用这 个函数来判断到底是否是串口发送完成中断,方法是: USART_GetITStatus(USART1, USART_IT_TC) 返回值是 SET,说明是串口发送完成中断发生。 通过以上的讲解,我们就可以达到串口最基本的配置了,关于串口更详细的介绍,请参考 《STM32 参考手册》第 516 页至 548 页,通用同步异步收发器一章。 9.2 硬件设计 本实验需要用到的硬件资源有: 1) 指示灯 DS0 2) 串口 1 串口 1 之前还没有介绍过,本实验用到的串口 1 与 USB 串口并没有在 PCB 上连接在一起, 需要通过跳线帽来连接一下。这里我们把 P6 的 RXD 和 TXD 用跳线帽与 PA9 和 PA10 连接起 来。如图 9.2.1 所示: 189 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 9.2.1 硬件连接图示意图 连接上这里之后,我们在硬件上就设置完成了,可以开始软件设计了。 9.3 软件设计 本章的代码设计,比前两章简单很多,因为我们的串口初始化代码和接收代码就是用我们 之前介绍的 SYSTEM 文件夹下的串口部分的内容。这里我们对代码部分稍作讲解。 打开串口实验工程,然后在 SYSTEM 组下双击 usart.c,我们就可以看到该文件里面的代码, 先介绍 uart_init 函数,该函数代码如下: //初始化 IO 串口 1 //bound:波特率 void uart_init(u32 bound) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; //①串口时钟使能,GPIO 时钟使能,复用时钟使能 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1| RCC_APB2Periph_GPIOA, ENABLE); //使能 USART1,GPIOA 时钟 //②串口复位 USART_DeInit(USART1); //③GPIO 端口模式设置 //复位串口 1 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //ISART1_TX PA.9 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOA, &GPIO_InitStructure); //复用推挽输出 //初始化 GPIOA.9 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; //USART1_RX PA.10 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入 GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 GPIOA.10 //④串口参数初始化 USART_InitStructure.USART_BaudRate = bound; //波特率设置 USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长为 8 位 USART_InitStructure.USART_StopBits = USART_StopBits_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_Init(USART1, &USART_InitStructure); //初始化串口 190 STM32F1 开发指南(库函数版) #if EN_USART1_RX //⑤初始化 NVIC ALIENTEK 战舰 STM32F103 V3 开发板教程 //如果使能了接收 NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); //⑤开启中断 USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //抢占优先级 3 //子优先级 3 //IRQ 通道使能 //中断优先级初始化 //开启中断 #endif //⑥使能串口 USART_Cmd(USART1, ENABLE); //使能串口 } 从该代码可以看出,其初始化串口的过程,和我们前面介绍的一致。我们用标号①~⑥标 示了顺序: ① 串口时钟使能,GPIO 时钟使能 ② 串口复位 ③ GPIO 端口模式设置 ④ 串口参数初始化 ⑤ 初始化 NVIC 并且开启中断 ⑥ 使能串口 这里需要重申的是,对于复用功能下的 GPIO 模式怎么判定,这个需要查看《中文参 考手册 V10》P110 的表格“8.1.11 外设的 GPIO 配置”,这个我们在前面的端口复用章节有 提到,这里 还是拿出来再讲解一下吧。查看手册得知,配置全双工的串口 1,那么 TX(PA9) 管脚需要配置为推挽复用输出,RX(PA10)管脚配置为浮空输入或者带上拉输入。模式配置 参考下面表格: 表 9.3.1 串口 GPIO 模式配置表 对于 NVIC 中断优先级管理,我们在前面的章节(4.5 中断优先级管理)也有讲解,这里不 做重复讲解了。 这 里 需 要 注 意 一 点 , 因 为 我 们 使 用 到 了 串 口 的 中 断 接 收 , 必 须 在 usart.h 里 面 设 置 EN_USART1_RX 为 1(默认设置就是 1 的) 。该函数才会配置中断使能,以及开启串口 1 的 NVIC 中断。这里我们把串口 1 中断放在组 2,优先级设置为组 2 里面的最低。 接下来,根据之前讲解的步骤 7,还要编写中断服务函数。串口 1 的中断服务函数 USART1_IRQHandler,在 5.3.3 已经有详细介绍了,这里我们就不再介绍了,大家可以翻过去 看看。 介绍完了这两个函数,我们回到 main.c,在 main.c 里面编写如下代码: #include "led.h" #include "delay.h" 191 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 #include "key.h" #include "sys.h" #include "usart.h" int main(void) { u8 t; u8 len; u16 times=0; delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 设 置 NVIC 中 断 分 组 2 uart_init(115200); //串口初始化波特率为 115200 LED_Init(); //LED 端口初始化 KEY_Init(); //初始化与按键连接的硬件接口 while(1) { if(USART_RX_STA&0x8000) { len=USART_RX_STA&0x3f; printf("\r\n 您发送的消息为:\r\n\r\n"); //得到此次接收到的数据长度 for(t=0;tCR 的第六位) 变成 0 前被刷新,看门狗电路在达到预置的时间周期时,会产生一个 MCU 复位。在递减计数 器达到窗口配置寄存器(WWDG->CFR)数值之前,如果 7 位的递减计数器数值(在控制寄存器中) 被刷新, 那么也将产生一个 MCU 复位。这表明递减计数器需要在一个有限的时间窗口中被刷 新。他们的关系可以用图 12.1.1 来说明: 图 12.1.1 窗口看门狗工作示意图 图 12.1.1 中,T[6:0]就是 WWDG_CR 的低七位,W[6:0]即是 WWDG->CFR 的低七位。T[6:0] 就是窗口看门狗的计数器,而 W[6:0]则是窗口看门狗的上窗口,下窗口值是固定的(0X40)。 当窗口看门狗的计数器在上窗口值之外被刷新,或者低于下窗口值都会产生复位。 上窗口值(W[6:0])是由用户自己设定的,根据实际要求来设计窗口值,但是一定要确保 窗口值大于 0X40,否则窗口就不存在了。 窗口看门狗的超时公式如下: Twwdg=(4096×2^WDGTB×(T[5:0]+1)) /Fpclk1; 其中: Twwdg:WWDG 超时时间(单位为 ms) Fpclk1:APB1 的时钟频率(单位为 Khz) WDGTB:WWDG 的预分频系数 T[5:0]:窗口看门狗的计数器低 6 位 根据上面的公式,假设 Fpclk1=36Mhz,那么可以得到最小-最大超时时间表如表 12.1.1 所 示: 209 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 表 12.1.1 36M 时钟下窗口看门狗的最小最大超时表 接下来,我们介绍窗口看门狗的 3 个寄存器。首先介绍控制寄存器(WWDG_CR),该寄 存器的各位描述如图 12.1.2 所示: 图 12.1.2 WWDG_CR 寄存器各位描述 可以看出,这里我们的 WWDG_CR 只有低八位有效,T[6:0]用来存储看门狗的计数器值, 随时更新的,每个窗口看门狗计数周期(4096×2^ WDGTB)减 1。当该计数器的值从 0X40 变 为 0X3F 的时候,将产生看门狗复位。 WDGA 位则是看门狗的激活位,该位由软件置 1,以启动看门狗,并且一定要注意的是该 位一旦设置,就只能在硬件复位后才能清零了。 窗口看门狗的第二个寄存器是配置寄存器(WWDG_CFR),该寄存器的各位及其描述如图 12.1.3 所示: 图 12.1.3 WWDG_ CFR 寄存器各位描述 该位中的 EWI 是提前唤醒中断,也就是在快要产生复位的前一段时间(T[6:0]=0X40)来 提醒我们,需要进行喂狗了,否则将复位!因此,我们一般用该位来设置中断,当窗口看门狗 的计数器值减到 0X40 的时候,如果该位设置,并开启了中断,则会产生中断,我们可以在中 断里面向 WWDG_CR 重新写入计数器的值,来达到喂狗的目的。注意这里在进入中断后,必 须在不大于 1 个窗口看门狗计数周期的时间(在 PCLK1 频率为 36M 且 WDGTB 为 0 的条件下, 该时间为 113us)内重新写 WWDG_CR,否则,看门狗将产生复位! 最后我们要介绍的是状态寄存器(WWDG_SR),该寄存器用来记录当前是否有提前唤醒 的标志。该寄存器仅有位 0 有效,其他都是保留位。当计数器值达到 40h 时,此位由硬件置 1。 它必须通过软件写 0 来清除。对此位写 1 无效。即使中断未被使能,在计数器的值达到 0X40 210 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 的时候,此位也会被置 1。 在介绍完了窗口看门狗的寄存器之后,我们介绍要如何启用 STM32 的窗口看门狗。这里 我们介绍库函数中用中断的方式来喂狗的方法,窗口看门狗库函数相关源码和定义分布在文件 stm32f10x_wwdg.c 文件和头文件 stm32f10x_wwdg.h 中。步骤如下: 1)使能 WWDG 时钟 WWDG 不同于 IWDG,IWDG 有自己独立的 40Khz 时钟,不存在使能问题。而 WWDG 使用的是 PCLK1 的时钟,需要先使能时钟。方法是: RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG, ENABLE); // WWDG 时钟使能 2)设置窗口值和分频数 设置窗口值的函数是: void WWDG_SetWindowValue(uint8_t WindowValue); 这个函数的入口参数 WindowValue 用来设置看门狗的上窗口值。 设置分频数的函数是: void WWDG_SetPrescaler(uint32_t WWDG_Prescaler); 这个函数同样只有一个入口参数,用来设置看门狗的分频值。 3)开启 WWDG 中断并分组 开启 WWDG 中断的函数为: WWDG_EnableIT(); //开启窗口看门狗中断 接下来是进行中断优先级配置,这里就不重复了,使用 NVIC_Init()函数即可。 4) 设置计数器初始值并使能看门狗 这一步在库函数里面是通过一个函数实现的: void WWDG_Enable(uint8_t Counter); 该函数既设置了计数器初始值,同时使能了窗口看门狗。 5) 编写中断服务函数 在最后,还是要编写窗口看门狗的中断服务函数,通过该函数来喂狗,喂狗要快,否则当 窗口看门狗计数器值减到 0X3F 的时候,就会引起软复位了。在中断服务函数里面也要将状态 寄存器的 EWIF 位清空。 完成了以上 5 个步骤之后,我们就可以使用 STM32 的窗口看门狗了。这一章的实验,我 们将通过 DS0 来指示 STM32 是否被复位了,如果被复位了就会点亮 300ms。DS1 用来指示中 断喂狗,每次中断喂狗翻转一次。 12.2 硬件设计 本实验用到的硬件资源有: 1) 指示灯 DS0 和 DS1 2) 窗口看门狗 其中指示灯前面介绍过了,窗口看门狗属于 STM32 的内部资源,只需要软件设置好即可 正常工作。我们通过 DS0 和 DS1 来指示 STM32 的复位情况和窗口看门狗的喂狗情况。 12.3 软件设计 打开我们的窗口看门狗实验可以看到,相对于独立看门狗,我们只增加了窗口看门狗相关 的库函数支持文件 stm32f10x_wwdg.c/stm32f10x_wwdg./c,然后在 wdg.c 加入如下代码(之前代 码保留): //保存 WWDG 计数器的设置值,默认为最大. 211 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 u8 WWDG_CNT=0x7f; //初始化窗口看门狗 //tr :T[6:0],计数器值 //wr :W[6:0],窗口值 //fprer:分频系数(WDGTB),仅最低 2 位有效 //Fwwdg=PCLK1/(4096*2^fprer). void WWDG_Init(u8 tr,u8 wr,u32 fprer) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG, ENABLE); // WWDG 时钟使能 WWDG_CNT=tr&WWDG_CNT; //初始化 WWDG_CNT. WWDG_SetPrescaler(fprer); //设置 IWDG 预分频值 WWDG_SetWindowValue(wr); //设置窗口值 WWDG_Enable(WWDG_CNT); //使能看门狗,设置 counter WWDG_ClearFlag(); //清除提前唤醒中断标志位 WWDG_NVIC_Init(); //初始化窗口看门狗 NVIC WWDG_EnableIT(); //开启窗口看门狗中断 } //重设置 WWDG 计数器的值 void WWDG_Set_Counter(u8 cnt) { WWDG_Enable(cnt); //使能看门狗,设置 counter . } //窗口看门狗中断服务程序 void WWDG_NVIC_Init() { NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = WWDG_IRQn; //WWDG 中断 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //抢占 2 子优先级 3 组 2 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //抢占 2,子优先级 3,组 2 NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE; NVIC_Init(&NVIC_InitStructure); //NVIC 初始化 } void WWDG_IRQHandler(void) { WWDG_SetCounter(WWDG_CNT); //当禁掉此句后,窗口看门狗将产生复位 WWDG_ClearFlag(); LED1=!LED1; //清除提前唤醒中断标志位 //LED 状态翻转 } 新增的这四个函数都比较简单,第一个函数 void WWDG_Init(u8 tr,u8 wr,u8 fprer)用来 设置 WWDG 的初始化值。包括看门狗计数器的值和看门狗比较值等。该函数就是按照我们上 一节讲解的 4 个步骤设计出来的代码。注意到这里有个全局变量 WWDG_CNT,该变量用来保 存最初设置 WWDG_CR 计数器的值。在后续的中断服务函数里面,就又把该数值放回到 212 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 WWDG_CR 上。 WWDG_Set_Counter()函数比较简单,就是用来重设窗口看门狗的计数器值的。该函数很简 单,我们就不多说了。 然后是中断分组函数,这个函数非常简单,之前有讲解,这里不重复。 最后在中断服务函数里面,先重设窗口看门狗的计数器值,然后清除提前唤醒中断标志。 最后对 LED1(DS1)取反,来监测中断服务函数的执行了状况。我们再把这几个函数名加入到 头文件里面去,以方便其他文件调用。 在完成了以上部分之后,我们就回到主函数,代码如下: int main(void) { delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置 NVIC 中断分组 2 uart_init(115200); //串口初始化波特率为 115200 LED_Init(); //LED 初始化 KEY_Init(); //按键初始化 LED0=0; delay_ms(300); WWDG_Init(0X7F,0X5F,WWDG_Prescaler_8);//计数器值为 7f,窗口寄存器为 5f, //分频数为 8 while(1) { LED0=1; } } 该函数通过 LED0(DS0)来指示是否正在初始化。而 LED1(DS1)用来指示是否发生了中 断。我们先让 LED0 亮 300ms,然后关闭以用于判断是否有复位发生了。在初始化 WWDG 之 后,我们回到死循环,关闭 LED1,并等待看门狗中断的触发/复位。 在编译完成之后,我们就可以下载这个程序到战舰 STM32 开发板上,看看结果是不是和 我们设计的一样。 12.4 下载验证 将代码下载到战舰 STM32 后,可以看到 DS0 亮一下之后熄灭,紧接着 DS1 开始不停的闪 烁。每秒钟闪烁 5 次左右,和我们预期的一致,说明我们的实验是成功的。 213 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第十三章 定时器中断实验 这一章,我们将向大家介绍如何使用 STM32F1 的通用定时器,STM32F1 的定时器功能十 分强大,有 TIME1 和 TIME8 等高级定时器,也有 TIME2~TIME5 等通用定时器,还有 TIME6 和 TIME7 等基本定时器。在《STM32 参考手册》里面,定时器的介绍占了 1/5 的篇幅,足见 其重要性。在本章中,我们将利用 TIM3 的定时器中断来控制 DS1 的翻转,在主函数用 DS0 的翻转来提示程序正在运行。本章,我们选择难度适中的通用定时器来介绍,本章将分为如下 几个部分: 13.1 STM32F1 通用定时器简介 13.2 硬件设计 13.3 软件设计 13.4 下载验证 214 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 13.1 STM32 通用定时器简介 STM32F1 的通用定时器是一个通过可编程预分频器(PSC)驱动的 16 位自动装载计数器 (CNT)构成。STM32 的通用定时器可以被用于:测量输入信号的脉冲长度(输入捕获)或者产 生输出波形(输出比较和 PWM)等。 使用定时器预分频器和 RCC 时钟控制器预分频器,脉冲长 度和波形周期可以在几个微秒到几个毫秒间调整。STM32 的每个通用定时器都是完全独立的, 没有互相共享的任何资源。 STM3F1 的通用 TIMx (TIM2、TIM3、TIM4 和 TIM5)定时器功能包括: 1)16 位向上、向下、向上/向下自动装载计数器(TIMx_CNT)。 2)16 位可编程(可以实时修改)预分频器(TIMx_PSC),计数器时钟频率的分频系数为 1~ 65535 之间的任意数值。 3)4 个独立通道(TIMx_CH1~4),这些通道可以用来作为: A.输入捕获 B.输出比较 C.PWM 生成(边缘或中间对齐模式) D.单脉冲模式输出 4)可使用外部信号(TIMx_ETR)控制定时器和定时器互连(可以用 1 个定时器控制另外 一个定时器)的同步电路。 5)如下事件发生时产生中断/DMA: A.更新:计数器向上溢出/向下溢出,计数器初始化(通过软件或者内部/外部触发) B.触发事件(计数器启动、停止、初始化或者由内部/外部触发计数) C.输入捕获 D.输出比较 E.支持针对定位的增量(正交)编码器和霍尔传感器电路 F.触发输入作为外部时钟或者按周期的电流管理 由于 STM32 通用定时器比较复杂,这里我们不再多介绍,请大家直接参考《STM32 参考 手册》第 253 页,通用定时器一章。为了深入了解 STM32 的通用寄存器,下面我们先介绍一 下与我们这章的实验密切相关的几个通用定时器的寄存器。 首先是控制寄存器 1(TIMx_CR1),该寄存器的各位描述如图 13.1.1 所示: 215 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 216 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 13.1.1 TIMx_CR1 寄存器各位描述 首先我们来看看 TIMx_CR1 的最低位,也就是计数器使能位,该位必须置 1,才能让定时 器开始计数。从第 4 位 DIR 可以看出默认的计数方式是向上计数,同时也可以向下计数,第 5,6 位是设置计数对齐方式的。从第 8 和第 9 位可以看出,我们还可以设置定时器的时钟分频因子 为 1,2,4 。 接 下 来 介 绍 第 二 个 与 我 们 这 章 密 切 相 关 的 寄 存 器 : DMA/ 中 断 使 能 寄 存 器 (TIMx_DIER)。该寄存器是一个 16 位的寄存器,其各位描述如图 13.1.2 所示: 图 13.1.2 TIMx_ DIER 寄存器各位描述 这里我们同样仅关心它的第 0 位,该位是更新中断允许位,本章用到的是定时器的更新中 断,所以该位要设置为 1,来允许由于更新事件所产生的中断。 接下来我们看第三个与我们这章有关的寄存器:预分频寄存器(TIMx_PSC)。该寄存器用 设置对时钟进行分频,然后提供给计数器,作为计数器的时钟。该寄存器的各位描述如图 13.1.3 所示: 图 13.1.3 TIMx_ PSC 寄存器各位描述 这里,定时器的时钟来源有 4 个: 1)内部时钟(CK_INT) 2)外部时钟模式 1:外部输入脚(TIx) 3)外部时钟模式 2:外部触发输入(ETR) 4)内部触发输入(ITRx):使用 A 定时器作为 B 定时器的预分频器(A 为 B 提供时钟)。 这些时钟,具体选择哪个可以通过 TIMx_SMCR 寄存器的相关位来设置。这里的 CK_INT 时钟是从 APB1 倍频的来的,除非 APB1 的时钟分频数设置为 1,否则通用定时器 TIMx 的时钟 是 APB1 时钟的 2 倍,当 APB1 的时钟不分频的时候,通用定时器 TIMx 的时钟就等于 APB1 的时钟。这里还要注意的就是高级定时器的时钟不是来自 APB1,而是来自 APB2 的。 这里顺带介绍一下 TIMx_CNT 寄存器,该寄存器是定时器的计数器,该寄存器存储了当前 定时器的计数值。 接着我们介绍自动重装载寄存器(TIMx_ARR),该寄存器在物理上实际对应着 2 个寄存器。 一个是程序员可以直接操作的,另外一个是程序员看不到的,这个看不到的寄存器在《STM32 参考手册》里面被叫做影子寄存器。事实上真正起作用的是影子寄存器。根据 TIMx_CR1 寄存 器中 APRE 位的设置:APRE=0 时,预装载寄存器的内容可以随时传送到影子寄存器,此时 2 者是连通的;而 APRE=1 时,在每一次更新事件(UEV)时,才把预装在寄存器的内容传送到 影子寄存器。 自动重装载寄存器的各位描述如图 13.1.4 所示: 217 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 13.1.4 TIMx_ ARR 寄存器各位描述 最后,我们要介绍的寄存器是:状态寄存器(TIMx_SR)。该寄存器用来标记当前与定时 器相关的各种事件/中断是否发生。该寄存器的各位描述如图 13.1.5 所示: 图 13.1.5 TIMx_ SR 寄存器各位描述 关于这些位的详细描述,请参考《STM32 参考手册》第 282 页。 只要对以上几个寄存器进行简单的设置,我们就可以使用通用定时器了,并且可以产生中 断。 这一章,我们将使用定时器产生中断,然后在中断服务函数里面翻转 DS1 上的电平,来指 示定时器中断的产生。接下来我们以通用定时器 TIM3 为实例,来说明要经过哪些步骤,才能 达到这个要求,并产生中断。这里我们就对每个步骤通过库函数的实现方式来描述。首先要提 到的是,定时器相关的库函数主要集中在固件库文件 stm32f10x_tim.h 和 stm32f10x_tim.c 文件 中。 1)TIM3 时钟使能。 TIM3 是挂载在 APB1 之下,所以我们通过 APB1 总线下的使能使能函数来使能 TIM3。调 用的函数是: RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //时钟使能 2)初始化定时器参数,设置自动重装值,分频系数,计数方式等。 在库函数中,定时器的初始化参数是通过初始化函数 TIM_TimeBaseInit 实现的: voidTIM_TimeBaseInit(TIM_TypeDef*TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct); 第一个参数是确定是哪个定时器,这个比较容易理解。第二个参数是定时器初始化参数结 构体指针,结构体类型为 TIM_TimeBaseInitTypeDef,下面我们看看这个结构体的定义: typedef struct { uint16_t TIM_Prescaler; uint16_t TIM_CounterMode; uint16_t TIM_Period; uint16_t TIM_ClockDivision; uint8_t TIM_RepetitionCounter; } TIM_TimeBaseInitTypeDef; 这个结构体一共有 5 个成员变量,要说明的是,对于通用定时器只有前面四个参数有用, 最后一个参数 TIM_RepetitionCounter 是高级定时器才有用的,这里不多解释。 第一个参数 TIM_Prescaler 是用来设置分频系数的,刚才上面有讲解。 218 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第二个参数 TIM_CounterMode 是用来设置计数方式,上面讲解过,可以设置为向上计数, 向下计数方式还有中央对齐计数方式,比较常用的是向上计数模式 TIM_CounterMode_Up 和向 下计数模式 TIM_CounterMode_Down。 第三个参数是设置自动重载计数周期值,这在前面也已经讲解过。 第四个参数是用来设置时钟分频因子。 针对 TIM3 初始化范例代码格式: TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period = 5000; TIM_TimeBaseStructure.TIM_Prescaler =7199; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); 3)设置 TIM3_DIER 允许更新中断。 因为我们要使用 TIM3 的更新中断,寄存器的相应位便可使能更新中断。在库函数里面定 时器中断使能是通过 TIM_ITConfig 函数来实现的: void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState); 第一个参数是选择定时器号,这个容易理解,取值为 TIM1~TIM17。 第二个参数非常关键,是用来指明我们使能的定时器中断的类型,定时器中断的类型有很 多种,包括更新中断 TIM_IT_Update,触发中断 TIM_IT_Trigger,以及输入捕获中断等等。 第三个参数就很简单了,就是失能还是使能。 例如我们要使能 TIM3 的更新中断,格式为: TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE ); 4)TIM3 中断优先级设置。 在定时器中断使能之后,因为要产生中断,必不可少的要设置 NVIC 相关寄存器,设置中 断优先级。之前多次讲解到用 NVIC_Init 函数实现中断优先级的设置,这里就不重复讲解。 5)允许 TIM3 工作,也就是使能 TIM3。 光配置好定时器还不行,没有开启定时器,照样不能用。我们在配置完后要开启定时器, 通过 TIM3_CR1 的 CEN 位来设置。在固件库里面使能定时器的函数是通过 TIM_Cmd 函数来实 现的: void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState) 这个函数非常简单,比如我们要使能定时器 3,方法为: TIM_Cmd(TIM3, ENABLE); //使能 TIMx 外设 6)编写中断服务函数。 在最后,还是要编写定时器中断服务函数,通过该函数来处理定时器产生的相关中断。在 中断产生后,通过状态寄存器的值来判断此次产生的中断属于什么类型。然后执行相关的操作, 我们这里使用的是更新(溢出)中断,所以在状态寄存器 SR 的最低位。在处理完中断之后应 该向 TIM3_SR 的最低位写 0,来清除该中断标志。 在固件库函数里面,用来读取中断状态寄存器的值判断中断类型的函数是: ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t) 该函数的作用是,判断定时器 TIMx 的中断类型 TIM_IT 是否发生中断。比如,我们要判断定 时器 3 是否发生更新(溢出)中断,方法为: if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET){} 固件库中清除中断标志位的函数是: 219 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT) 该函数的作用是,清除定时器 TIMx 的中断 TIM_IT 标志位。使用起来非常简单,比如我们在 TIM3 的溢出中断发生后,我们要清除中断标志位,方法是: TIM_ClearITPendingBit(TIM3, TIM_IT_Update ); 这里需要说明一下,固件库还提供了两个函数用来判断定时器状态以及清除定时器状态标 志位的函数 TIM_GetFlagStatus 和 TIM_ClearFlag,他们的作用和前面两个函数的作用类似。只 是在 TIM_GetITStatus 函数中会先判断这种中断是否使能,使能了才去判断中断标志位,而 TIM_GetFlagStatus 直接用来判断状态标志位。 通过以上几个步骤,我们就可以达到我们的目的了,使用通用定时器的更新中断,来控制 DS1 的亮灭。 13.2 硬件设计 本实验用到的硬件资源有: 1) 指示灯 DS0 和 DS1 2) 定时器 TIM3 本章将通过 TIM3 的中断来控制 DS1 的亮灭,DS1 是直接连接到 PE5 上的,这个前面已经 有介绍了。而 TIM3 属于 STM32 的内部资源,只需要软件设置即可正常工作。 13.3 软件设计 软件设计我们直接打开我们光盘实验 8 定时器中断实验即可。我们可以看到我们的工程中 的 HARDWARE 下面比以前多了一个 time.c 文件(包括头文件 time.h),这两个文件是我们自己 编写。同时还引入了定时器相关的固件库函数文件 stm32f10x_tim.c 和头文件 stm32f10x_tim.h。 下面我们来看看我们的 time.c 文件。 time.c 文件代码: #include "timer.h" #include "led.h" //通用定时器 3 中断初始化 //这里时钟选择为 APB1 的 2 倍,而 APB1 为 36M //arr:自动重装值。 //psc:时钟预分频数 //这里使用的是定时器 3! void TIM3_Int_Init(u16 arr,u16 psc) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //①时钟 TIM3 使能 //定时器 TIM3 初始化 TIM_TimeBaseStructure.TIM_Period = arr; //设置自动重装载寄存器周期的值 TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置时钟频率除数的预分频值 TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM 向上计数 220 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //②初始化 TIM3 TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE ); //③允许更新中断 //中断优先级 NVIC 设置 NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; //TIM3 中断 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //先占优先级 0 级 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //从优先级 3 级 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道被使能 NVIC_Init(&NVIC_InitStructure); //④初始化 NVIC 寄存器 TIM_Cmd(TIM3, ENABLE); //⑤使能 TIM3 } //定时器 3 中断服务程序⑥ void TIM3_IRQHandler(void) //TIM3 中断 { if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) //检查 TIM3 更新中断发生与否 { TIM_ClearITPendingBit(TIM3, TIM_IT_Update ); //清除 TIM3 更新中断标志 LED1=!LED1; } } 该文件下包含一个中断服务函数和一个定时器 3 中断初始化函数,中断服务函数比较简单, 在每次中断后,判断 TIM3 的中断类型,如果中断类型正确(溢出中断),则执行 LED1(DS1) 的取反。 TIM3_Int_Init()函数就是执行我们上面 13.1 节介绍的那 6 个步骤,我们分别用标号①~⑥来 标注,该函数的 2 个参数用来设置 TIM3 的溢出时间。在前面时钟系统部分我们讲解过,系统 初始化的时候在默认的系统初始化函数 SystemInit 函数里面已经初始化 APB1 的时钟为 2 分频, 所以 APB1 的时钟为 36M,而从 STM32 的内部时钟树图得知:当 APB1 的时钟分频数为 1 的 时候,TIM2~7 的时钟为 APB1 的时钟,而如果 APB1 的时钟分频数不为 1,那么 TIM2~7 的时 钟频率将为 APB1 时钟的两倍。因此,TIM3 的时钟为 72M,再根据我们设计的 arr 和 psc 的值, 就可以计算中断时间了。计算公式如下: Tout= ((arr+1)*(psc+1))/Tclk; 其中: Tclk:TIM3 的输入时钟频率(单位为 Mhz)。 Tout:TIM3 溢出时间(单位为 us)。 timer.h 文件的代码非常简单,一些函数申明,这里就不讲解。 最后,我们在主程序里面输入如下代码: int main(void) { delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置 NVIC 中断分组 2 uart_init(115200); //串口初始化波特率为 115200 221 STM32F1 开发指南(库函数版) LED_Init(); TIM3_Int_Init(4999,7199); ALIENTEK 战舰 STM32F103 V3 开发板教程 //LED 端口初始化 //10Khz 的计数频率,计数到 5000 为 500ms while(1) { LED0=!LED0; delay_ms(200); } } 这里的代码和之前大同小异,此段代码对 TIM3 进行初始化之后,进入死循环等待 TIM3 溢出中断,当 TIM3_CNT 的值等于 TIM3_ARR 的值的时候,就会产生 TIM3 的更新中断,然 后在中断里面取反 LED1,TIM3_CNT 再从 0 开始计数。根据上面的公式,我们可以算出中断 溢出时间为 500ms,即 Tout= ((4999+1)*( 7199+1))/72=500000us=500ms。 13.4 下载验证 在完成软件设计之后,我们将编译好的文件下载到战舰 STM32 开发板上,观看其运行结 果是否与我们编写的一致。如果没有错误,我们将看 DS0 不停闪烁(每 400ms 闪烁一次),而 DS1 也是不停的闪烁,但是闪烁时间较 DS0 慢(1s 一次)。 222 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第十四章 PWM 输出实验 上一章,我们介绍了 STM32F1 的通用定时器 TIM3,用该定时器的中断来控制 DS1 的闪烁, 这一章,我们将向大家介绍如何使用 STM32F1 的 TIM3 来产生 PWM 输出。在本章中,我们将 使用 TIM3 的通道 2,把通道 2 重映射到 PB5,产生 PWM 来控制 DS0 的亮度。本章分为如下 几个部分: 14.1 PWM 简介 14.2 硬件设计 14.3 软件设计 14.4 下载验证 223 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 14.1 PWM 简介 脉冲宽度调制(PWM),是英文“Pulse Width Modulation”的缩写,简称脉宽调制,是利用 微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。简单一点,就是对脉冲宽 度的控制。 STM32 的定时器除了 TIM6 和 7。其他的定时器都可以用来产生 PWM 输出。其中高级定 时器 TIM1 和 TIM8 可以同时产生多达 7 路的 PWM 输出。而通用定时器也能同时产生多达 4 路的 PWM 输出,这样,STM32 最多可以同时产生 30 路 PWM 输出!这里我们仅利用 TIM3 的 CH2 产生一路 PWM 输出。如果要产生多路输出,大家可以根据我们的代码稍作修改即可。 同样,我们首先通过对 PWM 相关的寄存器进行讲解,大家了解了定时器 TIM3 的 PWM 原理之后,我们再讲解怎么使用库函数产生 PWM 输出。 要使 STM32 的通用定时器 TIMx 产生 PWM 输出,除了上一章介绍的寄存器外,我们还会 用 到 3 个 寄 存 器 , 来 控 制 PWM 的 。 这 三 个 寄 存 器 分 别 是 : 捕 获 / 比 较 模 式 寄 存 器 (TIMx_CCMR1/2)、捕获/比较使能寄存器(TIMx_CCER)、捕获/比较寄存器(TIMx_CCR1~4)。 接下来我们简单介绍一下这三个寄存器。 首先是捕获/比较模式寄存器(TIMx_CCMR1/2),该寄存器总共有 2 个,TIMx _CCMR1 和 TIMx _CCMR2。TIMx_CCMR1 控制 CH1 和 2,而 TIMx_CCMR2 控制 CH3 和 4。该寄存器 的各位描述如图 14.1.1 所示: 图 14.1.1 TIMx_CCMR1 寄存器各位描述 该寄存器的有些位在不同模式下,功能不一样,所以在图 14.1.1 中,我们把寄存器分了 2 层,上面一层对应输出而下面的则对应输入。关于该寄存器的详细说明,请参考《STM32 参考 手册》第 288 页,14.4.7 一节。这里我们需要说明的是模式设置位 OCxM,此部分由 3 位组成。 总共可以配置成 7 种模式,我们使用的是 PWM 模式,所以这 3 位必须设置为 110/111。这两种 PWM 模式的区别就是输出电平的极性相反。 接下来,我们介绍捕获/比较使能寄存器(TIMx_CCER),该寄存器控制着各个输入输出通 道的开关。该寄存器的各位描述如图 14.1.2 所示: 图 14.1.2 TIMx_ CCER 寄存器各位描述 该寄存器比较简单,我们这里只用到了 CC2E 位,该位是输入/捕获 2 输出使能位,要想 PWM 从 IO 口输出,这个位必须设置为 1,所以我们需要设置该位为 1。该寄存器更详细的介 绍了,请参考《STM32 参考手册》第 292 页,14.4.9 这一节。 最后,我们介绍一下捕获/比较寄存器(TIMx_CCR1~4),该寄存器总共有 4 个,对应 4 个 输通道 CH1~4。因为这 4 个寄存器都差不多,我们仅以 TIMx_CCR1 为例介绍,该寄存器的各 位描述如图 14.1.3 所示: 224 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 14.1.3 寄存器 TIMx_ CCR1 各位描述 在输出模式下,该寄存器的值与 CNT 的值比较,根据比较结果产生相应动作。利用这点, 我们通过修改这个寄存器的值,就可以控制 PWM 的输出脉宽了。本章,我们使用的是 TIM3 的通道 2,所以我们需要修改 TIM3_CCR2 以实现脉宽控制 DS0 的亮度。 我们要利用 TIM3 的 CH2 输出 PWM 来控制 DS0 的亮度,但是 TIM3_CH2 默认是接在 PA7 上面的,而我们的 DS0 接在 PB5 上面,如果普通 MCU,可能就只能用飞线把 PA7 飞到 PB5 上来实现了,不过,我们用的是 STM32,它比较高级,可以通过重映射功能,把 TIM3_CH2 映射到 PB5 上。 STM32 的重映射控制是由复用重映射和调试 IO 配置寄存器(AFIO_MAPR)控制的,该 寄存器的各位描述如图 14.1.4 所示: 图 14.1.4 寄存器 AFIO_MAPR 各位描述 我们这里用到的是 TIM3 的重映射,从上图可以看出,TIM3_REMAP 是由[11:10]这 2 个位 控制的。TIM3_REMAP[1:0]重映射控制表如表 14.1.1 所示: 表 14.1.1 TIM3_REMAP 重映射控制表 默认条件下,TIM3_REMAP[1:0]为 00,是没有重映射的,所以 TIM3_CH1~TIM3_CH4 分 别是接在 PA6、PA7、PB0 和 PB1 上的,而我们想让 TIM3_CH2 映射到 PB5 上,则需要设置 TIM3_REMAP[1:0]=10,即部分重映射,这里需要注意,此时 TIM3_CH1 也被映射到 PB4 上了。 至此,我们把本章要用的几个相关寄存器都介绍完了,本章要实现通过重映射 TIM3_CH2 到 PB5 上,由 TIM3_CH2 输出 PWM 来控制 DS0 的亮度。下面我们介绍通过库函数来配置该 功能的步骤。 225 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 首先要提到的是,PWM 相关的函数设置在库函数文件 stm32f10x_tim.h 和 stm32f10x_tim.c 文件中。 1)开启 TIM3 时钟以及复用功能时钟,配置 PB5 为复用输出。 要使用 TIM3,我们必须先开启 TIM3 的时钟,这点相信大家看了这么多代码,应该明白了。 这里我们还要配置 PB5 为复用输出,这是因为 TIM3_CH2 通道将重映射到 PB5 上,此时,PB5 属于复用功能输出。库函数使能 TIM3 时钟的方法是: RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //使能定时器 3 时钟 这在前面一章已经提到过。库函数设置 AFIO 时钟的方法是: RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //复用时钟使能 这两行代码很容易组织,这里不做过多重复的讲解。设置 PB5 为复用功能输出的方法在前面的 几个实验都有类似的讲解,相信大家很明白,这里简单列出 GPIO 初始化的一行代码即可: GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出 2)设置 TIM3_CH2 重映射到 PB5 上。 因为 TIM3_CH2 默认是接在 PA7 上的,所以我们需要设置 TIM3_REMAP 为部分重映射(通 过 AFIO_MAPR 配置),让 TIM3_CH2 重映射到 PB5 上面。在库函数函数里面设置重映射的函 数是: void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState); 在前面 STM32 重映射章节 4.4.2 已经讲解过,STM32 重映射只能重映射到特定的端口。第一个 入口参数可以理解为设置重映射的类型,比如 TIM3 部分重映射入口参数为 GPIO_PartialRemap_TIM3,这点可以顾名思义了。所以 TIM3 部分重映射的库函数实现方法是: GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE); 3)初始化 TIM3,设置 TIM3 的 ARR 和 PSC。 在开启了 TIM3 的时钟之后,我们要设置 ARR 和 PSC 两个寄存器的值来控制输出 PWM 的 周期。当 PWM 周期太慢(低于 50Hz)的时候,我们就会明显感觉到闪烁了。因此,PWM 周 期在这里不宜设置的太小。这在库函数是通过 TIM_TimeBaseInit 函数实现的,在上一节定时器 中断章节我们已经有讲解,这里就不详细讲解,调用的格式为: TIM_TimeBaseStructure.TIM_Period = arr; //设置自动重装载值 TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置预分频值 TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数模式 TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据指定的参数初始化 TIMx 的 4)设置 TIM3_CH2 的 PWM 模式,使能 TIM3 的 CH2 输出。 接下来,我们要设置 TIM3_CH2 为 PWM 模式(默认是冻结的),因为我们的 DS0 是低电 平亮,而我们希望当 CCR2 的值小的时候,DS0 就暗,CCR2 值大的时候,DS0 就亮,所以我 们要通过配置 TIM3_CCMR1 的相关位来控制 TIM3_CH2 的模式。在库函数中,PWM 通道设 置是通过函数 TIM_OC1Init()~TIM_OC4Init()来设置的,不同的通道的设置函数不一样,这里我 们使用的是通道 2,所以使用的函数是 TIM_OC2Init()。 void TIM_OC2Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct); 这种初始化格式大家学到这里应该也熟悉了,所以我们直接来看看结构体 TIM_OCInitTypeDef 的定义: typedef struct { uint16_t TIM_OCMode; 226 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 uint16_t TIM_OutputState; uint16_t TIM_OutputNState; */ uint16_t TIM_Pulse; uint16_t TIM_OCPolarity; uint16_t TIM_OCNPolarity; uint16_t TIM_OCIdleState; uint16_t TIM_OCNIdleState; } TIM_OCInitTypeDef; 这里我们讲解一下与我们要求相关的几个成员变量: 参数 TIM_OCMode 设置模式是 PWM 还是输出比较,这里我们是 PWM 模式。 参数 TIM_OutputState 用来设置比较输出使能,也就是使能 PWM 输出到端口。 参数 TIM_OCPolarity 用来设置极性是高还是低。 其他的参数 TIM_OutputNState,TIM_OCNPolarity,TIM_OCIdleState 和 TIM_OCNIdleState 是 高级定时器 TIM1 和 TIM8 才用到的。 要实现我们上面提到的场景,方法是: TIM_OCInitTypeDef TIM_OCInitStructure; TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; //选择 PWM 模式 2 TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能 TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性高 TIM_OC2Init(TIM3, &TIM_OCInitStructure); //初始化 TIM3 OC2 5)使能 TIM3。 在完成以上设置了之后,我们需要使能 TIM3。使能 TIM3 的方法前面已经讲解过: TIM_Cmd(TIM3, ENABLE); //使能 TIM3 6)修改 TIM3_CCR2 来控制占空比。 最后,在经过以上设置之后,PWM 其实已经开始输出了,只是其占空比和频率都是固定 的,而我们通过修改 TIM3_CCR2 则可以控制 CH2 的输出占空比。继而控制 DS0 的亮度。 在库函数中,修改 TIM3_CCR2 占空比的函数是: void TIM_SetCompare2(TIM_TypeDef* TIMx, uint16_t Compare2); 理所当然,对于其他通道,分别有一个函数名字,函数格式为 TIM_SetComparex(x=1,2,3,4)。 通过以上 6 个步骤,我们就可以控制 TIM3 的 CH2 输出 PWM 波了。 14.2 硬件设计 本实验用到的硬件资源有: 1) 指示灯 DS0 2) 定时器 TIM3 这两个前面都有介绍,但是我们这里用到了 TIM3 的部分重映射功能,把 TIM3_CH2 直接 映射到了 PB5 上,而通过前面的学习,我们知道 PB5 和 DS0 是直接连接的,所以电路上并没 有任何变化。 14.3 软件设计 打开光盘里面的 PWM 输出实验代码可以看到相对上一章实验,我们在 timer.c 里面加入了 如下代码: 227 STM32F1 开发指南(库函数版) //TIM3 PWM 部分初始化 //PWM 输出初始化 //arr:自动重装值 //psc:时钟预分频数 ALIENTEK 战舰 STM32F103 V3 开发板教程 void TIM3_PWM_Init(u16 arr,u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //①使能定时器 3 时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB| RCC_APB2Periph_AFIO, ENABLE); //①使能 GPIO 和 AFIO 复用功能时钟 GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE); //②重映射 TIM3_CH2->PB5 //设置该引脚为复用输出功能,输出 TIM3 CH2 的 PWM 脉冲波形 GPIOB.5 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //TIM_CH2 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); //①初始化 GPIO //初始化 TIM3 TIM_TimeBaseStructure.TIM_Period = arr; //设置在自动重装载周期值 TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置预分频值 TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM 向上计数模式 TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //③初始化 TIMx //初始化 TIM3 Channel2 PWM 模式 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; //选择 PWM 模式 2 TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能 TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性高 TIM_OC2Init(TIM3, &TIM_OCInitStructure); //④初始化外设 TIM3 OC2 TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); //使能预装载寄存器 TIM_Cmd(TIM3, ENABLE); //⑤使能 TIM3 } 此部分代码包含了上面介绍的 PWM 输出设置的前 5 个步骤分别用标号①~⑤备注。这里我 们关于 TIM3 的设置就不再说了,这里提醒下:在配置 AFIO 相关寄存器的时候,必须先开启 辅助功能时钟。 头文件 timer.h 与上一章的不同是加入了 TIM3_PWM_Init 的声明,这个就没什么需要讲解 咯。 228 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 接下来,我们修改主程序里面的 main 函数如下: int main(void) { u16 led0pwmval=0; u8 dir=1; delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置 NVIC 中断分组 2 uart_init(115200); //串口初始化波特率为 115200 LED_Init(); //LED 端口初始化 TIM3_PWM_Init(899,0); //不分频,PWM 频率=72000/900=80Khz while(1) { delay_ms(10); if(dir)led0pwmval++; else led0pwmval--; if(led0pwmval>300)dir=0; if(led0pwmval==0)dir=1; TIM_SetCompare2(TIM3,led0pwmval); } } 这里,我们从死循环函数可以看出,我们将 led0pwmval 这个值设置为 PWM 比较值,也就 是通过 led0pwmval 来控制 PWM 的占空比,然后控制 led0pwmval 的值从 0 变到 300,然后又 从 300 变到 0,如此循环,因此 DS0 的亮度也会跟着从暗变到亮,然后又从亮变到暗。至于这 里的值,我们为什么取 300,是因为 PWM 的输出占空比达到这个值的时候,我们的 LED 亮度 变化就不大了(虽然最大值可以设置到 899),因此设计过大的值在这里是没必要的。至此,我 们的软件设计就完成了。 14.4 下载验证 在完成软件设计之后,将我们将编译好的文件下载到战舰 STM32 开发板上,观看其运行 结果是否与我们编写的一致。如果没有错误,我们将看 DS0 不停的由暗变到亮,然后又从亮变 到暗。每个过程持续时间大概为 3 秒钟左右。 实际运行结果如下图 14.4.1 所示: 图 14.4.1 PWM 控制 DS0 亮度 229 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第十五章 输入捕获实验 上一章,我们介绍了 STM32F1 的通用定时器作为 PWM 输出的使用方法,这一章,我们将 向大家介绍通用定时器作为输入捕获的使用。在本章中,我们将用 TIM5 的通道 1(PA0)来做 输入捕获,捕获 PA0 上高电平的脉宽(用 WK_UP 按键输入高电平),通过串口打印高电平脉 宽时间,从本章分为如下几个部分: 15.1 输入捕获简介 15.2 硬件设计 15.3 软件设计 15.4 下载验证 230 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 15.1 输入捕获简介 输入捕获模式可以用来测量脉冲宽度或者测量频率。STM32 的定时器,除了 TIM6 和 TIM7, 其他定时器都有输入捕获功能。STM32 的输入捕获,简单的说就是通过检测 TIMx_CHx 上的 边沿信号,在边沿信号发生跳变(比如上升沿/下降沿)的时候,将当前定时器的值(TIMx_CNT) 存放到对应的通道的捕获/比较寄存器(TIMx_CCRx)里面,完成一次捕获。同时还可以配置 捕获时是否触发中断/DMA 等。 本章我们用到 TIM5_CH1 来捕获高电平脉宽,也就是要先设置输入捕获为上升沿检测,记 录发生上升沿的时候 TIM5_CNT 的值。然后配置捕获信号为下降沿捕获,当下降沿到来时,发 生捕获,并记录此时的 TIM5_CNT 值。这样,前后两次 TIM5_CNT 之差,就是高电平的脉宽, 同时 TIM5 的计数频率我们是知道的,从而可以计算出高电平脉宽的准确时间。 接下来,我们介绍我们本章需要用到的一些寄存器配置,需要用到的寄存器有:TIMx_ARR、 TIMx_PSC、TIMx_CCMR1、TIMx_CCER、TIMx_DIER、TIMx_CR1、TIMx_CCR1 这些寄存 器在前面 2 章全部都有提到(这里的 x=5),我们这里就不再全部罗列了,我们这里针对性的介绍 这几个寄存器的配置。 首先 TIMx_ARR 和 TIMx_PSC,这两个寄存器用来设自动重装载值和 TIMx 的时钟分频, 用法同前面介绍的,我们这里不再介绍。 再来看看捕获/比较模式寄存器 1:TIMx_CCMR1,这个寄存器在输入捕获的时候,非常有 用,有必要重新介绍,该寄存器的各位描述如图 15.1.1 所示: 图 15.1.1 TIMx_CCMR1 寄存器各位描述 当在输入捕获模式下使用的时候,对应图 15.1.1 的第二行描述,从图中可以看出, TIMx_CCMR1 明显是针对 2 个通道的配置,低八位[7:0]用于捕获/比较通道 1 的控制,而高八 位[15:8]则用于捕获/比较通道 2 的控制,因为 TIMx 还有 CCMR2 这个寄存器,所以可以知道 CCMR2 是用来控制通道 3 和通道 4(详见《STM32 参考手册》290 页,14.4.8 节)。 这里我们用到的是 TIM5 的捕获/比较通道 1,我们重点介绍 TIMx_CMMR1 的[7:0]位(其 实高 8 位配置类似),TIMx_CMMR1 的[7:0]位详细描述见图 15.1.2 所示: 231 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 15.1.2 TIMx_CMMR1 [7:0]位详细描述 其中 CC1S[1:0],这两个位用于 CCR1 的通道配置,这里我们设置 IC1S[1:0]=01,也就是配 置 IC1 映射在 TI1 上(关于 IC1,TI1 不明白的,可以看《STM32 参考手册》14.2 节的图 98通用定时器框图),即 CC1 对应 TIMx_CH1。 输入捕获 1 预分频器 IC1PSC[1:0],这个比较好理解。我们是 1 次边沿就触发 1 次捕获,所 以选择 00 就是了。 f 输入捕获 1 滤波器 IC1F[3:0],这个用来设置输入采样频率和数字滤波器长度。其中,CK_INT f 是定时器的输入频率(TIMxCLK),一般为 72Mhz,而 DTS 则是根据 TIMx_CR1 的 CKD[1:0] f f 的设置来确定的,如果 CKD[1:0]设置为 00,那么 DTS = CK_INT。N 值就是滤波长度,举个简 单的例子:假设 IC1F[3:0]=0011,并设置 IC1 映射到通道 1 上,且为上升沿触发,那么在捕获 f 到上升沿的时候,再以 CK_INT 的频率,连续采样到 8 次通道 1 的电平,如果都是高电平,则 说明却是一个有效的触发,就会触发输入捕获中断(如果开启了的话)。这样可以滤除那些高电 平脉宽低于 8 个采样周期的脉冲信号,从而达到滤波的效果。这里,我们不做滤波处理,所以 232 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 设置 IC1F[3:0]=0000,只要采集到上升沿,就触发捕获。 再来看看捕获/比较使能寄存器:TIMx_CCER,该寄存器的各位描述见图 14.1.2(在第 14 章)。本章我们要用到这个寄存器的最低 2 位,CC1E 和 CC1P 位。这两个位的描述如图 15.1.3 所示: 图 15.1.3 TIMx_CCER 最低 2 位描述 所以,要使能输入捕获,必须设置 CC1E=0,而 CC1P 则根据自己的需要来配置。 接下来我们再看看 DMA/中断使能寄存器:TIMx_DIER,该寄存器的各位描述见图 13.1.2 (在第 13 章),本章,我们需要用到中断来处理捕获数据,所以必须开启通道 1 的捕获比较中 断,即 CC1IE 设置为 1。 控制寄存器:TIMx_CR1,我们只用到了它的最低位,也就是用来使能定时器的,这里前 面两章都有介绍,请大家参考前面的章节。 最后再来看看捕获/比较寄存器 1:TIMx_CCR1,该寄存器用来存储捕获发生时,TIMx_CNT 的值,我们从 TIMx_CCR1 就可以读出通道 1 捕获发生时刻的 TIMx_CNT 值,通过两次捕获(一 次上升沿捕获,一次下降沿捕获)的差值,就可以计算出高电平脉冲的宽度。 至此,我们把本章要用的几个相关寄存器都介绍完了,本章要实现通过输入捕获,来获取 TIM5_CH1(PA0)上面的高电平脉冲宽度,并从串口打印捕获结果。下面我们介绍输入捕获的配 置步骤: 1)开启 TIM5 时钟和 GPIOA 时钟,配置 PA0 为下拉输入。 要使用 TIM5,我们必须先开启 TIM5 的时钟。这里我们还要配置 PA0 为下拉输入,因为 我们要捕获 TIM5_CH1 上面的高电平脉宽,而 TIM5_CH1 是连接在 PA0 上面的。 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5, ENABLE); //使能 TIM5 时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能 GPIOA 时钟 这两个函数的使用在前面多次提到,还有 GPIO 初始化,这里也不重复了。 2)初始化 TIM5,设置 TIM5 的 ARR 和 PSC。 在开启了 TIM5 的时钟之后,我们要设置 ARR 和 PSC 两个寄存器的值来设置输入捕获的 自动重装载值和计数频率。这在库函数中是通过 TIM_TimeBaseInit 函数实现的,在上面章节已 经讲解过,这里不重复讲解。 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; 233 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 TIM_TimeBaseStructure.TIM_Period = arr; //设定计数器自动重装值 TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置预分频值 TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // TDTS = Tck_tim TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM 向上计数模式 TIM_TimeBaseInit(TIM5, &TIM_TimeBaseStructure); //根据指定的参数初始化 Tim5 3)设置 TIM5 的输入比较参数,开启输入捕获 输入比较参数的设置包括映射关系,滤波,分频以及捕获方式等。这里我们需要设置通道 1 为输入模式,且 IC1 映射到 TI1(通道 1)上面,并且不使用滤波(提高响应速度)器,上升沿捕 获。库函数是通过 TIM_ICInit 函数来初始化输入比较参数的: void TIM_ICInit(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct); 同样,我们来看看参数设置结构体 TIM_ICInitTypeDef 的定义: typedef struct { uint16_t TIM_Channel; uint16_t TIM_ICPolarity; uint16_t TIM_ICSelection; uint16_t TIM_ICPrescaler; uint16_t TIM_ICFilter; } TIM_ICInitTypeDef; 参数 TIM_Channel 很好理解,用来设置通道。我们设置为通道 1,为 TIM_Channel_1。 参 数 TIM_ICPolarit 是 用 来 设 置 输 入 信 号 的 有 效 捕 获 极 性 , 这 里 我 们 设 置 为 TIM_ICPolarity_Rising,上升沿捕获。同时库函数还提供了单独设置通道 1 捕获极性的函数为: TIM_OC1PolarityConfig(TIM5,TIM_ICPolarity_Falling), 这表示通道 1 为上升沿捕获,我们后面会用到,同时对于其他三个通道也有一个类似的函数, 使用的时候一定要分清楚使用的是哪个通道该调用哪个函数,格式为 TIM_OCxPolarityConfig()。 参 数 TIM_ICSelection 是 用 来 设 置 映 射 关 系 , 我 们 配 置 IC1 直 接 映 射 在 TI1 上 , 选 择 TIM_ICSelection_DirectTI。 参 数 TIM_ICPrescaler 用 来 设 置 输 入 捕 获 分 频 系 数 , 我 们 这 里 不 分 频 , 所 以 选 中 TIM_ICPSC_DIV1,还有 2,4,8 分频可选。 参数 TIM_ICFilter 设置滤波器长度,这里我们不使用滤波器,所以设置为 0。 这些参数的意义,在我们讲解寄存器的时候举例说明过,这里不做详细解释。 我们的配置代码是: TIM_ICInitTypeDef TIM5_ICInitStructure; TIM5_ICInitStructure.TIM_Channel = TIM_Channel_1; //选择输入端 IC1 映射到 TI1 上 TIM5_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; //上升沿捕获 TIM5_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; //映射到 TI1 上 TIM5_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //配置输入分频,不分频 TIM5_ICInitStructure.TIM_ICFilter = 0x00;//IC1F=0000 配置输入滤波器 不滤波 TIM_ICInit(TIM5, &TIM5_ICInitStructure); 4)使能捕获和更新中断(设置 TIM5 的 DIER 寄存器) 因为我们要捕获的是高电平信号的脉宽,所以,第一次捕获是上升沿,第二次捕获时下降 沿,必须在捕获上升沿之后,设置捕获边沿为下降沿,同时,如果脉宽比较长,那么定时器就 会溢出,对溢出必须做处理,否则结果就不准了。这两件事,我们都在中断里面做,所以必须 234 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 开启捕获中断和更新中断。 这里我们使用定时器的开中断函数 TIM_ITConfig 即可使能捕获和更新中断: TIM_ITConfig( TIM5,TIM_IT_Update|TIM_IT_CC1,ENABLE);//允许更新中断和捕获中断 5)设置中断分组,编写中断服务函数 设置中断分组的方法前面多次提到这里我们不做讲解,主要是通过函数 NVIC_Init()来完 成。分组完成后,我们还需要在中断函数里面完成数据处理和捕获设置等关键操作,从而实现 高电平脉宽统计。在中断服务函数里面,跟以前的外部中断和定时器中断实验中一样,我们在 中断开始的时候要进行中断类型判断,在中断结束的时候要清除中断标志位。使用到的函数在 上面的实验已经讲解过,分别为 TIM_GetITStatus()函数和 TIM_ClearITPendingBit()函数。 if (TIM_GetITStatus(TIM5, TIM_IT_Update) != RESET){}//判断是否为更新中断 if (TIM_GetITStatus(TIM5, TIM_IT_CC1) != RESET){}//判断是否发生捕获事件 TIM_ClearITPendingBit(TIM5, TIM_IT_CC1|TIM_IT_Update);//清除中断和捕获标志位 6)使能定时器(设置 TIM5 的 CR1 寄存器) 最后,必须打开定时器的计数器开关, 启动 TIM5 的计数器,开始输入捕获。 TIM_Cmd(TIM5,ENABLE ); //使能定时器 5 通过以上 6 步设置,定时器 5 的通道 1 就可以开始输入捕获了。 15.2 硬件设计 本实验用到的硬件资源有: 1) 指示灯 DS0 2) WK_UP 按键 3) 串口 4) 定时器 TIM3 5) 定时器 TIM5 前面 4 个,在之前的章节均有介绍。本节,我们将捕获 TIM5_CH1(PA0)上的高电平脉 宽,通过 WK_UP 按键输入高电平,并从串口打印高电平脉宽。同时我们保留上节的 PWM 输 出,大家也可以通过用杜邦线连接 PB5 和 PA0,来测量 PWM 输出的高电平脉宽。 15.3 软件设计 打开光盘的输入捕获实验,可以看到,我们的工程和上一个实验没有什么改动。因为我们 的输入捕获代码是直接添加在 timer.c 和 timer.h 中。同时输入捕获相关的库函数还是在 stm32f10x_tim.c 和 stm32f10x_tim.h 文件中。 我们在 timer.c 里面加入如下代码: //定时器 5 通道 1 输入捕获配置 TIM_ICInitTypeDef TIM5_ICInitStructure; void TIM5_Cap_Init(u16 arr,u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5, ENABLE); //①使能 TIM5 时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //①使能 GPIOA 时钟 235 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 //初始化 GPIOA.0 ① GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_ResetBits(GPIOA,GPIO_Pin_0); //PA0 设置 //PA0 输入 //初始化 GPIOA.0 //PA0 下拉 //②初始化 TIM5 参数 TIM_TimeBaseStructure.TIM_Period = arr; TIM_TimeBaseStructure.TIM_Prescaler =psc; //设定计数器自动重装值 //预分频器 TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // TDTS = Tck_tim TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM 向上计数模式 TIM_TimeBaseInit(TIM5, &TIM_TimeBaseStructure); //初始化 TIMx //③初始化 TIM5 输入捕获通道 1 TIM5_ICInitStructure.TIM_Channel = TIM_Channel_1; //选择输入端 IC1 映射到 TI1 上 TIM5_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; //上升沿捕获 TIM5_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; //映射到 TI1 上 TIM5_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //配置输入分频,不分频 TIM5_ICInitStructure.TIM_ICFilter = 0x00; //IC1F=0000 配置输入滤波器 不滤波 TIM_ICInit(TIM5, &TIM5_ICInitStructure); //初始化 TIM5 输入捕获通道 1 //⑤初始化 NVIC 中断优先级分组 NVIC_InitStructure.NVIC_IRQChannel = TIM5_IRQn; //TIM3 中断 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //先占优先级 2 级 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //从优先级 0 级 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道被使能 NVIC_Init(&NVIC_InitStructure); //初始化 NVIC TIM_ITConfig( TIM5,TIM_IT_Update|TIM_IT_CC1,ENABLE);//④允许更新中断捕获中断 TIM_Cmd(TIM5,ENABLE ); //⑥使能定时器 5 } u8 TIM5CH1_CAPTURE_STA=0; //输入捕获状态 u16 TIM5CH1_CAPTURE_VAL;//输入捕获值 //⑤定时器 5 中断服务程序 void TIM5_IRQHandler(void) { if((TIM5CH1_CAPTURE_STA&0X80)==0)//还未成功捕获 { if (TIM_GetITStatus(TIM5, TIM_IT_Update) != RESET) { if(TIM5CH1_CAPTURE_STA&0X40) //已经捕获到高电平了 { 236 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 if((TIM5CH1_CAPTURE_STA&0X3F)==0X3F)//高电平太长了 { TIM5CH1_CAPTURE_STA|=0X80; //标记成功捕获了一次 TIM5CH1_CAPTURE_VAL=0XFFFF; }else TIM5CH1_CAPTURE_STA++; } } if (TIM_GetITStatus(TIM5, TIM_IT_CC1) != RESET) //捕获 1 发生捕获事件 { if(TIM5CH1_CAPTURE_STA&0X40) //捕获到一个下降沿 { TIM5CH1_CAPTURE_STA|=0X80; //标记成功捕获到一次上升沿 TIM5CH1_CAPTURE_VAL=TIM_GetCapture1(TIM5); TIM_OC1PolarityConfig(TIM5,TIM_ICPolarity_Rising); //设置为上升沿捕获 }else //还未开始,第一次捕获上升沿 { TIM5CH1_CAPTURE_STA=0; //清空 TIM5CH1_CAPTURE_VAL=0; TIM_SetCounter(TIM5,0); TIM5CH1_CAPTURE_STA|=0X40; //标记捕获到了上升沿 TIM_OC1PolarityConfig(TIM5,TIM_ICPolarity_Falling); //设置为下降沿捕获 } } } TIM_ClearITPendingBit(TIM5, TIM_IT_CC1|TIM_IT_Update); //清除中断标志位 } 此部分代码包含 2 个函数,其中 TIM5_Cap_Init 函数用于 TIM5 通道 1 的输入捕获设置, 其设置和我们上面讲的步骤是一样的,这里就不多说,我们通过标号①~⑥标注了前面讲解的 步骤,大家可以对照看一下。重点来看看第二个函数。 TIM5_IRQHandler 是 TIM5 的中断服务函数,该函数用到了两个全局变量,用于辅助实现 高电平捕获。其中 TIM5CH1_CAPTURE_STA,是用来记录捕获状态,该变量类似我们在 usart.c 里面自行定义的 USART_RX_STA 寄存器(其实就是个变量,只是我们把它当成一个寄存器那样 来使用)。TIM5CH1_CAPTURE_STA 各位描述如表 15.3.1 所示: TIM5CH1_CAPTURE_STA bit7 bit6 bit5~0 捕获完成标志 捕获到高电平标志 捕获高电平后定时器溢出的次数 表 15.3.1 TIM5CH1_CAPTURE_STA 各位描述 另外一个变量 TIM5CH1_CAPTURE_VAL,则用来记录捕获到下降沿的时候,TIM5_CNT 的值。 现在我们来介绍一下,捕获高电平脉宽的思路:首先,设置 TIM5_CH1 捕获上升沿,这在 TIM5_Cap_Init 函数执行的时候就设置好了,然后等待上升沿中断到来,当捕获到上升沿中断, 此时如果 TIM5CH1_CAPTURE_STA 的第 6 位为 0,则表示还没有捕获到新的上升沿,就先把 TIM5CH1_CAPTURE_STA、TIM5CH1_CAPTURE_VAL 和 TIM5->CNT 等清零,然后再设置 237 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 TIM5CH1_CAPTURE_STA 的第 6 位为 1,标记捕获到高电平,最后设置为下降沿捕获,等待 下降沿到来。如果等待下降沿到来期间,定时器发生了溢出,就在 TIM5CH1_CAPTURE_STA 里面对溢出次数进行计数,当最大溢出次数来到的时候,就强制标记捕获完成(虽然此时还没 有捕获到下降沿)。当下降沿到来的时候,先设置 TIM5CH1_CAPTURE_STA 的第 7 位为 1,标 记成功捕获一次高电平,然后读取此时的定时器值到 TIM5CH1_CAPTURE_VAL 里面,最后设 置为上升沿捕获,回到初始状态。 这样,我们就完成一次高电平捕获了,只要 TIM5CH1_CAPTURE_STA 的第 7 位一直为 1, 那么就不会进行第二次捕获,我们在 main 函数处理完捕获数据后,将 TIM5CH1_CAPTURE_STA 置零,就可以开启第二次捕获。 这里我们还使用到一个函数 TIM_OC1PolarityConfig 来修改输入捕获通道 1 的极性的。相信这 个不难理解: void TIM_OC1PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity) 要设置为上升沿捕获,则为: TIM_OC1PolarityConfig(TIM5,TIM_ICPolarity_Rising); //设置为上升沿捕获 还有一个函数用来设置计数器寄存器值,这个同样很好理解: TIM_SetCounter(TIM5,0); 上行代码的意思就是计数值清零。 头文件 timer.h 内容比较简单,这里我们不详细讲解。接下来我们看看主程序内容如下: extern u8 TIM5CH1_CAPTURE_STA; //输入捕获状态 extern u16 TIM5CH1_CAPTURE_VAL; //输入捕获值 int main(void) { u32 temp=0; delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置 NVIC 中断分组 2 uart_init(115200); //串口初始化波特率为 115200 LED_Init(); //LED 端口初始化 TIM3_PWM_Init(899,0); //不分频。PWM 频率=72000/(899+1)=80Khz TIM5_Cap_Init(0XFFFF,72-1); //以 1Mhz 的频率计数 while(1) { delay_ms(10); TIM_SetCompare2(TIM3,TIM_GetCapture2(TIM3)+1); if(TIM_GetCapture2(TIM3)==300) TIM_SetCompare2(TIM3,0); if(TIM5CH1_CAPTURE_STA&0X80)//成功捕获到了一次上升沿 { temp=TIM5CH1_CAPTURE_STA&0X3F; temp*=65536;//溢出时间总和 temp+=TIM5CH1_CAPTURE_VAL;//得到总的高电平时间 printf("HIGH:%d us\r\n",temp); //打印总的高点平时间 TIM5CH1_CAPTURE_STA=0; //开启下一次捕获 } 238 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 } } 该 main 函数是在 PWM 实验的基础上修改来的,我们保留了 PWM 输出,同时通过设置 TIM5_Cap_Init(0XFFFF,72-1),将 TIM5_CH1 的捕获计数器设计为 1us 计数一次,并设置重装 载值为最大,所以我们的捕获时间精度为 1us。 主函数通过 TIM5CH1_CAPTURE_STA 的第 7 位,来判断有没有成功捕获到一次高电平, 如果成功捕获,则将高电平时间通过串口输出到电脑。 至此,我们的软件设计就完成了。 15.4 下载验证 在完成软件设计之后,将我们将编译好的文件下载到战舰 STM32 开发板上,可以看到 DS0 的状态和上一章差不多,由暗亮的循环。说明程序已经正常在跑了,我们再打开串口调试助 手,选择对应的串口,然后按 WK_UP 按键,可以看到串口打印的高电平持续时间,如图 15.4.1 所示: 图 15.4.1 PWM 控制 DS0 亮度 从上图可以看出,其中有 2 次高电平在 50us 以内的,这种就是按键按下时发生的抖动。这 就是为什么我们按键输入的时候,一般都需要做防抖处理,防止类似的情况干扰正常输入。大 家还可以用杜邦线连接 PA0 和 PB5,看看上一节中我们设置的 PWM 输出的高电平是如何变化 的。 239 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第十六章 电容触摸按键实验 上一章,我们介绍了 STM32F1 的输入捕获功能及其使用。这一章,我们将向大家介绍如 何通过输入捕获功能,来做一个电容触摸按键。在本章中,我们将用 TIM5 的通道 2(PA1)来 做输入捕获,并实现一个简单的电容触摸按键,通过该按键控制 DS1 的亮灭。从本章分为如下 几个部分: 16.1 电容触摸按键简介 16.2 硬件设计 16.3 软件设计 16.4 下载验证 240 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 16.1 电容触摸按键简介 触摸按键相对于传统的机械按键有寿命长、占用空间少、易于操作等诸多优点。大家看看 如今的手机,触摸屏、触摸按键大行其道,而传统的机械按键,正在逐步从手机上面消失。本 章,我们将给大家介绍一种简单的触摸按键:电容式触摸按键。 我们将利用战舰 STM32 开发板上的触摸按键(TPAD),来实现对 DS1 的亮灭控制。这里 TPAD 其实就是战舰 STM32 开发板上的一小块覆铜区域,实现原理如图 16.1.1 所示: 图 16.1.1 电容触摸按键原理 这里我们使用的是检测电容充放电时间的方法来判断是否有触摸,图中 R 是外接的电容充 电电阻,Cs 是没有触摸按下时 TPAD 与 PCB 之间的杂散电容。而 Cx 则是有手指按下的时候, 手指与 TPAD 之间形成的电容。图中的开关是电容放电开关(由实际使用时,由 STM32 的 IO 代替)。 先用开关将 Cs(或 Cs+Cx)上的电放尽,然后断开开关,让 R 给 Cs(或 Cs+Cx)充电, 当没有手指触摸的时候,Cs 的充电曲线如图中的 A 曲线。而当有手指触摸的时候,手指和 TPAD 之间引入了新的电容 Cx,此时 Cs+Cx 的充电曲线如图中的 B 曲线。从上图可以看出,A、B 两种情况下,Vc 达到 Vth 的时间分别为 Tcs 和 Tcs+Tcx。 其中,除了 Cs 和 Cx 我们需要计算,其他都是已知的,根据电容充放电公式: Vc=V0*(1-e^(-t/RC)) 其中 Vc 为电容电压,V0 为充电电压,R 为充电电阻,C 为电容容值,e 为自然底数,t 为 充电时间。根据这个公式,我们就可以计算出 Cs 和 Cx。利用这个公式,我们还可以把战舰开 发板作为一个简单的电容计,直接可以测电容容量了,有兴趣的朋友可以捣鼓下。 在本章中,其实我们只要能够区分 Tcs 和 Tcs+Tcx,就已经可以实现触摸检测了,当充电 时间在 Tcs 附近,就可以认为没有触摸,而当充电时间大于 Tcs+Tx 时,就认为有触摸按下(Tx 为检测阀值)。 本章,我们使用 PA1(TIM5_CH2)来检测 TPAD 是否有触摸,在每次检测之前,我们先配置 PA1 为推挽输出,将电容 Cs(或 Cs+Cx)放电,然后配置 PA1 为浮空输入,利用外部上拉电阻 给电容 Cs(Cs+Cx)充电,同时开启 TIM5_CH2 的输入捕获,检测上升沿,当检测到上升沿的时 候,就认为电容充电完成了,完成一次捕获检测。 在 MCU 每次复位重启的时候,我们执行一次捕获检测(可以认为没触摸),记录此时的值, 记为 tpad_default_val,作为判断的依据。在后续的捕获检测,我们就通过与 tpad_default_val 的 对比,来判断是不是有触摸发生。 241 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 关于输入捕获的配置,在上一章我们已经有详细介绍了,这里我们就不再介绍。至此,电 容触摸按键的原理介绍完毕。 16.2 硬件设计 本实验用到的硬件资源有: 1) 指示灯 DS0 和 DS1 2) 定时器 TIM5 3) 触摸按键 TPAD 前面两个之前均有介绍,我们需要通过 TIM5_CH2(PA1)采集 TPAD 的信号,所以本实 验需要用跳线帽短接多功能端口(P14)的 TPAD 和 ADC,以实现 TPAD 连接到 PA1。如图 16.2.1 所示: 图 16.2.1 TPAD 与 STM32 连接原理图 硬件设置(用跳线帽短接多功能端口的 ADC 和 TPAD 即可)好之后,下面我们开始软件 设计。 16.3 软件设计 前面讲解过,触摸按键我们是通过输入捕获实现的,所以使用的库函数依然是分布在 stm32f10x_tim.c 和 stm32f10x_tim.h 中。同时我们在 HARDWARE 组下面增加了 tpad.c 和 tpad.h 文件用来存放我们的触摸按键驱动代码。 打开 tpad.c 可以看到,我们在 tpad.c 里输入了如下代码: #define TPAD_ARR_MAX_VAL 0XFFFF //最大的 ARR 值 vu16 tpad_default_val=0;//空载的时候(没有手按下),计数器需要的时间 //初始化触摸按键 //获得空载的时候触摸按键的取值. //返回值:0,初始化成功;1,初始化失败 u8 TPAD_Init(u8 psc) { u16 buf[10]; u16 temp; u8 j,i; TIM5_CH2_Cap_Init(TPAD_ARR_MAX_VAL,psc-1);//以 1Mhz 的频率计数 for(i=0;i<10;i++)//连续读取 10 次 { buf[i]=TPAD_Get_Val(); 242 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 delay_ms(10); } for(i=0;i<9;i++)//排序 { for(j=i+1;j<10;j++) { if(buf[i]>buf[j])//升序排列 { temp=buf[i]; buf[i]=buf[j]; buf[j]=temp; } } } temp=0; for(i=2;i<8;i++)temp+=buf[i];//取中间的 8 个数据进行平均 tpad_default_val=temp/6; printf("tpad_default_val:%d\r\n",tpad_default_val); if(tpad_default_val>TPAD_ARR_MAX_VAL/2)return 1; //初始化遇到超过 TPAD_ARR_MAX_VAL/2 的数值,不正常! return 0; } //复位一次 void TPAD_Reset(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能 PA 时钟 //设置 GPIOA.1 为推挽使出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; //PA1 端口配置 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_ResetBits(GPIOA,GPIO_Pin_1); delay_ms(5); TIM_SetCounter(TIM5,0); TIM_ClearITPendingBit(TIM5, TIM_IT_CC2|TIM_IT_Update); GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING; //初始化 GPIOA.1 //PA.1 输出 0,放电 //延时 5ms //归 0 //清除中断标志 //GPIOA.1 浮空输入 GPIO_Init(GPIOA, &GPIO_InitStructure); } //得到定时器捕获值 //如果超时,则直接返回定时器的计数值. u16 TPAD_Get_Val(void) { 243 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 TPAD_Reset(); while(TIM_GetITStatus(TIM5, TIM_IT_CC2)== RESET) //等待溢出 { if(TIM_GetCounter(TIM5)>TPAD_ARR_MAX_VAL-500) return TIM_GetCounter(TIM5); //超时了,直接返回 CNT 的值 }; return TIM_GetCapture2(TIM5); } //读取 n 次,取最大值 u16 TPAD_Get_MaxVal(u8 n) { u16 temp=0; u16 res=0; while(n--) { temp=TPAD_Get_Val(); //得到一次值 if(temp>res)res=temp; }; return res; } //扫描触摸按键 //mode:0,不支持连续触发(按下一次必须松开才能按下一次);1,支持连续触发(可以一直按) //返回值:0,没有按下;1,有按下; #define TPAD_GATE_VAL 100 //触摸的门限值,也就是必须大于 tpad_default_val+TPAD_GATE_VAL,才认为是有效触摸. u8 TPAD_Scan(u8 mode) { static u8 keyen=0; //0,可以开始检测;>0,还不能开始检测 u8 res=0; u8 sample=3; //默认采样次数为 3 次 u16 rval; if(mode) { sample=6; keyen=0; //支持连按的时候,设置采样次数为 6 次 //支持连按 } rval=TPAD_Get_MaxVal(sample); if(rval>(tpad_default_val+TPAD_GATE_VAL)) //大于 tpad_default_val+TPAD_GATE_VAL,有效 { if(keyen==0)res=1; //keyen==0,有效 //printf("r:%d\r\n",rval); keyen=3; //至少要再过 3 次之后才能按键有效 244 } if(keyen)keyen--; STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 return res; } //定时器 2 通道 2 输入捕获配置 void TIM5_CH2_Cap_Init(u16 arr,u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM5_ICInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5, ENABLE); //使能 TIM5 时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能 PA 时钟 //设置 GPIOA.1 为浮空输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; //PA1 端口配置 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度 50MHz GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING; //浮空输入 GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 GPIOA.1 //初始化 TIM5 TIM_TimeBaseStructure.TIM_Period = arr; //设定计数器自动重装值 TIM_TimeBaseStructure.TIM_Prescaler =psc; //预分频器 TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // TDTS = Tck_tim TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数模式 TIM_TimeBaseInit(TIM5, &TIM_TimeBaseStructure); //根据参数初始化 TIMx //初始化 TIM5 通道 2 TIM5_ICInitStructure.TIM_Channel = TIM_Channel_2; //选择输入端 IC2 映射到 TI5 上 TIM5_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; //上升沿捕获 TIM5_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; TIM5_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //配置输入分频,不分频 TIM5_ICInitStructure.TIM_ICFilter = 0x03;//配置输入滤波器 8 个定时器时钟周期滤波 TIM_ICInit(TIM5, &TIM5_ICInitStructure);//初始化 I5 IC2 TIM_Cmd(TIM5,ENABLE ); //使能定时器 5 } 此部分代码包含 6 个函数,我们将介绍其中 4 个比较重要的函数:TIM5_CH2_Cap_Init、 TPAD_Get_Val、TPAD_Init 和 TPAD_Scan。 首先介绍 TIM5_CH2_Cap_Init 函数,该函数和上一章的输入捕获函数基本一样,不同的是, 这里我们设置的是 CH2 通道,并开启了输入滤波器。通过该函数的设置,我们将可以捕获 PA1 上的上升沿。关于配置的详细介绍大家可以看第 15 章输入捕获实验讲解。 我们再来看看 TPAD_Get_Val 函数,该函数用于得到定时器的一次捕获值。该函数先调用 TPAD_Reset,将电容放电,同时设置 TIM5_CNT 寄存器为 0,然后死循环等待发生上升沿捕获 245 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 (或计数溢出),将捕获到的值(或溢出值)作为返回值返回。 接着我们介绍 TPAD_Init 函数,该函数用于初始化输入捕获,并获取默认的 TPAD 值。该 函数有一个参数,用来传递定时器分频系数,其实是为了配置 TIM5_CH2_Cap_Init 为 1us 计数 周期。在该函数中连续 10 次读取 TPAD 值,将这些值升序排列后取中间 6 个值再做平均(这 样做的目的是尽量减少误差),并赋值给 tpad_default_val,用于后续触摸判断的标准。 最后,我们来看看 TPAD_Scan 函数,该函数用于扫描 TPAD 是否有触摸,该函数的参数 mode,用于设置是否支持连续触发。返回值如果是 0,说明没有触摸,如果是 1,则说明有触 摸。该函数同样包含了一个静态变量,用于检测控制,类似第八章的 KEY_Scan 函数。所以该 函数同样是不可重入的。在函数中,我们通过连续读取 3 次(不支持连续按的时候)TPAD 的值, 取这他们的最大值,和 tpad_default_val+TPAD_GATE_VAL 比较,如果大于则说明有触摸,如 果小于,则说明无触摸。其中 tpad_default_val 是我们在调用 TPAD_Init 函数的时候得到的值, 而 TPAD_GATE_VAL 则是我们设定的一个门限值(这个大家可以通过实验数据得出,根据实际 情况选择适合的值就好了),这里我们设置为 80。该函数,我们还做了一些其他的条件限制, 让触摸按键有更好的效果,这个就请大家看代码自行参悟了。 在 tpad.h 文件里面,我们只是进行了一些函数申明。 接下来,我们看看主程序里面的 main 函数如下: int main(void) { u8 t=0; delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); uart_init(115200); //串口初始化波特率为 115200 LED_Init(); TPAD_Init(); //LED 端口初始化 //初始化触摸按键 //设置 NVIC 中断分组 2 while(1) { if(TPAD_Scan(0)) //成功捕获到了一次上升沿(此函数执行时间至少 15ms) { LED1=!LED1; //LED1 取反 } t++; if(t==15) { t=0; LED0=!LED0; //LED0 取反,提示程序正在运行 } delay_ms(10); } } 该 main 函数比较简单,TPAD_Init()函数执行之后,就开始触摸按键的扫描,当有触摸的 时候,对 DS1 取反,而 DS0 则有规律的间隔取反,提示程序正在运行。 这里还要提醒一下大家,不要把 uart_init(115200);去掉,因为在 TPAD_Init 函数里面,我们 有用到 printf,如果你去掉了 uart_init,就会导致 printf 无法执行,从而死机。 至此,我们的软件设计就完成了。 246 16.4 下载验证 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 在完成软件设计之后,将我们将编译好的文件下载到战舰 STM32 开发板上,可以看到 DS0 慢速闪烁,此时,我们用手指触摸 ALIENTEK 战舰 STM32 开发板上的 TPAD(右下角的 LOGO 标志),就可以控制 DS1 的亮灭了。不过你要确保 TPAD 和 ADC 的跳线帽连接上了哦!如图 16.4.1 所示: 图 16.4.1 触摸区域和跳线帽短接方式 同时大家可以打开串口调试助手,每次复位的时候,会收到 tpad_default_val 的值,一般为 140 左右。 247 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第十七章 OLED 显示实验 前面几章的实例,均没涉及到液晶显示,这一章,我们将向大家介绍 OLED 的使用。在本 章中,我们将利用战舰 STM32 开发板上的 OLED 模块接口(与摄像头共用的这个),来点亮 OLED,并实现 ASCII 字符的显示。本章分为如下几个部分: 17.1 OLED 简介 17.2 硬件设计 17.3 软件设计 17.4 下载验证 248 17.1 OLED 简介 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 OLED,即有机发光二极管(Organic Light-Emitting Diode),又称为有机电激光显示(Organic Electroluminesence Display, OELD)。OLED 由于同时具备自发光,不需背光源、对比度高、 厚度薄、视角广、反应速度快、可用于挠曲性面板、使用温度范围广、构造及制程较简单等优 异之特性,被认为是下一代的平面显示器新兴应用技术。 LCD 都需要背光,而 OLED 不需要,因为它是自发光的。这样同样的显示,OLED 效果要 来得好一些。以目前的技术,OLED 的尺寸还难以大型化,但是分辨率确可以做到很高。在本 章中,我们使用的是 ALINETEK 的 OLED 显示模块,该模块有以下特点: 1)模块有单色和双色两种可选,单色为纯蓝色,而双色则为黄蓝双色。 2)尺寸小,显示尺寸为 0.96 寸,而模块的尺寸仅为 27mm*26mm 大小。 3)高分辨率,该模块的分辨率为 128*64。 4)多种接口方式,该模块提供了总共 5 种接口包括:6800、8080 两种并行接口方式、3 线或 4 线的穿行 SPI 接口方式,、IIC 接口方式(只需要 2 根线就可以控制 OLED 了!)。 5)不需要高压,直接接 3.3V 就可以工作了。 这里要提醒大家的是,该模块不和 5.0V 接口兼容,所以请大家在使用的时候一定要小心, 别直接接到 5V 的系统上去,否则可能烧坏模块。以上 5 种模式通过模块的 BS0~2 设置,BS0~2 的设置与模块接口模式的关系如表 17.1.1 所示: 接口方式 4 线 SPI IIC 8 位 6800 8 位 8080 BS1 0 1 0 1 BS2 0 0 1 1 表 17.1.1 OLED 模块接口方式设置表 表 17.1.1 中:“1”代表接 VCC,而“0”代表接 GND。 该模块的外观图如图 17.1.1 所示: 图 17.1.1 ALIENTEK OLED 模块外观图 ALIENTEK OLED 模块默认设置的是 BS0 接 GND,BS1 和 BS2 接 VCC ,即使用 8080 并 口方式,如果你想要设置为其他模式,则需要在 OLED 的背面,用烙铁修改 BS0~2 的设置。 模块的原理图如图 17.1.2 所示: 249 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 17.1.2 ALIENTEK OLED 模块原理图 该模块采用 8*2 的 2.54 排针与外部连接,总共有 16 个管脚,在 16 条线中,我们只用了 15 条,有一个是悬空的。15 条线中,电源和地线占了 2 条,还剩下 13 条信号线。在不同模式下, 我们需要的信号线数量是不同的,在 8080 模式下,需要全部 13 条,而在 IIC 模式下,仅需要 2 条线就够了!这其中有一条是共同的,那就是复位线 RST(RES),RST 上的低电平,将导致 OLED 复位,在每次初始化之前,都应该复位一下 OLED 模块。 ALIENTEK OLED 模块的控制器是 SSD1306,本章,我们将学习如何通过 STM32 来控制 该模块显示字符和数字,本章的实例代码将可以支持 2 种方式与 OLED 模块连接,一种是 8080 的并口方式,另外一种是 4 线 SPI 方式。 首先我们介绍一下模块的 8080 并行接口,8080 并行接口的发明者是 INTEL,该总线也被 广泛应用于各类液晶显示器,ALIENTEK OLED 模块也提供了这种接口,使得 MCU 可以快速 的访问 OLED。ALIENTEK OLED 模块的 8080 接口方式需要如下一些信号线: CS:OLED 片选信号。 WR:向 OLED 写入数据。 RD:从 OLED 读取数据。 D[7:0]:8 位双向数据线。 RST(RES):硬复位 OLED。 DC:命令/数据标志(0,读写命令;1,读写数据)。 模块的 8080 并口读/写的过程为:先根据要写入/读取的数据的类型,设置 DC 为高(数据) /低(命令),然后拉低片选,选中 SSD1306,接着我们根据是读数据,还是要写数据置 RD/WR 为低,然后: 在 RD 的上升沿, 使数据锁存到数据线(D[7:0])上; 250 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 在 WR 的上升沿,使数据写入到 SSD1306 里面; SSD1306 的 8080 并口写时序图如图 17.1.3 所示: 图 17.1.3 8080 并口写时序图 SSD1306 的 8080 并口读时序图如图 17.1.4 所示: 图 17.1.4 8080 并口读时序图 SSD1306 的 8080 接口方式下,控制脚的信号状态所对应的功能如表 17.1.2: 功能 RD WR CS DC 写命令 H ↑ L L 读状态 ↑ H L L 写数据 H ↑ L H 读数据 ↑ H L H 表 17.1.2 控制脚信号状态功能表 在 8080 方式下读数据操作的时候,我们有时候(例如读显存的时候)需要一个假读命 (Dummy Read),以使得微控制器的操作频率和显存的操作频率相匹配。在读取真正的数据之 前,由一个的假读的过程。这里的假读,其实就是第一个读到的字节丢弃不要,从第二个开始, 才是我们真正要读的数据。 一个典型的读显存的时序图,如图 17.1.5 所示: 251 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 17.1.5 读显存时序图 可以看到,在发送了列地址之后,开始读数据,第一个是 Dummy Read,也就是假读,我 们从第二个开始,才算是真正有效的数据。 并行接口模式就介绍到这里,我们接下来介绍一下 4 线串行(SPI)方式,4 先串口模式使 用的信号线有如下几条: CS:OLED 片选信号。 RST(RES):硬复位 OLED。 DC:命令/数据标志(0,读写命令;1,读写数据)。 SCLK:串行时钟线。在 4 线串行模式下,D0 信号线作为串行时钟线 SCLK。 SDIN:串行数据线。在 4 线串行模式下,D1 信号线作为串行数据线 SDIN。 模块的 D2 需要悬空,其他引脚可以接到 GND。在 4 线串行模式下,只能往模块写数据而 不能读数据。 在 4 线 SPI 模式下,每个数据长度均为 8 位,在 SCLK 的上升沿,数据从 SDIN 移入到 SSD1306,并且是高位在前的。DC 线还是用作命令/数据的标志线。在 4 线 SPI 模式下,写操 作的时序如图 17.1.6 所示: 图 17.1.6 4 线 SPI 写操作时序图 4 线串行模式就为大家介绍到这里。其他还有几种模式,在 SSD1306 的数据手册上都有详 细的介绍,如果要使用这些方式,请大家参考该手册。 接下来,我们介绍一下模块的显存,SSD1306 的显存总共为 128*64bit 大小,SSD1306 将 这些显存分为了 8 页,其对应关系如表 17.1.3 所示: 252 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 表 17.1.3 SSD1306 显存与屏幕对应关系表 可以看出,SSD1306 的每页包含了 128 个字节,总共 8 页,这样刚好是 128*64 的点阵大 小。因为每次写入都是按字节写入的,这就存在一个问题,如果我们使用只写方式操作模块, 那么,每次要写 8 个点,这样,我们在画点的时候,就必须把要设置的点所在的字节的每个位 都搞清楚当前的状态(0/1?),否则写入的数据就会覆盖掉之前的状态,结果就是有些不需要 显示的点,显示出来了,或者该显示的没有显示了。这个问题在能读的模式下,我们可以先读 出来要写入的那个字节,得到当前状况,在修改了要改写的位之后再写进 GRAM,这样就不会 影响到之前的状况了。但是这样需要能读 GRAM,对于 3 线或 4 线 SPI 模式,模块是不支持读 的,而且读->改->写的方式速度也比较慢。 所以我们采用的办法是在 STM32 的内部建立一个 OLED 的 GRAM(共 128*8 个字节),在 每次修改的时候,只是修改 STM32 上的 GRAM(实际上就是 SRAM),在修改完了之后,一次 性把 STM32 上的 GRAM 写入到 OLED 的 GRAM。当然这个方法也有坏处,就是对于那些 SRAM 很小的单片机(比如 51 系列)就比较麻烦了。 SSD1306 的命令比较多,这里我们仅介绍几个比较常用的命令,这些命令如表 17.1.4 所示: 表 17.1.4 SSD1306 常用命令表 第一个命令为 0X81,用于设置对比度的,这个命令包含了两个字节,第一个 0X81 为命令, 随后发送的一个字节为要设置的对比度的值。这个值设置得越大屏幕就越亮。 第二个命令为 0XAE/0XAF。0XAE 为关闭显示命令;0XAF 为开启显示命令。 第三个命令为 0X8D,该指令也包含 2 个字节,第一个为命令字,第二个为设置值,第二 个字节的 BIT2 表示电荷泵的开关状态,该位为 1,则开启电荷泵,为 0 则关闭。在模块初始化 的时候,这个必须要开启,否则是看不到屏幕显示的。 第四个命令为 0XB0~B7,该命令用于设置页地址,其低三位的值对应着 GRAM 的页地址。 253 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第五个指令为 0X00~0X0F,该指令用于设置显示时的起始列地址低四位。 第六个指令为 0X10~0X1F,该指令用于设置显示时的起始列地址高四位。 其他命令,我们就不在这里一一介绍了,大家可以参考 SSD1306 datasheet 的第 28 页。从 这页开始,对 SSD1306 的指令有详细的介绍。 最后,我们再来介绍一下 OLED 模块的初始化过程,SSD1306 的典型初始化框图如图 17.1.7 所示: 图 17.1.7 SSD1306 初始化框图 驱动 IC 的初始化代码,我们直接使用厂家推荐的设置就可以了,只要对细节部分进行一些 修改,使其满足我们自己的要求即可,其他不需要变动。 OLED 的介绍就到此为止,我们重点向大家介绍了 ALIENTEK OLED 模块的相关知识,接 下来我们将使用这个模块来显示字符和数字。通过以上介绍,我们可以得出 OLED 显示需要的 相关设置步骤如下: 1)设置 STM32 与 OLED 模块相连接的 IO。 这一步,先将我们与 OLED 模块相连的 IO 口设置为输出,具体使用哪些 IO 口,这里需要 根据连接电路以及 OLED 模块所设置的通讯模式来确定。这些将在硬件设计部分向大家介绍。 2)初始化 OLED 模块。 其实这里就是上面的初始化框图的内容,通过对 OLED 相关寄存器的初始化,来启动 OLED 的显示。为后续显示字符和数字做准备。 3)通过函数将字符和数字显示到 OLED 模块上。 这里就是通过我们设计的程序,将要显示的字符送到 OLED 模块就可以了,这些函数将在 软件设计部分向大家介绍。 通过以上三步,我们就可以使用 ALIENTEK OLED 模块来显示字符和数字了,在后面我们 还将会给大家介绍显示汉字的方法。这一部分就先介绍到这里。 254 17.2 硬件设计 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 本实验用到的硬件资源有: 1) 指示灯 DS0 2) OLED 模块 OLED 模块的电路在前面已有详细说明了,这里我们介绍 OLED 模块与战舰 STM32F103 的连接,开发板底板的 OLED/CAMERA 接口(P6 接口)和 ALIENTEK OLED 模块直接可 以对插(靠左插!),连接如图 17.2.1 所示: 图 17.2.1 OLED 模块与开发板连接示意图 图中圈出来的部分就是连接 OLED 的接口,这里在硬件上,OLED 与战舰 STM32 开发板 的 IO 口对应关系如下: OLED_CS 对应 PD6; OLED_RST 对应 PG15; OLED_RS 对应 PD3; OLED_WR 对应 PG14; OLED_RD 对应 PG13; OLED_D[7:0]对应 PC[7:0]; 这些线的连接,战舰 STM32 的内部已经连接好了,我们只需要将 OLED 模块插上去就好 了。实物连接如图 17.2.2 所示: 图 17.2.2 OLED 模块与开发板连接实物图 255 17.3 软件设计 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 我们直接打开 OLED 显示实验可以发现 HARDWARE 下面有一个 oled.c 文件,同时包含了 头文件 oled.h。这里要说明一下,在我们寄存器版本的代码中,我们是采取的位带操作,这里 我们采取的是库函数来进行 IO 操作。大家可以对照看一下。 oled.c 的代码,由于比较长,这里我们就不贴出来了,仅介绍几个比较重要的函数。首先 是 OLED_Init 函数,该函数的结构比较简单,开始是对 IO 口的初始化,这里我们用了宏定义 OLED_MODE 来决定要设置的 IO 口,其他就是一些初始化序列了,我们按照厂家提供的资料 来做就可以。最后要说明一点的是,因为 OLED 是无背光的,在初始化之后,我们把显存都清 空了,所以我们在屏幕上是看不到任何内容的,跟没通电一个样,不要以为这就是初始化失败, 要写入数据模块才会显示的。OLED_Init 函数代码如下: //初始化 SSD1306 void OLED_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC|RCC_APB2Periph_GPIOD| RCC_APB2Periph_GPIOG, ENABLE);//使能 PC,D,G 端口时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3|GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //PD3,PD6 推挽输出 //推挽输出 //速度 50MHz GPIO_Init(GPIOD, &GPIO_InitStructure); //初始化 GPIOD3,6 GPIO_SetBits(GPIOD,GPIO_Pin_3|GPIO_Pin_6); //PD3,PD6 输出高 #if OLED_MODE==1 GPIO_InitStructure.GPIO_Pin =0xFF; //PC0~7 OUT 推挽输出 GPIO_Init(GPIOC, &GPIO_InitStructure); GPIO_SetBits(GPIOC,0xFF); //PG13,14,15 OUT 推挽输出 //PC0~7 输出高 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13|GPIO_Pin_14|GPIO_Pin_15; GPIO_Init(GPIOG, &GPIO_InitStructure); //PG13,14,15 OUT 输出高 GPIO_SetBits(GPIOG,GPIO_Pin_13|GPIO_Pin_14|GPIO_Pin_15); #else GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1; //PC0,1 OUT 推挽输出 GPIO_Init(GPIOC, &GPIO_InitStructure); GPIO_SetBits(GPIOC,GPIO_Pin_0|GPIO_Pin_1); //PC0,1 OUT 输出高 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15; //PG15 OUT 推挽输出 RST GPIO_Init(GPIOG, &GPIO_InitStructure); GPIO_SetBits(GPIOG,GPIO_Pin_15); //PG15 OUT 输出高 #endif OLED_RST=0; delay_ms(100); OLED_RST=1; OLED_WR_Byte(0xAE,OLED_CMD); //关闭显示 256 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 OLED_WR_Byte(0xD5,OLED_CMD); //设置时钟分频因子,震荡频率 OLED_WR_Byte(80,OLED_CMD); //[3:0],分频因子;[7:4],震荡频率 OLED_WR_Byte(0xA8,OLED_CMD); //设置驱动路数 OLED_WR_Byte(0X3F,OLED_CMD); //默认 0X3F(1/64) OLED_WR_Byte(0xD3,OLED_CMD); //设置显示偏移 OLED_WR_Byte(0X00,OLED_CMD); //默认为 0 OLED_WR_Byte(0x40,OLED_CMD); //设置显示开始行 [5:0],行数. OLED_WR_Byte(0x8D,OLED_CMD); //电荷泵设置 OLED_WR_Byte(0x14,OLED_CMD); //bit2,开启/关闭 OLED_WR_Byte(0x20,OLED_CMD); //设置内存地址模式 OLED_WR_Byte(0x02,OLED_CMD); //[1:0],00,列地址模式;01, //行地址模式;10,页地址模式;默认 10; OLED_WR_Byte(0xA1,OLED_CMD); //段重定义设置,bit0:0,0->0;1,0->127; OLED_WR_Byte(0xC0,OLED_CMD); //设置 COM 扫描方向;bit3:0,普通模式;1, //重定义模式 COM[N-1]->COM0;N:驱动路数 OLED_WR_Byte(0xDA,OLED_CMD); //设置 COM 硬件引脚配置 OLED_WR_Byte(0x12,OLED_CMD); //[5:4]配置 OLED_WR_Byte(0x81,OLED_CMD); //对比度设置 OLED_WR_Byte(0xEF,OLED_CMD); //1~255;默认 0X7F (亮度设置,越大越亮) OLED_WR_Byte(0xD9,OLED_CMD); //设置预充电周期 OLED_WR_Byte(0xf1,OLED_CMD); //[3:0],PHASE 1;[7:4],PHASE 2; OLED_WR_Byte(0xDB,OLED_CMD); //设置 VCOMH 电压倍率 OLED_WR_Byte(0x30,OLED_CMD); //[6:4] 000,0.65*vcc;001,0.77*vcc;011,0.83*vcc; OLED_WR_Byte(0xA4,OLED_CMD); //全局显示开启;bit0:1,开启;0,关闭;(白屏/黑屏) OLED_WR_Byte(0xA6,OLED_CMD); //设置显示方式;bit0:1,反相显示;0,正常显示 OLED_WR_Byte(0xAF,OLED_CMD); //开启显示 OLED_Clear();//清屏 } 接着,要介绍的是 OLED_Refresh_Gram 函数。我们在 STM32 内部定义了一个块 GRAM: u8 OLED_GRAM[128][8];此部分 GRAM 对应 OLED 模块上的 GRAM。在操作的时候,我们只 要修改 STM32 内部的 GRAM 就可以了,然后通过 OLED_Refresh_Gram 函数把 GRAM 一次刷 新到 OLED 的 GRAM 上。该函数代码如下: //更新显存到 LCD void OLED_Refresh_Gram(void) { u8 i,n; for(i=0;i<8;i++) { OLED_WR_Byte (0xb0+i,OLED_CMD); OLED_WR_Byte (0x00,OLED_CMD); OLED_WR_Byte (0x10,OLED_CMD); //设置页地址(0~7) //设置显示位置—列低地址 //设置显示位置—列高地址 for(n=0;n<128;n++)OLED_WR_Byte(OLED_GRAM[n][i],OLED_DATA); } } 257 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 OLED_Refresh_Gram 函数先设置页地址,然后写入列地址(也就是纵坐标),然后从 0 开 始写入 128 个字节,写满该页,最后循环把 8 页的内容都写入,就实现了整个从 STM32 显存 到 OLED 显存的拷贝。 OLED_Refresh_Gram 函数还用到了一个外部函数,也就是我们接着要介绍的函数: OLED_WR_Byte,该函数直接和硬件相关,函数代码如下: #if OLED_MODE==1 //向 SSD1306 写入一个字节。 //dat:要写入的数据/命令 //cmd:数据/命令标志 0,表示命令;1,表示数据; void OLED_WR_Byte(u8 dat,u8 cmd) { DATAOUT(dat); if(cmd) OLED_RS_Set(); else OLED_RS_Clr(); OLED_CS_Clr(); OLED_WR_Clr(); OLED_WR_Set(); OLED_CS_Set(); OLED_RS_Set(); } #else //向 SSD1306 写入一个字节。 //dat:要写入的数据/命令 //cmd:数据/命令标志 0,表示命令;1,表示数据; void OLED_WR_Byte(u8 dat,u8 cmd) { u8 i; if(cmd) OLED_RS_Set(); else OLED_RS_Clr(); OLED_CS_Clr(); for(i=0;i<8;i++) { OLED_SCLK_Clr(); if(dat&0x80) OLED_SDIN_Set(); else OLED_SDIN_Clr(); OLED_SCLK_Set(); dat<<=1; } 258 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 OLED_CS_Set(); OLED_RS_Set(); } #endif 这 里 有 2 个 一 样 的 函 数 , 通 过 宏 定 义 OLED_MODE 来 决 定 使 用 哪 一 个 。 如 果 OLED_MODE=1,就定义为并口模式,选择第一个函数,而如果为 0,则为 4 线串口模式,选 择第二个函数。这两个函数输入参数均为 2 个:dat 和 cmd,dat 为要写入的数据,cmd 则表明 该数据是命令还是数据。这两个函数的时序操作就是根据上面我们对 8080 接口以及 4 线 SPI 接口的时序来编写的。 OLED_GRAM[128][8]中的 128 代表列数(x 坐标),而 8 代表的是页,每页又包含 8 行, 总共 64 行(y 坐标)。从高到低对应行数从小到大。比如,我们要在 x=100,y=29 这个点写入 1,则可以用这个句子实现: OLED_GRAM[100][4]|=1<<2; 一个通用的在点(x,y)置 1 表达式为: OLED_GRAM[x][7-y/8]|=1<<(7-y%8); 其中 x 的范围为:0~127;y 的范围为:0~63。 因此,我们可以得出下一个将要介绍的函数:画点函数,void OLED_DrawPoint(u8 x,u8 y, u8 t);函数代码如下: void OLED_DrawPoint(u8 x,u8 y,u8 t) { u8 pos,bx,temp=0; if(x>127||y>63)return;//超出范围了. pos=7-y/8; bx=y%8; temp=1<<(7-bx); if(t)OLED_GRAM[x][pos]|=temp; else OLED_GRAM[x][pos]&=~temp; } 该函数有 3 个参数,前两个是坐标,第三个 t 为要写入 1 还是 0。该函数实现了我们在 OLED 模块上任意位置画点的功能。 在介绍完画点函数之后,我们介绍一下显示字符函数,OLED_ShowChar,在介绍之前,我 们来介绍一下字符(ASCII 字符集)是怎么显示在 OLED 模块上去的。要显示字符,我们先要 有字符的点阵数据,ASCII 常用的字符集总共有 95 个,从空格符开始,分别为: !"#$%&'()*+, -0123456789 : ;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxy z{|}~. 我们先要得到这个字符集的点阵数据,这里我们介绍一个款很好的字符提取软件: PCtoLCD2002 完美版。该软件可以提供各种字符,包括汉字(字体和大小都可以自己设置)阵 提取,且取模方式可以设置好几种,常用的取模方式,该软件都支持。该软件还支持图形模式, 也就是用户可以自己定义图片的大小,然后画图,根据所画的图形再生成点阵数据,这功能在 制作图标或图片的时候很有用。 该软件的界面如图 17.3.1 所示: 259 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 17.3.1 PCtoLCD2002 软件界面 然后我们选择设置,在设置里面设置取模方式如图 17.3.2 所示: 图 17.3.2 设置取模方式 上图设置的取模方式,在右上角的取模说明里面有,即:从第一列开始向下每取 8 个点作 为一个字节,如果最后不足 8 个点就补满 8 位。取模顺序是从高到低,即第一个点作为最高位。 如*-------取为 10000000。其实就是按如图 17.3.3 所示的这种方式: 260 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 17.3.3 取模方式图解 从上到下,从左到右,高位在前。我们按这样的取模方式,然后把 ASCII 字符集按 12*6 大小和 16*0 大小取模出来(对应汉字大小为 12*12 和 16*16,字符的只有汉字的一半大!),保 存在 oledfont.h 里面,每个 12*6 的字符占用 12 个字节,每个 16*8 的字符占用 16 个字节。具 体见 oledfont.h 部分代码(该部分我们不再这里列出来了,请大家参考光盘里面的代码)。 在知道了取模方式之后,我们就可以根据取模的方式来编写显示字符的代码了,这里我们 针对以上取模方式的显示字符代码如下: //在指定位置显示一个字符,包括部分字符 //x:0~127 //y:0~63 //mode:0,反白显示;1,正常显示 //size:选择字体 12/16/24 void OLED_ShowChar(u8 x,u8 y,u8 chr,u8 size,u8 mode) { u8 temp,t,t1; u8 y0=y; u8 csize=(size/8+((size%8)?1:0))*(size/2);//得到字体一个字符对应点阵集所占的字节数 chr=chr-' ';//得到偏移后的值 for(t=0;t'~')t=' '; OLED_ShowNum(103,48,t,3,16);//显示 ASCII 字符的码值 delay_ms(500); LED0=!LED0; } } 该部分代码用于在 OLED 上显示一些字符,然后从空格键开始不停的循环显示 ASCII 字符 集,并显示该字符的 ASCII 值。注意在 main.c 文件里面包含 oled.h 头文件,同时把 oled.c 文件 加入到 HARDWARE 组下,然后我们编译此工程,直到编译成功为止。 17.4 下载验证 将代码下载到战舰 STM32 后,可以看到 DS0 不停的闪烁,提示程序已经在运行了。同时 可以看到 OLED 模块显示如图 17.4.1 所示: 263 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 17.4.1 OLED 显示效果 图中 OLED 显示了三种尺寸的字符:24*12(ALIENTEK)、16*8(0.96’ OLED TEST)和 12*6(剩下的内容)。说明我们的实验是成功的,实现了三种不同尺寸 ASCII 字符的显示,在 最后一行不停的显示 ASCII 字符以及其码值。 通过这一章的学习,我们学会了 ALIENTEK OLED 模块的使用,在调试代码的时候,又多 了一种显示信息的途径,在以后的程序编写中,大家可以好好利用。 264 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第十八章 TFTLCD 显示实验 上一章我们介绍了 OLED 模块及其显示,但是该模块只能显示单色/双色,不能显示彩色, 而且尺寸也较小。本章我们将介绍 ALIENTEK 2.8 寸 TFT LCD 模块,该模块采用 TFTLCD 面 板,可以显示 16 位色的真彩图片。在本章中,我们将利用战舰 STM32 开发板上的 LCD 接口, 来点亮 TFTLCD,并实现 ASCII 字符和彩色的显示等功能,并在串口打印 LCD 控制器 ID,同 时在 LCD 上面显示。本章分为如下几个部分: 18.1 TFTLCD 简介 18.2 硬件设计 18.3 软件设计 18.4 下载验证 265 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 18.1 TFTLCD&FSMC 简介 本章我们将通过 STM32 的 FSMC 接口来控制 TFTLCD 的显示,所以本节分为两个部分, 分别介绍 TFTLCD 和 FSMC。 18.1.1 TFTLCD 简介 TFT-LCD 即薄膜晶体管液晶显示器。其英文全称为:Thin Film Transistor-Liquid Crystal Display。TFT-LCD 与无源 TN-LCD、STN-LCD 的简单矩阵不同,它在液晶显示屏的每一个象 素上都设置有一个薄膜晶体管(TFT),可有效地克服非选通时的串扰,使显示液晶屏的静态特 性与扫描线数无关,因此大大提高了图像质量。TFT-LCD 也被叫做真彩液晶显示器。 上一章介绍了 OLED 模块,本章,我们给大家介绍 ALIENTEK TFTLCD 模块,该模块有 如下特点: 1,2.4’/2.8’/3.5’/4.3’/7’ 5 种大小的屏幕可选。 2,320×240 的分辨率(3.5’分辨率为:320*480,4.3’和 7’分辨率为:800*480)。 3,16 位真彩显示。 4,自带触摸屏,可以用来作为控制输入。 本章,我们以 2.8 寸的 ALIENTEK TFTLCD 模块为例介绍,该模块支持 65K 色显示,显示 分辨率为 320×240,接口为 16 位的 80 并口,自带触摸屏。 该模块的外观图如图 18.1.1.1 所示: 图 18.1.1.1 ALIENTEK 2.8 寸 TFTLCD 外观图 模块原理图如图 18.1.1.2 所示: 266 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 18.1.1.2 ALIENTEK 2.8 寸 TFTLCD 模块原理图 TFTLCD 模块采用 2*17 的 2.54 公排针与外部连接,接口定义如图 18.1.1.3 所示: 图 18.1.1.3 ALIENTEK 2.8 寸 TFTLCD 模块接口图 从图 18.1.1.3 可以看出,ALIENTEK TFTLCD 模块采用 16 位的并方式与外部连接,之所以 不采用 8 位的方式,是因为彩屏的数据量比较大,尤其在显示图片的时候,如果用 8 位数据线, 就会比 16 位方式慢一倍以上,我们当然希望速度越快越好,所以我们选择 16 位的接口。图 18.1.3 还列出了触摸屏芯片的接口,关于触摸屏本章我们不多介绍,后面的章节会有详细的介绍。该 模块的 80 并口有如下一些信号线: CS:TFTLCD 片选信号。 WR:向 TFTLCD 写入数据。 RD:从 TFTLCD 读取数据。 D[15:0]:16 位双向数据线。 RST:硬复位 TFTLCD。 267 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 RS:命令/数据标志(0,读写命令;1,读写数据)。 80 并口在上一节我们已经有详细的介绍了,这里我们就不再介绍,需要说明的是,TFTLCD 模块的 RST 信号线是直接接到 STM32 的复位脚上,并不由软件控制,这样可以省下来一个 IO 口。另外我们还需要一个背光控制线来控制 TFTLCD 的背光。所以,我们总共需要的 IO 口数 目为 21 个。这里还需要注意,我们标注的 DB1~DB8,DB10~DB17,是相对于 LCD 控制 IC 标 注的,实际上大家可以把他们就等同于 D0~D15,这样理解起来就比较简单一点。 ALIENTEK 提 供 的 2.8 寸 TFTLCD 模 块 , 其 驱 动 芯 片 有 很 多 种 类 型 , 比 如 有 : ILI9341/ILI9325/RM68042/RM68021/ILI9320/ILI9328/LGDP4531/LGDP4535/SPFD5408/SSD128 9/1505/B505/C505/NT35310/NT35510 等(具体的型号,大家可以通过下载本章实验代码,通过串 口或者 LCD 显示查看),这里我们仅以 ILI9341 控制器为例进行介绍,其他的控制基本都类似, 我们就不详细阐述了。 ILI9341 液晶控制器自带显存,其显存总大小为 172800(240*320*18/8),即 18 位模式(26 万色)下的显存量。在 16 位模式下,ILI9341 采用 RGB565 格式存储颜色数据,此时 ILI9341 的 18 位数据线与 MCU 的 16 位数据线以及 LCD GRAM 的对应关系如图 18.1.1.4 所示: 图 18.1.1.4 16 位数据与显存对应关系图 从图中可以看出,ILI9341 在 16 位模式下面,数据线有用的是:D17~D13 和 D11~D1,D0 和 D12 没有用到,实际上在我们 LCD 模块里面,ILI9341 的 D0 和 D12 压根就没有引出来,这 样,ILI9341 的 D17~D13 和 D11~D1 对应 MCU 的 D15~D0。 这样 MCU 的 16 位数据,最低 5 位代表蓝色,中间 6 位为绿色,最高 5 位为红色。数值越 大,表示该颜色越深。另外,特别注意 ILI9341 所有的指令都是 8 位的(高 8 位无效),且参数 除了读写 GRAM 的时候是 16 位,其他操作参数,都是 8 位的,这个和 ILI9320 等驱动器不一 样,必须加以注意。 接下来,我们介绍一下 ILI9341 的几个重要命令,因为 ILI9341 的命令很多,我们这里就 不全部介绍了,有兴趣的大家可以找到 ILI9341 的 datasheet 看看。里面对这些命令有详细的介 绍。我们将介绍:0XD3,0X36,0X2A,0X2B,0X2C,0X2E 等 6 条指令。 首先来看指令:0XD3,这个是读 ID4 指令,用于读取 LCD 控制器的 ID,该指令如表 18.1.1.1 所示: 控制 顺序 RS RD WR D15~D8 各位描述 HEX D7 D6 D5 D4 D3 D2 D1 D0 指令 0 1 ↑ XX 1 1 0 1 0 0 1 1 D3H 参数 1 1 ↑ 1 XX XXXXXXXX X 参数 2 1 ↑ 1 XX 0 0 0 0 0 0 0 0 00H 参数 3 1 ↑ 1 XX 1 0 0 1 0 0 1 1 93H 参数 4 1 ↑ 1 XX 0 1 0 0 0 0 0 1 41H 表 18.1.1.1 0XD3 指令描述 从上表可以看出,0XD3 指令后面跟了 4 个参数,最后 2 个参数,读出来是 0X93 和 0X41, 刚好是我们控制器 ILI9341 的数字部分,从而,通过该指令,即可判别所用的 LCD 驱动器是什 么型号,这样,我们的代码,就可以根据控制器的型号去执行对应驱动 IC 的初始化代码,从而 兼容不同驱动 IC 的屏,使得一个代码支持多款 LCD。 268 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 接下来看指令:0X36,这是存储访问控制指令,可以控制 ILI9341 存储器的读写方向,简 单的说,就是在连续写 GRAM 的时候,可以控制 GRAM 指针的增长方向,从而控制显示方式 (读 GRAM 也是一样)。该指令如表 18.1.1.2 所示: 控制 顺序 RS RD WR D15~D8 各位描述 HEX D7 D6 D5 D4 D3 D2 D1 D0 指令 0 1 ↑ XX 0 0 1 1 0 1 1 0 36H 参数 1 1 ↑ XX MY MX MV ML BGR MH 0 0 0 表 18.1.1.2 0X36 指令描述 从上表可以看出,0X36 指令后面,紧跟一个参数,这里我们主要关注:MY、MX、MV 这三个位,通过这三个位的设置,我们可以控制整个 ILI9341 的全部扫描方向,如表 18.1.1.3 所示: 控制位 效果 MY MX MV LCD 扫描方向(GRAM 自增方式) 000 从左到右,从上到下 100 010 110 从左到右,从下到上 从右到左,从上到下 从右到左,从下到上 001 从上到下,从左到右 011 从上到下,从右到左 101 111 从下到上,从左到右 从下到上,从右到左 表 18.1.1.3 MY、MX、MV 设置与 LCD 扫描方向关系表 这样,我们在利用 ILI9341 显示内容的时候,就有很大灵活性了,比如显示 BMP 图片, BMP 解码数据,就是从图片的左下角开始,慢慢显示到右上角,如果设置 LCD 扫描方向为从 左到右,从下到上,那么我们只需要设置一次坐标,然后就不停的往 LCD 填充颜色数据即可, 这样可以大大提高显示速度。 接下来看指令:0X2A,这是列地址设置指令,在从左到右,从上到下的扫描方式(默认) 下面,该指令用于设置横坐标(x 坐标),该指令如表 18.1.1.4 所示: 控制 各位描述 顺序 RS RD WR D15~D8 D7 D6 D5 D4 D3 D2 D1 D0 HEX 指令 0 1 ↑ XX 0 0 1 0 1 0 1 0 2AH 参数 1 1 1 ↑ XX SC15 SC14 SC13 SC12 SC11 SC10 SC9 SC8 SC 参数 2 1 1 ↑ XX SC7 SC6 SC5 SC4 SC3 SC2 SC1 SC0 参数 3 1 1 ↑ XX EC15 EC14 EC13 EC12 EC11 EC10 EC9 EC8 EC 参数 4 1 1 ↑ XX EC7 EC6 EC5 EC4 EC3 EC2 EC1 EC0 表 18.1.1.4 0X2A 指令描述 在默认扫描方式时,该指令用于设置 x 坐标,该指令带有 4 个参数,实际上是 2 个坐标值: SC 和 EC,即列地址的起始值和结束值,SC 必须小于等于 EC,且 0≤SC/EC≤239。一般在设 置 x 坐标的时候,我们只需要带 2 个参数即可,也就是设置 SC 即可,因为如果 EC 没有变化, 我们只需要设置一次即可(在初始化 ILI9341 的时候设置),从而提高速度。 与 0X2A 指令类似,指令:0X2B,是页地址设置指令,在从左到右,从上到下的扫描方式 (默认)下面,该指令用于设置纵坐标(y 坐标)。该指令如表 18.1.1.5 所示: 269 STM32F1 开发指南(库函数版) 顺序 指令 参数 1 参数 2 参数 3 参数 4 控制 RS RD WR 0 1↑ 1 1↑ 1 1↑ 1 1↑ 1 1↑ D15~D8 XX XX XX XX XX D7 0 SP15 SP7 EP15 EP7 D6 0 SP14 SP6 EP14 EP6 ALIENTEK 战舰 STM32F103 V3 开发板教程 各位描述 D5 D4 D3 HEX D2 D1 D0 1 0 1 0 1 0 2BH SP13 SP12 SP11 SP10 SP9 SP8 SP SP5 SP4 SP3 SP2 SP1 SP0 EP13 EP12 EP11 EP10 EP9 EP8 EP EP5 EP4 EP3 EP2 EP1 EP0 表 18.1.1.5 0X2B 指令描述 在默认扫描方式时,该指令用于设置 y 坐标,该指令带有 4 个参数,实际上是 2 个坐标值: SP 和 EP,即页地址的起始值和结束值,SP 必须小于等于 EP,且 0≤SP/EP≤319。一般在设置 y 坐标的时候,我们只需要带 2 个参数即可,也就是设置 SP 即可,因为如果 EP 没有变化,我 们只需要设置一次即可(在初始化 ILI9341 的时候设置),从而提高速度。 接下来看指令:0X2C,该指令是写 GRAM 指令,在发送该指令之后,我们便可以往 LCD 的 GRAM 里面写入颜色数据了,该指令支持连续写,指令描述如表 18.1.1.6 所示: 顺序 控制 RS RD WR D15~D8 各位描述 HEX D7 D6 D5 D4 D3 D2 D1 D0 指令 0 1 ↑ XX 0 0 1 0 1 1 0 0 2CH 参数 1 1 1 ↑ D1[15:0] XX …… 1 1 ↑ D2[15:0] XX 参数 n 1 1 ↑ Dn[15:0] XX 表 18.1.1.6 0X2C 指令描述 从上表可知,在收到指令 0X2C 之后,数据有效位宽变为 16 位,我们可以连续写入 LCD GRAM 值,而 GRAM 的地址将根据 MY/MX/MV 设置的扫描方向进行自增。例如:假设设置 的是从左到右,从上到下的扫描方式,那么设置好起始坐标(通过 SC,SP 设置)后,每写入 一个颜色值,GRAM 地址将会自动自增 1(SC++),如果碰到 EC,则回到 SC,同时 SP++,一 直到坐标:EC,EP 结束,其间无需再次设置的坐标,从而大大提高写入速度。 最后,来看看指令:0X2E,该指令是读 GRAM 指令,用于读取 ILI9341 的显存(GRAM), 该指令在 ILI9341 的数据手册上面的描述是有误的,真实的输出情况如表 18.1.1.7 所示: 控制 各位描述 顺序 RS RD WR D15~D11 D10 D9 D8 D7 D6 D5 D4 D3 D2 D1 D0 HEX 指令 0 1 ↑ XX 0 0 1 0 1 1 1 0 2EH 参数 1 1 ↑ 1 XX dummy 参数 2 1 ↑ 1 R1[4:0] XX G1[5:0] XX R1G1 参数 3 1 ↑ 1 B1[4:0] XX R2[4:0] XX B1R2 参数 4 1 ↑ 1 G2[5:0] XX B2[4:0] XX G2B2 参数 5 1 ↑ 1 R3[4:0] XX G3[5:0] XX R3G3 参数 N 1 ↑ 1 按以上规律输出 表 18.1.1.7 0X2E 指令描述 该指令用于读取 GRAM,如表 18.1.1.7 所示,ILI9341 在收到该指令后,第一次输出的是 dummy 数据,也就是无效的数据,第二次开始,读取到的才是有效的 GRAM 数据(从坐标: SC,SP 开始),输出规律为:每个颜色分量占 8 个位,一次输出 2 个颜色分量。比如:第一次 输出是 R1G1,随后的规律为:B1R2G2B2R3G3B3R4G4B4R5G5... 以此类推。如果 我们只需要读取一个点的颜色值,那么只需要接收到参数 3 即可,如果要连续读取(利用 GRAM 270 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 地址自增,方法同上),那么就按照上述规律去接收颜色数据。 以上,就是操作 ILI9341 常用的几个指令,通过这几个指令,我们便可以很好的控制 ILI9341 显示我们所要显示的内容了。 一般 TFTLCD 模块的使用流程如图 18.1.1.5: 硬复位 LCD_RST=0; delay_ms(100); LCD_RST=1; 初始化序列 写GRAM指令 设置坐标 读GRAM指令 写入颜色数据 读出颜色数据 LCD显示 单片机处理 图 18.1.1.5 TFTLCD 使用流程 任何 LCD,使用流程都可以简单的用以上流程图表示。其中硬复位和初始化序列,只需要 执行一次即可。而画点流程就是:设置坐标写 GRAM 指令写入颜色数据,然后在 LCD 上 面,我们就可以看到对应的点显示我们写入的颜色了。读点流程为:设置坐标读 GRAM 指令 读取颜色数据,这样就可以获取到对应点的颜色数据了。 以上只是最简单的操作,也是最常用的操作,有了这些操作,一般就可以正常使用 TFTLCD 了。接下来我们将该模块(2.8 寸屏模块)用来来显示字符和数字,通过以上介绍,我们可以得 出 TFTLCD 显示需要的相关设置步骤如下: 1)设置 STM32F1 与 TFTLCD 模块相连接的 IO。 这一步,先将我们与 TFTLCD 模块相连的 IO 口进行初始化,以便驱动 LCD。这里我们用 到的是 FSMC,FSMC 将在 18.1.2 节向大家详细介绍。 2)初始化 TFTLCD 模块。 即图 18.1.1.5 的初始化序列,这里我们没有硬复位 LCD,因为战舰 STM32F103 的 LCD 接 口,将 TFTLCD 的 RST 同 STM32F1 的 RESET 连接在一起了,只要按下开发板的 RESET 键, 就会对 LCD 进行硬复位。初始化序列,就是向 LCD 控制器写入一系列的设置值(比如伽马校 准),这些初始化序列一般 LCD 供应商会提供给客户,我们直接使用这些序列即可,不需要深 入研究。在初始化之后,LCD 才可以正常使用。 3)通过函数将字符和数字显示到 TFTLCD 模块上。 这一步则通过图 18.1.1.5 左侧的流程,即:设置坐标写 GRAM 指令写 GRAM 来实现, 但是这个步骤,只是一个点的处理,我们要显示字符/数字,就必须要多次使用这个步骤,从而 达到显示字符/数字的目的,所以需要设计一个函数来实现数字/字符的显示,之后调用该函数, 就可以实现数字/字符的显示了。 18.1.2 FSMC 简介 大容量,且引脚数在 100 脚以上的 STM32F103 芯片都带有 FSMC 接口,ALIENTEK 战舰 271 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 STM32 开发板的主芯片为 STM32F103ZET6,是带有 FSMC 接口的。 FSMC,即灵活的静态存储控制器,能够与同步或异步存储器和 16 位 PC 存储器卡连接, STM32 的 FSMC 接口支持包括 SRAM、NAND FLASH、NOR FLASH 和 PSRAM 等存储器。 FSMC 的框图如图 18.1.2.1 所示: 图 18.1.2.1 FSMC 框图 从上图我们可以看出,STM32 的 FSMC 将外部设备分为 3 类:NOR/PSRAM 设备、NAND 设备、PC 卡设备。他们共用地址数据总线等信号,他们具有不同的 CS 以区分不同的设备,比 如本章我们用到的 TFTLCD 就是用的 FSMC_NE4 做片选,其实就是将 TFTLCD 当成 SRAM 来 控制。 这里我们介绍下为什么可以把 TFTLCD 当成 SRAM 设备用:首先我们了解下外部 SRAM 的连接,外部 SRAM 的控制一般有:地址线(如 A0~A18)、数据线(如 D0~D15)、写信号(WE)、 读信号(OE)、片选信号(CS),如果 SRAM 支持字节控制,那么还有 UB/LB 信号。而 TFTLCD 的信号我们在 18.1.1 节有介绍,包括:RS、D0~D15、WR、RD、CS、RST 和 BL 等,其中真 正在操作 LCD 的时候需要用到的就只有:RS、D0~D15、WR、RD 和 CS。其操作时序和 SRAM 的控制完全类似,唯一不同就是 TFTLCD 有 RS 信号,但是没有地址信号。 TFTLCD 通过 RS 信号来决定传送的数据是数据还是命令,本质上可以理解为一个地址信 号,比如我们把 RS 接在 A0 上面,那么当 FSMC 控制器写地址 0 的时候,会使得 A0 变为 0, 272 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 对 TFTLCD 来说,就是写命令。而 FSMC 写地址 1 的时候,A0 将会变为 1,对 TFTLCD 来说, 就是写数据了。这样,就把数据和命令区分开了,他们其实就是对应 SRAM 操作的两个连续地 址。当然 RS 也可以接在其他地址线上,战舰 STM32 开发板是把 RS 连接在 A10 上面的。 STM32 的 FSMC 支持 8/16/32 位数据宽度,我们这里用到的 LCD 是 16 位宽度的,所以在 设置的时候,选择 16 位宽就 OK 了。我们再来看看 FSMC 的外部设备地址映像,STM32 的 FSMC 将外部存储器划分为固定大小为 256M 字节的四个存储块,如图 18.1.2.2 所示: 图 18.1.2.2 FSMC 存储块地址映像 从上图可以看出,FSMC 总共管理 1GB 空间,拥有 4 个存储块(Bank),本章,我们用到 的是块 1,所以在本章我们仅讨论块 1 的相关配置,其他块的配置,请参考《STM32 参考手册》 第 19 章(324 页)的相关介绍。 STM32 的 FSMC 存储块 1(Bank1)被分为 4 个区,每个区管理 64M 字节空间,每个区都 有独立的寄存器对所连接的存储器进行配置。Bank1 的 256M 字节空间由 28 根地址线 (HADDR[27:0])寻址。 这 里 HADDR 是 内 部 AHB 地 址 总 线 , 其 中 HADDR[25:0] 来 自 外 部 存 储 器 地 址 FSMC_A[25:0],而 HADDR[26:27]对 4 个区进行寻址。如表 18.1.2.1 所示: Bank1 所选区 片选信号 地址范围 HADDR [27:26] [25:0] 第1区 第2区 FSMC_NE1 FSMC_NE2 0X6000,0000~63FF,FFFF 0X6400,0000~67FF,FFFF 00 FSMC_A[25:0] 01 273 STM32F1 开发指南(库函数版) 第3区 第4区 FSMC_NE3 FSMC_NE4 ALIENTEK 战舰 STM32F103 V3 开发板教程 0X6800,0000~6BFF,FFFF 10 0X6C00,0000~6FFF,FFFF 11 表 18.1.2.1 Bank1 存储区选择表 表 18.1.2.1 中,我们要特别注意 HADDR[25:0]的对应关系: 当 Bank1 接的是 16 位宽度存储器的时候:HADDR[25:1] FSMC[24:0]。 当 Bank1 接的是 8 位宽度存储器的时候:HADDR[25:0] FSMC[25:0]。 不论外部接 8 位/16 位宽设备,FSMC_A[0]永远接在外部设备地址 A[0]。 这里,TFTLCD 使用的是 16 位数据宽度,所以 HADDR[0]并没有用到,只有 HADDR[25:1]是有效的,对应关 系变为:HADDR[25:1] FSMC[24:0],相当于右移了一位,这里请大家特别留意。另外, HADDR[27:26]的设置,是不需要我们干预的,比如:当你选择使用 Bank1 的第三个区,即使 用 FSMC_NE3 来连接外部设备的时候,即对应了 HADDR[27:26]=10,我们要做的就是配置对 应第 3 区的寄存器组,来适应外部设备即可。STM32 的 FSMC 各 Bank 配置寄存器如表 18.1.2.2 所示: 内部控制器 存储块 管理的地址范围 支持的设备类型 配置寄存器 NOR FLASH 控制器 Bank1 0X6000,0000~ 0X6FFF,FFFF SRAM/ROM NOR FLASH PSRAM FSMC_BCR1/2/3/4 FSMC_BTR1/2/2/3 FSMC_BWTR1/2/3/4 NAND FLASH /PC CARD 控制器 Bank2 Bank3 Bank4 0X7000,0000~ 0X7FFF,FFFF 0X8000,0000~ 0X8FFF,FFFF 0X9000,0000~ 0X9FFF,FFFF NAND FLASH PC Card FSMC_PCR2/3/4 FSMC_SR2/3/4 FSMC_PMEM2/3/4 FSMC_PATT2/3/4 FSMC_PIO4 表 18.1.2.2 FSMC 各 Bank 配置寄存器表 对于 NOR FLASH 控制器,主要是通过 FSMC_BCRx、FSMC_BTRx 和 FSMC_BWTRx 寄 存器设置(其中 x=1~4,对应 4 个区)。通过这 3 个寄存器,可以设置 FSMC 访问外部存储器 的时序参数,拓宽了可选用的外部存储器的速度范围。FSMC 的 NOR FLASH 控制器支持同步 和异步突发两种访问方式。选用同步突发访问方式时,FSMC 将 HCLK(系统时钟)分频后,发 送给外部存储器作为同步时钟信号 FSMC_CLK。此时需要的设置的时间参数有 2 个: 1,HCLK 与 FSMC_CLK 的分频系数(CLKDIV),可以为 2~16 分频; 2,同步突发访问中获得第 1 个数据所需要的等待延迟(DATLAT)。 对于异步突发访问方式,FSMC 主要设置 3 个时间参数:地址建立时间(ADDSET)、数据 建立时间(DATAST)和地址保持时间(ADDHLD)。FSMC 综合了 SRAM/ROM、PSRAM 和 NOR Flash 产品的信号特点,定义了 4 种不同的异步时序模型。选用不同的时序模型时,需要设置不 同的时序参数,如表 18.1.2.3 所列: 时序模型 简单描述 时间参数 Mode1 SRAM/CRAM 时序 DATAST、ADDSET ModeA SRAM/CRAM OE 选通型时序 DATAST、ADDSET 异步 Mode2/B NOR FLASH 时序 DATAST、ADDSET ModeC NOR FLASH OE 选通型时序 DATAST、ADDSET ModeD 延长地址保持时间的异步时序 DATAST、ADDSET、ADDHLK 274 STM32F1 开发指南(库函数版) 同步突发 ALIENTEK 战舰 STM32F103 V3 开发板教程 根据同步时钟 FSMC_CK 读取 多个顺序单元的数据 CLKDIV、DATLAT 表 18.1.2.3 NOR FLASH 控制器支持的时序模型 在实际扩展时,根据选用存储器的特征确定时序模型,从而确定各时间参数与存储器读/ 写周期参数指标之间的计算关系;利用该计算关系和存储芯片数据手册中给定的参数指标,可 计算出 FSMC 所需要的各时间参数,从而对时间参数寄存器进行合理的配置。 本章,我们使用异步模式 A(ModeA)方式来控制 TFTLCD,模式 A 的读操作时序如图 18.1.2.3 所示: 图 18.1.2.3 模式 A 读操作时序图 模式 A 支持独立的读写时序控制,这个对我们驱动 TFTLCD 来说非常有用,因为 TFTLCD 在读的时候,一般比较慢,而在写的时候可以比较快,如果读写用一样的时序,那么只能以读 的时序为基准,从而导致写的速度变慢,或者在读数据的时候,重新配置 FSMC 的延时,在读 操作完成的时候,再配置回写的时序,这样虽然也不会降低写的速度,但是频繁配置,比较麻 烦。而如果有独立的读写时序控制,那么我们只要初始化的时候配置好,之后就不用再配置, 既可以满足速度要求,又不需要频繁改配置。 模式 A 的写操作时序如图 18.1.2.4 所示: 275 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 18.1.2.4 模式 A 写操作时序 从模式 A 的读写时序图,我们可以看出,读操作还存在额外的 2 个 HCLK 周期,用于数据 存储,所以同样的配置读操作一般比写操作会慢一点。图 18.1.2.3 和图 18.1.2.4 中的 ADDSET 与 DATAST,是通过不同的寄存器设置的,接下来我们讲解一下 Bank1 的几个控制寄存器 首先,我们介绍 SRAM/NOR 闪存片选控制寄存器:FSMC_BCRx(x=1~4),该寄存器各位 描述如图 18.1.2.5 所示: 18.1.2.5 FSMC_BCRx 寄存器各位描述 该寄存器我们在本章用到的设置有:EXTMOD、WREN、MWID、MTYP 和 MBKEN 这几 个设置,我们将逐个介绍。 EXTMOD:扩展模式使能位,也就是是否允许读写不同的时序,很明显,我们本章需要读 写不同的时序,故该位需要设置为 1。 WREN:写使能位。我们需要向 TFTLCD 写数据,故该位必须设置为 1。 MWID[1:0]:存储器数据总线宽度。00,表示 8 位数据模式;01 表示 16 位数据模式;10 和 11 保留。我们的 TFTLCD 是 16 位数据线,所以设置 WMID[1:0]=01。 MTYP[1:0]:存储器类型。00 表示 SRAM、ROM;01 表示 PSRAM;10 表示 NOR FLASH;11 保留。前面提到,我们把 TFTLCD 当成 SRAM 用,所以需要设置 MTYP[1:0]=00。 MBKEN:存储块使能位。这个容易理解,我们需要用到该存储块控制 TFTLCD,当然要 使能这个存储块了。 接下来,我们看看 SRAM/NOR 闪存片选时序寄存器:FSMC_BTRx(x=1~4),该寄存器各 位描述如图 18.1.2.6 所示: 276 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 18.1.2.6 FSMC_BTRx 寄存器各位描述 这个寄存器包含了每个存储器块的控制信息,可以用于 SRAM、ROM 和 NOR 闪存存储器。 如果 FSMC_BCRx 寄存器中设置了 EXTMOD 位,则有两个时序寄存器分别对应读(本寄存器) 和写操作(FSMC_BWTRx 寄存器)。因为我们要求读写分开时序控制,所以 EXTMOD 是使能了 的,也就是本寄存器是读操作时序寄存器,控制读操作的相关时序。本章我们要用到的设置有: ACCMOD、DATAST 和 ADDSET 这三个设置。 ACCMOD[1:0]:访问模式。00 表示访问模式 A;01 表示访问模式 B;10 表示访问模式 C; 11 表示访问模式 D,本章我们用到模式 A,故设置为 00。 DATAST[7:0]:数据保持时间。0 为保留设置,其他设置则代表保持时间为: (DATAST +1) 个 HCLK 时钟周期,最大为 256 个 HCLK 周期。对 ILI9320 来说,其实就是 RD 低电平持续时 间,一般为 150ns。而一个 HCLK 时钟周期为 13.8ns 左右(1/72Mhz),为了兼容其他屏,我们 这里设置 DATAST 为 15,也就是 16 个 HCLK 周期,时间大约是 234ns(未计算数据存储的 2 个 HCLK 时间)。 ADDSET[3:0]:地址建立时间。其建立时间为:(ADDSET+1)个 HCLK 周期,最大为 16 个 HCLK 周期。对 ILI9320 来说,这里相当于 RD 高电平持续时间,本来这里我们应该设置和 DATAST 一样,但是由于 CS 切换延时的存在,我们这里可以设置 ADDSET 为较小的值,本章 我们设置 ADDSET 为 1,即 2 个 HCLK 周期,同样可以正常使用。 最后,我们再来看看 SRAM/NOR 闪写时序寄存器:FSMC_BWTRx(x=1~4),该寄存器各 位描述如图 18.1.2.7 所示: 图 18.1.2.7 FSMC_BWTRx 寄存器各位描述 该寄存器在本章用作写操作时序控制寄存器,需要用到的设置同样是:ACCMOD、DATAST 和 ADDSET 这三个设置。这三个设置的方法同 FSMC_BTRx 一模一样,只是这里对应的是写 操作的时序,ACCMOD 设置同 FSMC_BTRx 一模一样,同样是选择模式 A,另外 DATAST 和 ADDSET 则对应低电平和高电平持续时间,对 ILI9320 来说,这两个时间只需要 50ns 就够了, 比读操作快得多。所以我们这里设置 DATAST 为 3,即 4 个 HCLK 周期,时间约为 55ns。同样 由于 CS 切换延时的存在,我们可以设置 ADDSET 为 0,即 1 个 HCLK 周期。 至此,我们对 STM32 的 FSMC 介绍就差不多了,通过以上两个小节的了解,我们可以开 始写 LCD 的驱动代码了。不过,这里还要给大家做下科普,在 MDK 的寄存器定义里面,并没 有定义 FSMC_BCRx、FSMC_BTRx、FSMC_BWTRx 等这个单独的寄存器,而是将他们进行了 一些组合。 FSMC_BCRx 和 FSMC_BTRx,组合成 BTCR[8]寄存器组,他们的对应关系如下: BTCR[0]对应 FSMC_BCR1,BTCR[1]对应 FSMC_BTR1 BTCR[2]对应 FSMC_BCR2,BTCR[3]对应 FSMC_BTR2 277 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 BTCR[4]对应 FSMC_BCR3,BTCR[5]对应 FSMC_BTR3 BTCR[6]对应 FSMC_BCR4,BTCR[7]对应 FSMC_BTR4 FSMC_BWTRx 则组合成 BWTR[7],他们的对应关系如下: BWTR[0]对应 FSMC_BWTR1,BWTR[2]对应 FSMC_BWTR2, BWTR[4]对应 FSMC_BWTR3,BWTR[6]对应 FSMC_BWTR4, BWTR[1]、BWTR[3]和 BWTR[5]保留,没有用到。 通过上面的讲解,通过对 FSMC 相关的寄存器的描述,大家对 FSMC 的原理有了一个初步 的认识,如果还不熟悉的朋友,请一定要搜索网络资料理解 FSMC 的原理。只有理解了原理, 使用库函数才可以得心应手。那么在库函数中是怎么实现 FSMC 的配置的呢?FSMC_BCRx, FSMC_BTRx 寄存器在库函数是通过什么函数来配置的呢?下面我们来讲解一下 FSMC 相关的 库函数: 1.FSMC 初始化函数 根据前面的讲解,初始化 FSMC 主要是初始化三个寄存器 FSMC_BCRx,FSMC_BTRx, FSMC_BWTRx,那么在固件库中是怎么初始化这三个参数的呢? 固件库提供了 3 个 FSMC 初始化函数分别为 FSMC_NORSRAMInit(); FSMC_NANDInit(); FSMC_PCCARDInit(); 这三个函数分别用来初始化 4 种类型存储器。这里根据名字就很好判断对应关系。用来初始化 NOR 和 SRAM 使用同一个函数 FSMC_NORSRAMInit()。所以我们之后使用的 FSMC 初始化函 数为 FSMC_NORSRAMInit()。下面我们看看函数定义: void FSMC_NORSRAMInit(FSMC_NORSRAMInitTypeDef* FSMC_NORSRAMInitStruct); 这个函数只有一个入口参数,也就是 FSMC_NORSRAMInitTypeDef 类型指针变量,这个结构体 的成员变量非常多,因为 FSMC 相关的配置项非常多。 typedef struct { uint32_t FSMC_Bank; uint32_t FSMC_DataAddressMux; uint32_t FSMC_MemoryType; uint32_t FSMC_MemoryDataWidth; uint32_t FSMC_BurstAccessMode; uint32_t FSMC_AsynchronousWait; uint32_t FSMC_WaitSignalPolarity; uint32_t FSMC_WrapMode; uint32_t FSMC_WaitSignalActive; uint32_t FSMC_WriteOperation; uint32_t FSMC_WaitSignal; uint32_t FSMC_ExtendedMode; uint32_t FSMC_WriteBurst; FSMC_NORSRAMTimingInitTypeDef* FSMC_ReadWriteTimingStruct; FSMC_NORSRAMTimingInitTypeDef* FSMC_WriteTimingStruct; }FSMC_NORSRAMInitTypeDef; 从这个结构体我们可以看出,前面有 13 个基本类型(unit32_t)的成员变量,这 13 个参数是用 278 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 来配置片选控制寄存器 FSMC_BCRx。最后面还有两个 SMC_NORSRAMTimingInitTypeDef 指针类型的成员变量。前面我们讲到,FSMC 有读时序和 写时序之分,所以这里就是用来设置读时序和写时序的参数了, 也就是说,这两个参数是用来 配置寄存器 FSMC_BTRx 和 FSMC_BWTRx,后面我们会讲解到。下面我们主要来看看模式 A 下的相关配置参数: 参数 FSMC_Bank 用来设置使用到的存储块标号和区号,前面讲过,我们是使用的存储块 1 区 号 4,所以选择值为 FSMC_Bank1_NORSRAM4。 参数 FSMC_MemoryType 用来设置存储器类型,我们这里是 SRAM,所以选择值为 FSMC_MemoryType_SRAM。 参数 FSMC_MemoryDataWidth 用来设置数据宽度,可选 8 位还是 16 位,这里我们是 16 位数据 宽度,所以选择值为 FSMC_MemoryDataWidth_16b。 参数 FSMC_WriteOperation 用来设置写使能,毫无疑问,我们前面讲解过我们要向 TFT 写数据, 所以要写使能,这里我们选择 FSMC_WriteOperation_Enable。 参数 FSMC_ExtendedMode 是设置扩展模式使能位,也就是是否允许读写不同的时序,这里我 们采取的读写不同时序,所以设置值为 FSMC_ExtendedMode_Enable。 上面的这些参数是与模式 A 相关的,下面我们也来稍微了解一下其他几个参数的意义吧: 参数 FSMC_DataAddressMux 用来设置地址/数据复用使能,若设置为使能,那么地址的低 16 位和数据将共用数据总线,仅对 NOR 和 PSRAM 有效,所以我们设置为默认值不复用,值 FSMC_DataAddressMux_Disable。 参 数 FSMC_BurstAccessMode , FSMC_AsynchronousWait , FSMC_WaitSignalPolarity , FSMC_WaitSignalActive , FSMC_WrapMode , FSMC_WaitSignal FSMC_WriteBurst 和 FSMC_WaitSignal 这些参数在成组模式同步模式才需要设置,大家可以参考中文参考手册了解 相关参数的意思。 接 下 来 我 们 看 看 设 置 读 写 时 序 参 数 的 两 个 变 量 FSMC_ReadWriteTimingStruct 和 FSMC_WriteTimingStruct,他们都是 FSMC_NORSRAMTimingInitTypeDef 结构体指针类型,这 两个参数在初始化的时候分别用来初始化片选控制寄存器 FSMC_BTRx 和写操作时序控制寄存 器 FSMC_BWTRx。 下面我们看看 FSMC_NORSRAMTimingInitTypeDef 类型的定义: typedef struct { uint32_t FSMC_AddressSetupTime; uint32_t FSMC_AddressHoldTime; uint32_t FSMC_DataSetupTime; uint32_t FSMC_BusTurnAroundDuration; uint32_t FSMC_CLKDivision; uint32_t FSMC_DataLatency; uint32_t FSMC_AccessMode; }FSMC_NORSRAMTimingInitTypeDef; 这个结构体有 7 个参数用来设置 FSMC 读写时序。其实这些参数的意思我们前面在讲解 FSMC 的时序的时候有提到,主要是设计地址建立保持时间,数据建立时间等等配置,对于我们的实 验中,读写时序不一样,读写速度要求不一样,所以对于参数 FSMC_DataSetupTime 设置了不 同 的 值 , 大 家 可 以 对 照 理 解 一 下 。 记 住 , 这 些 参 数 的 意 义 在 前 面 讲 解 FSMC_BTRx 和 FSMC_BWTRx 寄存器的时候都有提到,大家可以翻过去看看。 279 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 2.FSMC 使能函数 FSMC 对不同的存储器类型同样提供了不同的使能函数: void FSMC_NORSRAMCmd(uint32_t FSMC_Bank, FunctionalState NewState); void FSMC_NANDCmd(uint32_t FSMC_Bank, FunctionalState NewState); void FSMC_PCCARDCmd(FunctionalState NewState); 这个就比较好理解,我们这里不讲解,我们是 SRAM,所以使用的第一个函数。 18.2 硬件设计 本实验用到的硬件资源有: 1) 指示灯 DS0 2) TFTLCD 模块 TFTLCD 模块的电路见图 18.1.1.2,这里我们介绍 TFTLCD 模块与 ALIETEK 战舰 STM32 开发板的连接,战舰 STM32 开发板底板的 LCD 接口和 ALIENTEK TFTLCD 模块直接可以对 插,连接关系如图 18.2.1 所示: 图 18.2.1 TFTLCD 与开发板连接示意图 图 18.2.1 中圈出来的部分就是连接 TFTLCD 模块的接口,板上的接口比液晶模块的插针要 多 2 个口,液晶模块在这里是靠右插的。多出的 2 个口是给 OLED 用的,所以 OLED 模块在接 这里的时候,是靠左插的,这个请大家注意一下。 在硬件上,TFTLCD 模块与战舰 STM32 开发板的 IO 口对应关系如下: LCD_BL(背光控制)对应 PB0; LCD_CS 对应 PG12 即 FSMC_NE4; LCD _RS 对应 PG0 即 FSMC_A10; LCD _WR 对应 PD5 即 FSMC_NWE; LCD _RD 对应 PD4 即 FSMC_NOE; LCD _D[15:0]则直接连接在 FSMC_D15~FSMC_D0; 这些线的连接,战舰 STM32 开发板的内部已经连接好了,我们只需要将 TFTLCD 模块插 上去就好了。实物连接(4.3 寸 TFTLCD 模块)如图 18.2.2 所示: 280 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 18.2.2 TFTLCD 与开发板连接实物图 18.3 软件设计 打开我们光盘的 TFT LCD 显示实验工程可以看到我们添加了两个文件 lcd.c 和头文件 lcd.h。同时,FSMC 相关的库函数分布在 stm32f10x_fsmc.c 文件和头文件 stm32f10x_fsmc.h 中。 在 lcd.c 里面代码比较多,我们这里就不贴出来了,只针对几个重要的函数进行讲解。完整 版的代码见光盘4,程序源码标准例程-V3.5 库函数版本实验 13 TFTLCD 显示实验的 lcd.c 文件。 本实验,我们用到 FSMC 驱动 LCD,通过前面的介绍,我们知道 TFTLCD 的 RS 接在 FSMC 的 A10 上面,CS 接在 FSMC_NE4 上,并且是 16 位数据总线。即我们使用的是 FSMC 存储器 1 的第 4 区,我们定义如下 LCD 操作结构体(在 lcd.h 里面定义): //LCD 操作结构体 typedef struct { vu16 LCD_REG; vu16 LCD_RAM; } LCD_TypeDef; //使用 NOR/SRAM 的 Bank1.sector4,地址位 HADDR[27,26]=11 A10 作为数据命令区分线 //注意 16 位数据总线时,STM32 内部地址会右移一位对齐! #define LCD_BASE ((u32)(0x6C000000 | 0x000007FE)) #define LCD ((LCD_TypeDef *) LCD_BASE) 其中 LCD_BASE,必须根据我们外部电路的连接来确定,我们使用 Bank1.sector4 就是从 地址 0X6C000000 开始,而 0X000007FE,则是 A10 的偏移量。我们将这个地址强制转换为 LCD_TypeDef 结构体地址,那么可以得到 LCD->LCD_REG 的地址就是 0X6C00,07FE,对应 A10 的状态为 0(即 RS=0),而 LCD-> LCD_RAM 的地址就是 0X6C00,0800(结构体地址自增), 281 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 对应 A10 的状态为 1(即 RS=1)。 所以,有了这个定义,当我们要往 LCD 写命令/数据的时候,可以这样写: LCD->LCD_REG=CMD; //写命令 LCD->LCD_RAM=DATA; //写数据 而读的时候反过来操作就可以了,如下所示: CMD= LCD->LCD_REG; //读 LCD 寄存器 DATA = LCD->LCD_RAM; //读 LCD 数据 这其中,CS、WR、RD 和 IO 口方向都是由 FSMC 控制,不需要我们手动设置了。接下来, 我们先介绍一下 lcd.h 里面的另一个重要结构体: //LCD 重要参数集 typedef struct { u16 width; u16 height; //LCD 宽度 //LCD 高度 u16 id; u8 dir; u16 wramcmd; u16 setxcmd; u16 setycmd; //LCD ID //横屏还是竖屏控制:0,竖屏;1,横屏。 //开始写 gram 指令 //设置 x 坐标指令 //设置 y 坐标指令 }_lcd_dev; //LCD 参数 extern _lcd_dev lcddev; //管理 LCD 重要参数 该结构体用于保存一些 LCD 重要参数信息,比如 LCD 的长宽、LCD ID(驱动 IC 型号)、 LCD 横竖屏状态等,这个结构体虽然占用了 10 个字节的内存,但是却可以让我们的驱动函数 支持不同尺寸的 LCD,同时可以实现 LCD 横竖屏切换等重要功能,所以还是利大于弊的。有 了以上了解,下面我们开始介绍 lcd.c 里面的一些重要函数。 先看 7 个简单,但是很重要的函数: //写寄存器函数 //regval:寄存器值 void LCD_WR_REG(u16 regval) { LCD->LCD_REG=regval; //写入要写的寄存器序号 } //写 LCD 数据 //data:要写入的值 void LCD_WR_DATA(u16 data) { LCD->LCD_RAM=data; } //读 LCD 数据 //返回值:读到的值 u16 LCD_RD_DATA(void) { 282 STM32F1 开发指南(库函数版) vu16 ram; //防止被优化 ALIENTEK 战舰 STM32F103 V3 开发板教程 ram=LCD->LCD_RAM; return ram; } //写寄存器 //LCD_Reg:寄存器地址 //LCD_RegValue:要写入的数据 void LCD_WriteReg(u16 LCD_Reg, u16 LCD_RegValue) { LCD->LCD_REG = LCD_Reg; //写入要写的寄存器序号 LCD->LCD_RAM = LCD_RegValue; //写入数据 } //读寄存器 //LCD_Reg:寄存器地址 //返回值:读到的数据 u16 LCD_ReadReg(u16 LCD_Reg) { LCD_WR_REG(LCD_Reg); //写入要读的寄存器序号 delay_us(5); return LCD_RD_DATA(); //返回读到的值 } //开始写 GRAM void LCD_WriteRAM_Prepare(void) { LCD->LCD_REG=lcddev.wramcmd; } //LCD 写 GRAM //RGB_Code:颜色值 void LCD_WriteRAM(u16 RGB_Code) { LCD->LCD_RAM = RGB_Code;//写十六位 GRAM } 因为 FSMC 自动控制了 WR/RD/CS 等这些信号,所以这 7 个函数实现起来都非常简单,我 们就不多说,注意,上面有几个函数,我们添加了一些对 MDK –O2 优化的支持,去掉的话, 在-O2 优化的时候会出问题。这些函数实现功能见函数前面的备注,通过这几个简单函数的组 合,我们就可以对 LCD 进行各种操作了。 第八个要介绍的函数是坐标设置函数,该函数代码如下: /设置光标位置 //Xpos:横坐标 //Ypos:纵坐标 void LCD_SetCursor(u16 Xpos, u16 Ypos) { 283 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 if(lcddev.id==0X9341||lcddev.id==0X5310) { LCD_WR_REG(lcddev.setxcmd); LCD_WR_DATA(Xpos>>8);LCD_WR_DATA(Xpos&0XFF); LCD_WR_REG(lcddev.setycmd); LCD_WR_DATA(Ypos>>8);LCD_WR_DATA(Ypos&0XFF); }else if(lcddev.id==0X6804) { if(lcddev.dir==1)Xpos=lcddev.width-1-Xpos;//横屏时处理 LCD_WR_REG(lcddev.setxcmd); LCD_WR_DATA(Xpos>>8);LCD_WR_DATA(Xpos&0XFF); LCD_WR_REG(lcddev.setycmd); LCD_WR_DATA(Ypos>>8);LCD_WR_DATA(Ypos&0XFF); }else if(lcddev.id==0X1963) { if(lcddev.dir==0)//x 坐标需要变换 { Xpos=lcddev.width-1-Xpos; LCD_WR_REG(lcddev.setxcmd); LCD_WR_DATA(0);LCD_WR_DATA(0); LCD_WR_DATA(Xpos>>8);LCD_WR_DATA(Xpos&0XFF); }else { LCD_WR_REG(lcddev.setxcmd); LCD_WR_DATA(Xpos>>8);LCD_WR_DATA(Xpos&0XFF); LCD_WR_DATA((lcddev.width-1)>>8); LCD_WR_DATA((lcddev.width-1)&0XFF); } LCD_WR_REG(lcddev.setycmd); LCD_WR_DATA(Ypos>>8);LCD_WR_DATA(Ypos&0XFF); LCD_WR_DATA((lcddev.height-1)>>8);LCD_WR_DATA((lcddev.height-1)&0XFF); }else if(lcddev.id==0X5510) { LCD_WR_REG(lcddev.setxcmd);LCD_WR_DATA(Xpos>>8); LCD_WR_REG(lcddev.setxcmd+1);LCD_WR_DATA(Xpos&0XFF); LCD_WR_REG(lcddev.setycmd);LCD_WR_DATA(Ypos>>8); LCD_WR_REG(lcddev.setycmd+1);LCD_WR_DATA(Ypos&0XFF); }else { if(lcddev.dir==1)Xpos=lcddev.width-1-Xpos;//横屏其实就是调转 x,y 坐标 LCD_WriteReg(lcddev.setxcmd, Xpos); LCD_WriteReg(lcddev.setycmd, Ypos); } 284 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 } 该函数非常重要,该函数实现了将 LCD 的当前操作点设置到指定坐标(x,y),有了该函数, 我们就可以在液晶上任意作图了。这里面的 lcddev.setxcmd、lcddev.setycmd、lcddev.width、 lcddev.height 等指令/参数都是在 LCD_Display_Dir 函数里面初始化的,该函数根据 lcddev.id 的 不同,执行不同的设置,由于篇幅所限,这里就不列出来介绍了,请大家参考本例程源码。另 外,因为 9341/5310/6804/1963/5510 等的设置同其他屏有些不太一样,所以进行了区别对待。 接下来我们介绍第九个函数:画点函数。该函数实现代码如下: //画点 //x,y:坐标 //POINT_COLOR:此点的颜色 void LCD_DrawPoint(u16 x,u16 y) { LCD_SetCursor(x,y); LCD_WriteRAM_Prepare(); //设置光标位置 //开始写入 GRAM LCD->LCD_RAM=POINT_COLOR; } 该函数实现比较简单,就是先设置坐标,然后往坐标写颜色。其中 POINT_COLOR 是我们 定义的一个全局变量,用于存放画笔颜色,顺带介绍一下另外一个全局变量:BACK_COLOR, 该变量代表 LCD 的背景色。LCD_DrawPoint 函数虽然简单,但是至关重要,其他几乎所有上 层函数,都是通过调用这个函数实现的。 有了画点,当然还需要有读点的函数,第九个介绍的函数就是读点函数,用于读取 LCD 的 GRAM,这里说明一下,为什么 OLED 模块没做读 GRAM 的函数,而这里做了。因为 OLED 模块是单色的,所需要全部 GRAM 也就 1K 个字节,而 TFTLCD 模块为彩色的,点数也比 OLED 模块多很多,以 16 位色计算,一款 320×240 的液晶,需要 320×240×2 个字节来存储颜色值, 也就是也需要 150K 字节,这对任何一款单片机来说,都不是一个小数目了。而且我们在图形 叠加的时候,可以先读回原来的值,然后写入新的值,在完成叠加后,我们又恢复原来的值。 这样在做一些简单菜单的时候,是很有用的。这里我们读取 TFTLCD 模块数据的函数为 LCD_ReadPoint,该函数直接返回读到的 GRAM 值。该函数使用之前要先设置读取的 GRAM 地址,通过 LCD_SetCursor 函数来实现。LCD_ReadPoint 的代码如下: //读取个某点的颜色值 //x,y:坐标 //返回值:此点的颜色 u16 LCD_ReadPoint(u16 x,u16 y) { vu16 r=0,g=0,b=0; if(x>=lcddev.width||y>=lcddev.height)return 0; //超过了范围,直接返回 LCD_SetCursor(x,y); if(lcddev.id==0X9341||lcddev.id==0X6804||lcddev.id==0X5310||lcddev.id==0X1963) LCD_WR_REG(0X2E);//9341/6804/3510/1963 发送读 GRAM 指令 else if(lcddev.id==0X5510)LCD_WR_REG(0X2E00);//5510 发送读 GRAM 指令 else LCD_WR_REG(0X22); //其他 IC 发送读 GRAM 指令 if(lcddev.id==0X9320)opt_delay(2); //FOR 9320,延时 2us r=LCD_RD_DATA(); //dummy Read 285 STM32F1 开发指南(库函数版) if(lcddev.id==0X1963)return r; ALIENTEK 战舰 STM32F103 V3 开发板教程 //1963 直接读就可以 opt_delay(2); r=LCD_RD_DATA(); //实际坐标颜色 if(lcddev.id==0X9341||lcddev.id==0X5310||lcddev.id==0X5510)//这些 LCD 要分 2 次读出 { opt_delay(2); b=LCD_RD_DATA(); g=r&0XFF;//对于 9341/5310/5510,第一次读取的是 RG 值,R 在前,G 在后,各占 8 位 g<<=8; } if(lcddev.id==0X9325||lcddev.id==0X4535||lcddev.id==0X4531||lcddev.id==0XB505|| lcddev.id==0XC505)return r; //这几种 IC 直接返回颜色值 else if(lcddev.id==0X9341||lcddev.id==0X5310||lcddev.id==0X5510)return (((r>>11)<<11)| ((g>>10)<<5)|(b>>11)); //ILI9341/NT35310/NT35510 需要公式转换一下 else return LCD_BGR2RGB(r); //其他 IC } 在 LCD_ReadPoint 函数中,因为我们的代码不止支持一种 LCD 驱动器,所以,我们根据 不同的 LCD 驱动器((lcddev.id)型号,执行不同的操作,以实现对各个驱动器兼容,提高函数 的通用性。 第十一个要介绍的是字符显示函数 LCD_ShowChar,该函数同前面 OLED 模块的字符显示 函数差不多,但是这里的字符显示函数多了 1 个功能,就是可以以叠加方式显示,或者以非叠 加方式显示。叠加方式显示多用于在显示的图片上再显示字符。非叠加方式一般用于普通的显 示。该函数实现代码如下: //在指定位置显示一个字符 //x,y:起始坐标 //num:要显示的字符:" "--->"~" //size:字体大小 12/16/24 //mode:叠加方式(1)还是非叠加方式(0) void LCD_ShowChar(u16 x,u16 y,u8 num,u8 size,u8 mode) { u8 temp,t1,t; u16 y0=y; u8 csize=(size/8+((size%8)?1:0))*(size/2);//得到字体一个字符对应点阵集所占的字节数 num=num-' '; //ASCII 字库从空格开始取模,所以-' '即可得到对应字符的字库(点阵) for(t=0;t=lcddev.height)return; //超区域了 if((y-y0)==size) { y=y0; x++; if(x>=lcddev.width)return; //超区域了 break; } } } } 在 LCD_ShowChar 函数里面,我们采用快速画点函数 LCD_Fast_DrawPoint 来画点显示字 符,该函数同 LCD_DrawPoint 一样,只是带了颜色参数,且减少了函数调用的时间,详见本例 程源码。该代码中我们用到了三个字符集点阵数据数组 asc2_2412、asc2_1206 和 asc2_1608, 这几个字符集的点阵数据的提取方式,同十七章介绍的提取方法是一模一样的。详细请参考第 十七章。 最后,我们再介绍一下 TFTLCD 模块的初始化函数 LCD_Init,该函数先初始化 STM32 与 TFTLCD 连接的 IO 口,并配置 FSMC 控制器,然后读取 LCD 控制器的型号,根据控制 IC 的 型号执行不同的初始化代码,其简化代码如下: //初始化 lcd //该初始化函数可以初始化各种 ILI93XX 液晶,但是其他函数是基于 ILI9320 的!!! //在其他型号的驱动芯片上没有测试! void LCD_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; FSMC_NORSRAMInitTypeDef FSMC_NSInitStructure; FSMC_NORSRAMTimingInitTypeDef readWriteTiming; FSMC_NORSRAMTimingInitTypeDef writeTiming; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_FSMC,ENABLE);//使能 FSMC 时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOD| RCC_APB2Periph_GPIOE|RCC_APB2Periph_GPIOG| RCC_APB2Periph_AFIO,ENABLE); // ①使能 GPIO 以及 AFIO 复用功能时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); //PB0 推挽输出 背光 //推挽输出 //②初始化 PB0 //PORTD 复用推挽输出 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; 287 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOD, &GPIO_InitStructure); //②初始化 PORTD //PORTE 复用推挽输出 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_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOE, &GPIO_InitStructure); //②初始化 PORTE //PORTG12 复用推挽输出 A0 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_12; //PORTD 复用推挽输出 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOG, &GPIO_InitStructure); //②初始化 PORTG readWriteTiming.FSMC_AddressSetupTime = 0x01; //地址建立时间 2 个 HCLK 1 readWriteTiming.FSMC_AddressHoldTime = 0x00; //地址保持时间模式 A 未用到 readWriteTiming.FSMC_DataSetupTime = 0x0f; // 数据保存时间为 16 个 HCLK readWriteTiming.FSMC_BusTurnAroundDuration = 0x00; readWriteTiming.FSMC_CLKDivision = 0x00; readWriteTiming.FSMC_DataLatency = 0x00; readWriteTiming.FSMC_AccessMode = FSMC_AccessMode_A; //模式 A writeTiming.FSMC_AddressSetupTime = 0x00; //地址建立时间为 1 个 HCLK writeTiming.FSMC_AddressHoldTime = 0x00; //地址保持时间(A writeTiming.FSMC_DataSetupTime = 0x03; //数据保存时间为 4 个 HCLK writeTiming.FSMC_BusTurnAroundDuration = 0x00; writeTiming.FSMC_CLKDivision = 0x00; writeTiming.FSMC_DataLatency = 0x00; writeTiming.FSMC_AccessMode = FSMC_AccessMode_A; //模式 A FSMC_NSInitStructure.FSMC_Bank = FSMC_Bank1_NORSRAM4;//这里我们使 //用 NE4,也就对应 BTCR[6],[7]。 FSMC_NSInitStructure.FSMC_DataAddressMux = FSMC_DataAddressMux_Disable; //不复用数据地址 FSMC_NSInitStructure.FSMC_MemoryType=FSMC_MemoryType_SRAM;// SRAM FSMC_NSInitStructure.FSMC_MemoryDataWidth= FSMC_MemoryDataWidth_16b; //存储器数据宽度为 16bit FSMC_NSInitStructure.FSMC_BurstAccessMode=FSMC_BurstAccessMode_Disable; FSMC_NSInitStructure.FSMC_WaitSignalPolarity = FSMC_WaitSignalPolarity_Low; FSMC_NSInitStructure.FSMC_AsynchronousWait= 288 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 FSMC_AsynchronousWait_Disable; FSMC_NSInitStructure.FSMC_WrapMode = FSMC_WrapMode_Disable; FSMC_NSInitStructure.FSMC_WaitSignalActive= FSMC_WaitSignalActive_BeforeWaitState; FSMC_NSInitStructure.FSMC_WriteOperation = FSMC_WriteOperation_Enable; //存储器写使能 FSMC_NSInitStructure.FSMC_WaitSignal = FSMC_WaitSignal_Disable; FSMC_NSInitStructure.FSMC_ExtendedMode = FSMC_ExtendedMode_Enable; // 读写使用不同的时序 FSMC_NSInitStructure.FSMC_WriteBurst = FSMC_WriteBurst_Disable; FSMC_NSInitStructure.FSMC_ReadWriteTimingStruct = &readWriteTiming; FSMC_NSInitStructure.FSMC_WriteTimingStruct = &writeTiming; //写时序 FSMC_NORSRAMInit(&FSMC_NSInitStructure); //③初始化 FSMC 配置 FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM4, ENABLE); // ④使能 BANK1 delay_ms(50); // delay 50 ms lcddev.id=LCD_ReadReg(0x0000); //读 ID(9320/9325/9328/4531/4535 等 IC) if(lcddev.id<0XFF||lcddev.id==0XFFFF||lcddev.id==0X9300) //ID 不正确,新增 0X9300 判断,因为 9341 在未被复位的情况下会被读成 9300 { //尝试 9341 ID 的读取 LCD_WR_REG(0XD3); lcddev.id=LCD_RD_DATA(); lcddev.id=LCD_RD_DATA(); lcddev.id=LCD_RD_DATA(); //dummy read //读到 0X00 //读取 93 lcddev.id<<=8; lcddev.id|=LCD_RD_DATA(); if(lcddev.id!=0X9341) //读取 41 //非 9341,尝试是不是 6804 { LCD_WR_REG(0XBF); lcddev.id=LCD_RD_DATA();//dummy read lcddev.id=LCD_RD_DATA();//读回 0X01 lcddev.id=LCD_RD_DATA();//读回 0XD0 lcddev.id=LCD_RD_DATA();//这里读回 0X68 lcddev.id<<=8; lcddev.id|=LCD_RD_DATA();//这里读回 0X04 if(lcddev.id!=0X6804) //也不是 6804,尝试看看是不是 NT35310 { LCD_WR_REG(0XD4); lcddev.id=LCD_RD_DATA(); lcddev.id=LCD_RD_DATA(); lcddev.id=LCD_RD_DATA(); //dummy read //读回 0X01 //读回 0X53 lcddev.id<<=8; 289 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 lcddev.id|=LCD_RD_DATA(); //这里读回 0X10 if(lcddev.id!=0X5310) //也不是 NT35310,尝试看看是不是 NT35510 { LCD_WR_REG(0XDA00); lcddev.id=LCD_RD_DATA(); //读回 0X00 LCD_WR_REG(0XDB00); lcddev.id=LCD_RD_DATA(); //读回 0X80 lcddev.id<<=8; LCD_WR_REG(0XDC00); lcddev.id|=LCD_RD_DATA(); //读回 0X00 if(lcddev.id==0x8000)lcddev.id=0x5510; //NT35510 读回的 ID 是 8000H,为方便区分,我们强制设置为 5510 if(lcddev.id!=0X5510)//也不是 NT5510,尝试看看是不是 SSD1963 { LCD_WR_REG(0XA1); lcddev.id=LCD_RD_DATA(); lcddev.id=LCD_RD_DATA(); //读回 0X57 lcddev.id<<=8; lcddev.id|=LCD_RD_DATA(); //读回 0X61 if(lcddev.id==0X5761)lcddev.id=0X1963;//SSD1963 读回的 ID 是 //5761H,为方便区分,我们强制设置为 1963 } } } } } printf(" LCD ID:%x\r\n",lcddev.id); //打印 LCD ID if(lcddev.id==0X9341) //9341 初始化 { ……//9341 初始化代码 }else if(lcddev.id==0xXXXX) //其他 LCD 初始化代码 { ……//其他 LCD 驱动 IC,初始化代码 } LCD_Display_Dir(0); LCD_LED=1; //默认为竖屏显示 //点亮背光 LCD_Clear(WHITE); } 从初始化代码可以看出,LCD 初始化步骤为①~⑤在代码中标注: ①GPIO,FSMC,AFIO 时钟使能。 ②GPIO 初始化:GPIO_Init()函数。 ③FSMC 初始化:FSMC_NORSRAMInit()函数。 290 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 ④ FSMC 使能:FSMC_NORSRAMCmd()函数。 ⑤ 同的 LCD 驱动器的初始化代码。 该函数先对 FSMC 相关 IO 进行初始化,然后是 FSMC 的初始化,这个我们在前面都有介 绍,最后根据读到的 LCD ID,对不同的驱动器执行不同的初始化代码,从上面的代码可以看 出,这个初始化函数可以针对十多款不同的驱动 IC 执行初始化操作,这样大大提高了整个程序 的通用性。大家在以后的学习中应该多使用这样的方式,以提高程序的通用性、兼容性。 特别注意:本函数使用了 printf 来打印 LCD ID,所以,如果你在主函数里面没有初始化串 口,那么将导致程序死在 printf 里面!!如果不想用 printf,那么请注释掉它。 大家可以打开 lcd.h 和 lcd.c 文件看里面的代码,了解各个函数功能。接下来,我们打开 main.c 内容如下: int main(void) { u8 x=0; u8 lcd_id[12]; //存放 LCD ID 字符串 delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); uart_init(115200); //串口初始化波特率为 115200 LED_Init(); //LED 端口初始化 //设置 NVIC 中断分组 2 LCD_Init(); POINT_COLOR=RED; sprintf((char*)lcd_id,"LCD ID:%04X",lcddev.id);//将 LCD ID 打印到 lcd_id 数组。 while(1) { switch(x) { case 0:LCD_Clear(WHITE);break; case 1:LCD_Clear(BLACK);break; case 2:LCD_Clear(BLUE);break; case 3:LCD_Clear(RED);break; case 4:LCD_Clear(MAGENTA);break; case 5:LCD_Clear(GREEN);break; case 6:LCD_Clear(CYAN);break; case 7:LCD_Clear(YELLOW);break; case 8:LCD_Clear(BRRED);break; case 9:LCD_Clear(GRAY);break; case 10:LCD_Clear(LGRAY);break; case 11:LCD_Clear(BROWN);break; } POINT_COLOR=RED; LCD_ShowString(30,40,210,24,24,"WarShip STM32 ^_^"); LCD_ShowString(30,70,200,16,16,"TFTLCD TEST"); LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(30,110,200,16,16,lcd_id); //显示 LCD ID 291 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 LCD_ShowString(30,130,200,12,12,"2014/5/4"); x++; if(x==12)x=0; LED0=!LED0; delay_ms(1000); } } 该部分代码将显示一些固定的字符,字体大小包括 24*12、16*8 和 12*6 等三种,同时显示 LCD 驱动 IC 的型号,然后不停的切换背景颜色,每 1s 切换一次。而 LED0 也会不停的闪烁, 指示程序已经在运行了。其中我们用到一个 sprintf 的函数,该函数用法同 printf,只是 sprintf 把打印内容输出到指定的内存区间上,sprintf 的详细用法,请百度。 在编译通过之后,我们开始下载验证代码。 18.4 下载验证 将程序下载到战舰 STM32 后,可以看到 DS0 不停的闪烁,提示程序已经在运行了。同时 可以看到 TFTLCD 模块的显示如图 18.4.1 所示: 图 18.4.1 TFTLCD 显示效果图 我们可以看到屏幕的背景是不停切换的,同时 DS0 不停的闪烁,证明我们的代码被正确的 执行了,达到了我们预期的目的,实现了 TFTLCD 的驱动,以及字符的显示。另外,本例程除 了不支持 CPLD 方案的 7 寸屏模块,其余所有的 ALIENTEK TFTLCD 模块都可以支持,直接 插上去即可使用。 292 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第十九章 USMART 调试组件实验 本章,我们将向大家介绍一个十分重要的辅助调试工具:USMART 调试组件。该组件由 ALIENTEK 开发提供,功能类似 linux 的 shell(RTT 的 finsh 也属于此类)。USMART 最主要 的功能就是通过串口调用单片机里面的函数,并执行,对我们调试代码是很有帮助的。本章分 为如下几个部分: 19.1 USMART 调试组件简介 19.2 硬件设计 19.3 软件设计 19.4 下载验证 293 19.1 USMART 调试组件简介 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 USMART 是由 ALIENTEK 开发的一个灵巧的串口调试互交组件,通过它你可以通过串口 助手调用程序里面的任何函数,并执行。因此,你可以随意更改函数的输入参数(支持数字(10/16 进制)、字符串、函数入口地址等作为参数),单个函数最多支持 10 个输入参数,并支持函数返 回值显示,目前最新版本为 V3.1。 USMART 的特点如下: 1, 可以调用绝大部分用户直接编写的函数。 2, 资源占用极少(最少情况:FLASH:4K;SRAM:72B)。 3, 支持参数类型多(数字(包含 10/16 进制)、字符串、函数指针等)。 4, 支持函数返回值显示。 5, 支持参数及返回值格式设置。 6, 支持函数执行时间计算(V3.1 版本新特性)。 7, 使用方便。 有了 USMART,你可以轻易的修改函数参数、查看函数运行结果,从而快速解决问题。比 如你调试一个摄像头模块,需要修改其中的几个参数来得到最佳的效果,普通的做法:写函数 ->修改参数->下载->看结果->不满意->修改参数->下载->看结果->不满意….不停的循环,直到满 意为止。这样做很麻烦不说,单片机也是有寿命的啊,老这样不停的刷,很折寿的。而利用 USMART,则只需要在串口调试助手里面输入函数及参数,然后直接串口发送给单片机,就执 行了一次参数调整,不满意的话,你在串口调试助手修改参数在发送就可以了,直到你满意为 止。这样,修改参数十分方便,不需要编译、不需要下载、不会让单片机折寿。 USMART 支持的参数类型基本满足任何调试了,支持的类型有:10 或者 16 进制数字、字 符串指针(如果该参数是用作参数返回的话,可能会有问题!)、函数指针等。因此绝大部分函 数,可以直接被 USMART 调用,对于不能直接调用的,你只需要重写一个函数,把影响调用 的参数去掉即可,这个重写后的函数,即可以被 USMART 调用了。 USMART 的实现流程简单概括就是:第一步,添加需要调用的函数(在 usmart_config.c 里 面的 usmart_nametab 数组里面添加);第二步,初始化串口;第三步,初始化 USMART(通过 usmart_init 函数实现);第四步,轮询 usmart_scan 函数,处理串口数据。 经过以上简单介绍,我们对 USMART 有了个大概了解,接下来我们来简单介绍下 USMART 组件的移植。 USMART 组件总共包含 6 文件如图 19.1.1 所示: 图 19.1.1 USMART 组件代码 294 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 其中 redeme.txt 是一个说明文件,不参与编译。其他五个文件,usmart.c 负责与外部互交等。 usmat_str.c 主要负责命令和参数解析。usmart_config.c 主要由用户添加需要由 usmart 管理的函 数。 usmart.h 和 usmart_str.h 是两个头文件,其中 usmart.h 里面含有几个用户配置宏定义,可以 用来配置 usmart 的功能及总参数长度(直接和 SRAM 占用挂钩)、是否使能定时器扫描、是否使 用读写函数等。 USMART 的移植,只需要实现 5 个函数。其中 4 个函数都在 usmart.c 里面,另外一个是串 口接收函数,必须有由用户自己实现,用于接收串口发送过来的数据。 第一个函数,串口接收函数。该函数,我们是通过 SYSTEM 文件夹默认的串口接收来实现 的,该函数在 5.3.1 节有介绍过,我们这里就不列出来了。SYSTEM 文件夹里面的串口接收函 数,最大可以一次接收 200 字节,用于从串口接收函数名和参数等。大家如果在其他平台移植, 请参考 SYSTEM 文件夹串口接收的实现方式进行移植。 第二个是 void usmart_init(void)函数,该函数的实现代码如下: //初始化串口控制器 //sysclk:系统时钟(Mhz) void usmart_init(u8 sysclk) { #if USMART_ENTIMX_SCAN==1 Timer4_Init(1000,(u32)sysclk*100-1); //分频,时钟为 10K ,100ms 中断一次 //注意,计数频率必须为 10Khz,以和 runtime 单位(0.1ms)同步. #endif usmart_dev.sptype=1; //十六进制显示参数 } 该函数有一个参数 sysclk,就是用于定时器初始化。这里需要说明一下,为了让我们的库 函数和寄存器实现函数一致,我们这里不直接通过 SystemCoreClock 来获取系统时钟,直接通 过在外面设置的方式(当然你也可以去掉 sysclk 这个参数),这样函数体里面的 Timer4_Init 函 数 就 可 修 改 为 Timer4_Init(1000,(u32) SystemCoreClock/10000-1) 。 另 外 USMART_ENTIMX_SCAN 是在 usmart.h 里面定义的一个是否使能定时器中断扫描的宏定义。 如果为 1,就通过定时器初始化函数 Timer4_Init 初始化定时器 4 中断,每 100ms 中断一次,并 在中断服务程序 TIM4_IRQHandler 里面调用 usmart_scan 函数进行扫描,这里我们就不列出代 码,因为之前的实验对这方面讲解较多。如果为 0,那么需要用户需要自行间隔一定时间(100ms 左右为宜)调用一次 usmart_scan 函数,以实现串口数据处理。注意:如果要使用函数执行时 间统计功能(runtime 1),则必须设置 USMART_ENTIMX_SCAN 为 1。另外,为了让统计时 间精确到 0.1ms,定时器的计数时钟频率必须设置为 10Khz,否则时间就不是 0.1ms 了。 第三和第四个函数仅用于服务 USMART 的函数执行时间统计功能(串口指令:runtime 1), 分别是:usmart_reset_runtime 和 usmart_get_runtime,这两个函数代码如下: //复位 runtime //需要根据所移植到的 MCU 的定时器参数进行修改 void usmart_reset_runtime(void) { TIM_ClearFlag(TIM4,TIM_FLAG_Update);//清除中断标志位 TIM_SetAutoreload(TIM4,0XFFFF);//将重装载值设置到最大 TIM_SetCounter(TIM4,0); //清空定时器的 CNT 295 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 usmart_dev.runtime=0; } //获得 runtime 时间 //返回值:执行时间,单位:0.1ms,最大延时时间为定时器 CNT 值的 2 倍*0.1ms //需要根据所移植到的 MCU 的定时器参数进行修改 u32 usmart_get_runtime(void) { if(TIM_GetFlagStatus(TIM4,TIM_FLAG_Update)==SET)//在运行期间产生定时器溢出 { usmart_dev.runtime+=0XFFFF; } usmart_dev.runtime+=TIM_GetCounter(TIM4); return usmart_dev.runtime; //返回计数值} 这里我们还是利用定时器 4 来做执行时间计算,usmart_reset_runtime 函数在每次 USMART 调用函数之前执行,清除计数器,然后在函数执行完之后,调用 usmart_get_runtime 获取整个 函数的运行时间。由于 usmart 调用的函数,都是在中断里面执行的,所以我们不太方便再用定 时器的中断功能来实现定时器溢出统计,因此,USMART 的函数执行时间统计功能,最多可以 统计定时器溢出 1 次的时间,对 STM32 来说,定时器是 16 位的,最大计数是 65535,而由于 我们定时器设置的是 0.1ms 一个计时周期(10Khz),所以最长计时时间是:65535*2*0.1ms=13.1 秒。也就是说,如果函数执行时间超过 13.1 秒,那么计时将不准确。 最后一个是 usmart_scan 函数,该函数用于执行 usmart 扫描,该函数需要得到两个参量, 第一个是从串口接收到的数组(USART_RX_BUF),第二个是串口接收状态(USART_RX_STA)。 接收状态包括接收到的数组大小,以及接收是否完成。该函数代码如下: //usmart 扫描函数 //通过调用该函数,实现 usmart 的各个控制.该函数需要每隔一定时间被调用一次 //以及时执行从串口发过来的各个函数. //本函数可以在中断里面调用,从而实现自动管理. //如果非 ALIENTEK 用户,则 USART_RX_STA 和 USART_RX_BUF[]需要用户自己实现 void usmart_scan(void) { u8 sta,len; if(USART_RX_STA&0x8000)//串口接收完成? { len=USART_RX_STA&0x3fff; //得到此次接收到的数据长度 USART_RX_BUF[len]='\0'; //在末尾加入结束符. sta=usmart_dev.cmd_rec(USART_RX_BUF);//得到函数各个信息 if(sta==0)usmart_dev.exe();//执行函数 else { len=usmart_sys_cmd_exe(USART_RX_BUF); if(len!=USMART_FUNCERR)sta=len; if(sta) { switch(sta) 296 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 { case USMART_FUNCERR: printf("函数错误!\r\n"); break; case USMART_PARMERR: printf("参数错误!\r\n"); break; case USMART_PARMOVER: printf("参数太多!\r\n"); break; case USMART_NOFUNCFIND: printf("未找到匹配的函数!\r\n"); break; } } } USART_RX_STA=0;//状态寄存器清空 } } 该函数的执行过程:先判断串口接收是否完成(USART_RX_STA 的最高位是否为 1),如 果完成,则取得串口接收到的数据长度(USART_RX_STA 的低 14 位),并在末尾增加结束符, 再执行解析,解析完之后清空接收标记(USART_RX_STA 置零)。如果没执行完成,则直接跳 过,不进行任何处理。 完成这几个函数的移植,你就可以使用 USMART 了。不过,需要注意的是,usmart 同外 部的互交,一般是通过 usmart_dev 结构体实现,所以 usmart_init 和 usmart_scan 的调用分别是 通过:usmart_dev.init 和 usmart_dev.scan 实现的。 下面,我们将在第十八章实验的基础上,移植 USMART,并通过 USMART 调用一些 TFTLCD 的内部函数,让大家初步了解 USMART 的使用。 19.2 硬件设计 本实验用到的硬件资源有: 1) 指示灯 DS0 和 DS1 2) 串口 3) TFTLCD 模块 这三个硬件在前面章节均有介绍,本章不再介绍。 19.3 软件设计 这里我们在上一章的实验的基础上通过添加文件的方式讲解 USMART 的引入,当然大家 也可以直接打开我们光盘的实例工程。打开上一章的工程,复制 USMART 文件夹(该文件夹 可以在光盘的本章实验例程里面找到)到本工程文件夹下面,如图 19.3.1 所示: 297 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 19.3.1 复制 USMART 文件夹到工程文件夹下 接着,我们打开工程,并新建 USMART 组,添加 USMART 组件代码,同时把 USMART 文件夹添加到头文件包含路径,在主函数里面加入 include“usmart.h”如图 19.3.2 所示: 图 19.3.2 添加 USMART 组件代码 由于 USMART 默认提供了 STM32 的 TIM4 中断初始化设置代码,我们只需要在 usmart.h 里面设置 USMART_ENTIMX_SCAN 为 1,即可完成 TIM4 的设置,通过 TIM4 的中断服务函 数,调用 usmart_dev.scan()(就是 usmart_scan 函数),实现 usmart 的扫描。此部分代码我们就 不列出来了,请参考 usmart.c。 298 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 此时,我们就可以使用 USMART 了,不过在主程序里面还得执行 usmart 的初始化,另外 还需要针对你自己想要被 USMART 调用的函数在 usmart_config.c 里面进行添加。下面先介绍 如何添加自己想要被 USMART 调用的函数,打开 usmart_config.c,如图 19.3.3 所示: 图 19.3.3 添加需要被 USMART 调用的函数 这里的添加函数很简单,只要把函数所在头文件添加进来,并把函数名按上图所示的方式 增加即可,默认我们添加了两个函数:delay_ms 和 delay_us。另外,read_addr 和 write_addr 属 于 usmart 自带的函数,用于读写指定地址的数据,通过配置 USMART_USE_WRFUNS,可以 使能或者禁止这两个函数。 这里我们根据自己的需要按上图的格式添加其他函数,添加完之后如图 19.3.4 所示: 299 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 19.3.4 添加函数后 上图中,我们添加了 lcd.h,并添加了很多 LCD 函数,最后我们还添加了 led_set 和 test_fun 两个函数,这两个函数在 main.c 里面实现,代码如下: //LED 状态设置函数 void led_set(u8 sta) { LED1=sta; } //函数参数调用测试函数 void test_fun(void(*ledset)(u8),u8 sta) { ledset(sta); } led_set 函数,用于设置 LED1 的状态,而第二个函数 test_fun 则是测试 USMART 对函数参 数的支持的,test_fun 的第一个参数是函数,在 USMART 里面也是可以被调用的。 在添加完函数之后,我们修改 main 函数,如下: int main(void) { delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置 NVIC 中断分组 2 300 STM32F1 开发指南(库函数版) uart_init(115200); LED_Init(); ALIENTEK 战舰 STM32F103 V3 开发板教程 //串口初始化波特率为 115200 //LED 端口初始化 LCD_Init(); usmart_dev.init(SystemCoreClock/1000000); //初始化 USMART POINT_COLOR=RED; LCD_ShowString(30,50,200,16,16,"WarShip STM32 ^_^"); LCD_ShowString(30,70,200,16,16,"USMART TEST"); LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(30,110,200,16,16,"2015/1/14"); while(1) { LED0=!LED0; delay_ms(500); } } 此代码显示简单的信息后,就是在死循环等待串口数据。至此,整个 usmart 的移植就完成 了。编译成功后,就可以下载程序到开发板,开始 USMART 的体验。 19.4 下载验证 将程序下载到战舰 STM32 后,可以看到 DS0 不停的闪烁,提示程序已经在运行了。同时, 屏幕上显示了一些字符(就是主函数里面要显示的字符)。 我们打开串口调试助手 XCOM,选择正确的串口号多条发送勾选发送新行(即发送回 车键)选项,然后发送 list 指令,即可打印所有 usmart 可调用函数。如下图所示: 301 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 19.4.1 驱动串口调试助手 上图中 list、id、?、help、hex、dec 和 runtime 都属于 usmart 自带的系统命令。下面我们 简单介绍下这几个命令: 上图中 list、id、help、hex、dec 和 runtime 都属于 usmart 自带的系统命令,点击后方的数 字按钮,即可发送对应的指令。下面我们简单介绍下这几个命令: list,该命令用于打印所有 usmart 可调用函数。发送该命令后,串口将受到所有能被 usmart 调用得到函数,如图 19.4.1 所示。 id,该指令用于获取各个函数的入口地址。比如前面写的 test_fun 函数,就有一个函数参数, 我们需要先通过 id 指令,获取 led_set 函数的 id(即入口地址),然后将这个 id 作为函数参数, 传递给 test_fun。 help(或者‘ ?’也可以),发送该指令后,串口将打印 usmart 使用的帮助信息。 hex 和 dec,这两个指令可以带参数,也可以不带参数。当不带参数的时候,hex 和 dec 分 别用于设置串口显示数据格式为 16 进制/10 进制。当带参数的时候,hex 和 dec 就执行进制转 换,比如输入:hex 1234,串口将打印:HEX:0X4D2,也就是将 1234 转换为 16 进制打印出来。 又比如输入:dec 0X1234,串口将打印:DEC:4660,就是将 0X1234 转换为 10 进制打印出来。 runtime 指令,用于函数执行时间统计功能的开启和关闭,发送:runtime 1,可以开启函数 执行时间统计功能;发送:runtime 0,可以关闭函数执行时间统计功能。函数执行时间统计功 能,默认是关闭的。 大家可以亲自体验下这几个系统指令,不过要注意,所有的指令都是大小写敏感的,不要 写错哦。 接下来,我们将介绍如何调用 list 所打印的这些函数,先来看一个简单的 delay_ms 的调用, 我们分别输入 delay_ms(1000)和 delay_ms(0x3E8),如图 19.4.2 所示: 图 19.4.2 串口调用 delay_ms 函数 302 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 从上图可以看出,delay_ms(1000)和 delay_ms(0x3E8)的调用结果是一样的,都是延时 1000ms,因为 usmart 默认设置的是 hex 显示,所以看到串口打印的参数都是 16 进制格式的, 大家可以通过发送 dec 指令切换为十进制显示。另外,由于 USMART 对调用函数的参数大小写 不敏感,所以参数写成:0X3E8 或者 0x3e8 都是正确的。另外,发送:runtime 1,开启运行时 间统计功能,从测试结果看,USMART 的函数运行时间统计功能,是相当准确的。 我们再看另外一个函数,LCD_ShowString 函数,该函数用于显示字符串,我们通过串口输 入:LCD_ShowString(20,200,200,100,16,"This is a test for usmart!!"),如图 19.4.3 所示: 图 19.4.3 串口调用 LCD_ShowString 函数 该函数用于在指定区域,显示指定字符串,发送给开发板后,我们可以看到 LCD 在我们指 定的地方显示了:This is a test for usmart!! 这个字符串。 其他函数的调用,也都是一样的方法,这里我们就不多介绍了,最后说一下带有函数参数 的函数的调用。我们将 led_set 函数作为 test_fun 的参数,通过在 test_fun 里面调用 led_set 函数, 实现对 DS1(LED1)的控制。前面说过,我们要调用带有函数参数的函数,就必须先得到函数参 数的入口地址(id),通过输入 id 指令,我们可以得到 led_set 的函数入口地址是:0X0800022D (注意:这个地址要以实际串口输出结果为准),所以,我们在串口输入:test_fun(0X0800022D,0), 就可以控制 DS1 亮了。如图 19.4.4 所示: 303 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 19.4.4 串口调用 test_fun 函数 在开发板上,我们可以看到,收到串口发送的 test_fun(0X080052C9,0)后,开发板的 DS1 亮了,然后大家可以通过发送 test_fun(0X080052C9,1),来关闭 DS1。说明我们成功的通过 test_fun 函数调用 led_set,实现了对 DS1 的控制。也就验证了 USMART 对函数参数的支持。 USMART 调试组件的使用,就为大家介绍到这里。USMART 是一个非常不错的调试组件, 希望大家能学会使用,可以达到事半功倍的效果。 304 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第二十章 RTC 实时时钟实验 前面我们介绍了两款液晶模块,这一章我们将介绍 STM32F1 的内部实时时钟(RTC)。在 本章中,我们将利用 ALIENTEK 2.8 寸 TFTLCD 模块来显示日期和时间,实现一个简单的时钟。 另外,本章将顺带向大家介绍 BKP 的使用。本章分为如下几个部分: 20.1 STM32F1 RTC 时钟简介 20.2 硬件设计 20.3 软件设计 20.4 下载验证 305 20.1 STM32F1 RTC 时钟简介 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 STM32 的实时时钟(RTC)是一个独立的定时器。STM32 的 RTC 模块拥有一组连续计数 的计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当 前的时间和日期。 RTC 模块和时钟配置系统(RCC_BDCR 寄存器)是在后备区域,即在系统复位或从待机模式 唤醒后 RTC 的设置和时间维持不变。但是在系统复位后,会自动禁止访问后备寄存器和 RTC, 以防止对后备区域(BKP)的意外写操作。所以在要设置时间之前, 先要取消备份区域(BKP) 写保护。 RTC 的简化框图,如图 20.1.1 所示: 图 20.1.1 RTC 框图 RTC 由两个主要部分组成(参见图 20.1.1),第一部分(APB1 接口)用来和 APB1 总线相连。 此单元还包含一组 16 位寄存器,可通过 APB1 总线对其进行读写操作。APB1 接口由 APB1 总 线时钟驱动,用来与 APB1 总线连接。 另一部分(RTC 核心)由一组可编程计数器组成,分成两个主要模块。第一个模块是 RTC 的 预分频模块,它可编程产生 1 秒的 RTC 时间基准 TR_CLK。RTC 的预分频模块包含了一个 20 位的可编程分频器(RTC 预分频器)。如果在 RTC_CR 寄存器中设置了相应的允许位,则在每个 TR_CLK 周期中 RTC 产生一个中断(秒中断)。第二个模块是一个 32 位的可编程计数器,可被 初始化为当前的系统时间,一个 32 位的时钟计数器,按秒钟计算,可以记录 4294967296 秒, 约合 136 年左右,作为一般应用,这已经是足够了的。 RTC 还有一个闹钟寄存器 RTC_ALR,用于产生闹钟。系统时间按 TR_CLK 周期累加并与 306 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 存储在 RTC_ALR 寄存器中的可编程时间相比较,如果 RTC_CR 控制寄存器中设置了相应允许 位,比较匹配时将产生一个闹钟中断。 RTC 内核完全独立于 RTC APB1 接口,而软件是通过 APB1 接口访问 RTC 的预分频值、计 数器值和闹钟值的。但是相关可读寄存器只在 RTC APB1 时钟进行重新同步的 RTC 时钟的上升 沿被更新,RTC 标志也是如此。这就意味着,如果 APB1 接口刚刚被开启之后,在第一次的内 部寄存器更新之前,从 APB1 上读取的 RTC 寄存器值可能被破坏了(通常读到 0)。因此,若 在读取 RTC 寄存器曾经被禁止的 RTC APB1 接口,软件首先必须等待 RTC_CRL 寄存器的 RSF 位(寄存器同步标志位,bit3)被硬件置 1。 要理解 RTC 原理,我们必须先通过对寄存器的讲解,让大家有一个全面的了解。接下来, 我们介绍一下 RTC 相关的几个寄存器。首先要介绍的是 RTC 的控制寄存器,RTC 总共有 2 个 控制寄存器 RTC_CRH 和 RTC_CRL,两个都是 16 位的。RTC_CRH 的各位描如图 20.1.2 所示: 图 20.1.2 RTC_CRH 寄存器各位描述 该寄存器用来控制中断的,我们本章将要用到秒钟中断,所以在该寄存器必须设置最低位 为 1,以允许秒钟中断。我们再看看 RTC_CRL 寄存器。该寄存器各位描述如图 20.1.3 所示: 307 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 20.1.3 RTC_CRL 寄存器各位描述 本章我们用到的是该寄存器的 0、3~5 这几个位,第 0 位是秒钟标志位,我们在进入闹钟 中断的时候,通过判断这位来决定是不是发生了秒钟中断。然后必须通过软件将该位清零(写 0)。第 3 位为寄存器同步标志位,我们在修改控制寄存器 RTC_CRH/CRL 之前,必须先判断该 位,是否已经同步了,如果没有则等待同步,在没同步的情况下修改 RTC_CRH/CRL 的值是不 行的。第 4 位为配置标位,在软件修改 RTC_CNT/RTC_ALR/RTC_PRL 的值的时候,必须先软 件置位该位,以允许进入配置模式。第 5 位为 RTC 操作位,该位由硬件操作,软件只读。通过 该位可以判断上次对 RTC 寄存器的操作是否完成,如果没有,我们必须等待上一次操作结束才 能开始下一次操作。 第二个要介绍的寄存器是 RTC 预分频装载寄存器,也有 2 个寄存器组成,RTC_PRLH 和 RTC_PRLL。这两个寄存器用来配置 RTC 时钟的分频数的,比如我们使用外部 32.768K 的晶振 作为时钟的输入频率,那么我们要设置这两个寄存器的值为 32767,以得到一秒钟的计数频率。 RTC_PRLH 的各位描述如图 20.1.4 所示: 308 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 20.1.4 RTC_PRLH 寄存器各位描述 从图 20.1.4 可以看出,RTC_PRLH 只有低四位有效,用来存储 PRL 的 19~16 位。而 PRL 的前 16 位,存放在 RTC_PRLL 里面,寄存器 RTC_PRLL 的各位描述如图 20.1.5 所示: 图 20.1.5 RTC_PRLL 寄存器各位描述 在介绍完这两个寄存器之后,我们介绍 RTC 预分频器余数寄存器,该寄存器也有 2 个寄存 器组成 RTC_DIVH 和 RTC_DIVL,这两个寄存器的作用就是用来获得比秒钟更为准确的时钟, 比如可以得到 0.1 秒,或者 0.01 秒等。该寄存器的值自减的,用于保存还需要多少时钟周期获 得一个秒信号。在一次秒钟更新后,由硬件重新装载。这两个寄存器和 RTC 预分频装载寄存器 的各位是一样的,这里我们就不列出来了。 接着要介绍的是 RTC 最重要的寄存器,RTC 计数器寄存器 RTC_CNT。该寄存器由 2 个 16 位的寄存器组成 RTC_CNTH 和 RTC_CNTL,总共 32 位,用来记录秒钟值(一般情况下)。此 两个计数器也比较简单,我们也不多说了。注意一点,在修改这个寄存器的时候要先进入配置 模式。 最后我们介绍 RTC 部分的最后一个寄存器,RTC 闹钟寄存器,该寄存器也是由 2 个 16 为 的寄存器组成 RTC_ALRH 和 RTC_ALRL。总共也是 32 位,用来标记闹钟产生的时间(以秒为 单位),如果 RTC_CNT 的值与 RTC_ALR 的值相等,并使能了中断的话,会产生一个闹钟中断。 该寄存器的修改也要进入配置模式才能进行。 因为我们使用到备份寄存器来存储 RTC 的相关信息(我们这里主要用来标记时钟是否已经 经过了配置),我们这里顺便介绍一下 STM32 的备份寄存器。 备份寄存器是 42 个 16 位的寄存器(战舰开发板就是大容量的),可用来存储 84 个字节的 用户应用程序数据。他们处在备份域里,当 VDD 电源被切断,他们仍然由 VBAT 维持供电。 即使系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位。 此外,BKP 控制寄存器用来管理侵入检测和 RTC 校准功能,这里我们不作介绍。 复位后,对备份寄存器和 RTC 的访问被禁止,并且备份域被保护以防止可能存在的意外的 写操作。执行以下操作可以使能对备份寄存器和 RTC 的访问: 1)通过设置寄存器 RCC_APB1ENR 的 PWREN 和 BKPEN 位来打开电源和后备接口的时 钟 2)电源控制寄存器(PWR_CR)的 DBP 位来使能对后备寄存器和 RTC 的访问。 我们一般用 BKP 来存储 RTC 的校验值或者记录一些重要的数据,相当于一个 EEPROM, 309 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 不过这个 EEPROM 并不是真正的 EEPROM,而是需要电池来维持它的数据。关于 BKP 的详细 介绍请看《STM32 参考手册》的第 47 页,5.1 一节。 最后,我们还要介绍一下备份区域控制寄存器 RCC_BDCR。该寄存器的个位描述如图 20.1.6 所示: 图 20.1.6 RCC_ BDCR 寄存器各位描述 RTC 的时钟源选择及使能设置都是通过这个寄存器来实现的,所以我们在 RTC 操作之前 先要通过这个寄存器选择 RTC 的时钟源,然后才能开始其他的操作。 寄存器介绍就给大家介绍到这里了,我们下面来看看要经过哪几个步骤的配置才能使 RTC 正常工作,这里我们将对每个步骤通过库函数的实现方式来讲解。 RTC 相关的库函数在文件 stm32f10x_rtc.c 和 stm32f10x_rtc.h 文件中,BKP 相关的库函数在 文件 stm32f10x_bkp.c 和文件 stm32f10x_bkp.h 文件中。 310 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 RTC 正常工作的一般配置步骤如下: 1)使能电源时钟和备份区域时钟。 前面已经介绍了,我们要访问 RTC 和备份区域就必须先使能电源时钟和备份区域时钟。 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); 2)取消备份区写保护。 要向备份区域写入数据,就要先取消备份区域写保护(写保护在每次硬复位之后被使能), 否则是无法向备份区域写入数据的。我们需要用到向备份区域写入一个字节,来标记时钟已经 配置过了,这样避免每次复位之后重新配置时钟。取消备份区域写保护的库函数实现方法是: PWR_BackupAccessCmd(ENABLE); //使能 RTC 和后备寄存器访问 3)复位备份区域,开启外部低速振荡器。 在取消备份区域写保护之后,我们可以先对这个区域复位,以清除前面的设置,当然这个 操作不要每次都执行,因为备份区域的复位将导致之前存在的数据丢失,所以要不要复位,要 看情况而定。然后我们使能外部低速振荡器,注意这里一般要先判断 RCC_BDCR 的 LSERDY 位来确定低速振荡器已经就绪了才开始下面的操作。 备份区域复位的函数是: BKP_DeInit();//复位备份区域 开启外部低速振荡器的函数是: RCC_LSEConfig(RCC_LSE_ON);// 开启外部低速振荡器 4)选择 RTC 时钟,并使能。 这里我们将通过 RCC_BDCR 的 RTCSEL 来选择选择外部 LSI 作为 RTC 的时钟。然后通过 RTCEN 位使能 RTC 时钟。 库函数中,选择 RTC 时钟的函数是: RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //选择 LSE 作为 RTC 时钟 对于 RTC 时钟的选择,还有 RCC_RTCCLKSource_LSI 和 RCC_RTCCLKSource_HSE_Div128 两个,顾名思义,前者为 LSI,后者为 HSE 的 128 分频,这在时钟系统章节有讲解过。 使能 RTC 时钟的函数是: RCC_RTCCLKCmd(ENABLE); //使能 RTC 时钟 5)设置 RTC 的分频,以及配置 RTC 时钟。 在开启了 RTC 时钟之后,我们要做的就是设置 RTC 时钟的分频数,通过 RTC_PRLH 和 RTC_PRLL 来设置,然后等待 RTC 寄存器操作完成,并同步之后,设置秒钟中断。然后设置 RTC 的允许配置位(RTC_CRH 的 CNF 位),设置时间(其实就是设置 RTC_CNTH 和 RTC_CNTL 两个寄存器)。下面我们一一这些步骤用到的库函数: 在进行 RTC 配置之前首先要打开允许配置位(CNF),库函数是: RTC_EnterConfigMode();/// 允许配置 在配置完成之后,千万别忘记更新配置同时退出配置模式,函数是: RTC_ExitConfigMode();//退出配置模式,更新配置 设置 RTC 时钟分频数,库函数是: void RTC_SetPrescaler(uint32_t PrescalerValue); 这个函数只有一个入口参数,就是 RTC 时钟的分频数,很好理解。 然后是设置秒中断允许,RTC 使能中断的函数是: void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState); 这个函数的第一个参数是设置秒中断类型,这些通过宏定义定义的。对于使能秒中断方法是: RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能 RTC 秒中断 311 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 下一步便是设置时间了,设置时间实际上就是设置 RTC 的计数值,时间与计数值之间是需要换 算的。库函数中设置 RTC 计数值的方法是: void RTC_SetCounter(uint32_t CounterValue)最后在配置完成之后 通过这个函数直接设置 RTC 计数值。 6)更新配置,设置 RTC 中断分组。 在设置完时钟之后,我们将配置更新同时退出配置模式,这里还是通过 RTC_CRH 的 CNF 来实现。库函数的方法是: RTC_ExitConfigMode();//退出配置模式,更新配置 在退出配置模式更新配置之后我们在备份区域 BKP_DR1 中写入 0X5050 代表我们已经初始化 过时钟了,下次开机(或复位)的时候,先读取 BKP_DR1 的值,然后判断是否是 0X5050 来 决定是不是要配置。接着我们配置 RTC 的秒钟中断,并进行分组。 往备份区域写用户数据的函数是: void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data); 这个函数的第一个参数就是寄存器的标号了,这个是通过宏定义定义的。比如我们要往 BKP_DR1 写入 0x5050,方法是: BKP_WriteBackupRegister(BKP_DR1, 0X5050); 同时,有写便有读,读取备份区域指定寄存器的用户数据的函数是: uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR); 这个函数就很好理解了,这里不做过多讲解。 设置中断分组的方法之前已经详细讲解过,调用 NVIC_Init 函数即可,这里不做重复讲解。 7)编写中断服务函数。 最后,我们要编写中断服务函数,在秒钟中断产生的时候,读取当前的时间值,并显示到 TFTLCD 模块上。 通过以上几个步骤,我们就完成了对 RTC 的配置,并通过秒钟中断来更新时间。接下来我 们将进行下一步的工作。 20.2 硬件设计 本实验用到的硬件资源有: 1) 指示灯 DS0 2) 串口 3) TFTLCD 模块 4) RTC 前面 3 个都介绍过了,而 RTC 属于 STM32 内部资源,其配置也是通过软件设置好就可以 了。不过 RTC 不能断电,否则数据就丢失了,我们如果想让时间在断电后还可以继续走,那么 必须确保开发板的电池有电(ALIENTEK 战舰 STM32 开发板标配是有电池的)。 20.3 软件设计 同样,打开我们光盘的 RTC 时钟实验,可以看到,我们的工程中加入了 rtc.c 源文件和 rtc.h 头文件,同时,引入了 stm32f10x_rtc.c 和 stm32f10x_bkp.c 库文件。 由于篇幅所限,rtc.c 中的代码,我们不全部贴出了,这里针对几个重要的函数,进行简要 说明,首先是 RTC_Init,其代码如下: //实时时钟配置 //初始化 RTC 时钟,同时检测时钟是否工作正常 312 STM32F1 开发指南(库函数版) //BKP->DR1 用于保存是否第一次配置的设置 //返回 0:正常 //其他:错误代码 ALIENTEK 战舰 STM32F103 V3 开发板教程 u8 RTC_Init(void) { u8 temp=0; //检查是不是第一次配置时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); //①使能 PWR 和 BKP 外设时钟 PWR_BackupAccessCmd(ENABLE); //②使能后备寄存器访问 if (BKP_ReadBackupRegister(BKP_DR1) != 0x5050) //从指定的后备寄存器中 //读出数据:读出了与写入的指定数据不相乎 { BKP_DeInit(); RCC_LSEConfig(RCC_LSE_ON); //③复位备份区域 //设置外部低速晶振(LSE) while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET&&temp<250) //检查指定的 RCC 标志位设置与否,等待低速晶振就绪 { temp++; delay_ms(10); } if(temp>=250)return 1;//初始化时钟失败,晶振有问题 RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //设置 RTC 时钟 //(RTCCLK),选择 LSE 作为 RTC 时钟 RCC_RTCCLKCmd(ENABLE); //使能 RTC 时钟 RTC_WaitForLastTask(); //等待最近一次对 RTC 寄存器的写操作完成 RTC_WaitForSynchro(); //等待 RTC 寄存器同步 RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能 RTC 秒中断 RTC_WaitForLastTask(); //等待最近一次对 RTC 寄存器的写操作完成 RTC_EnterConfigMode(); // 允许配置 RTC_SetPrescaler(32767); //设置 RTC 预分频的值 RTC_WaitForLastTask(); //等待最近一次对 RTC 寄存器的写操作完成 RTC_Set(2015,1,14,17,42,55); //设置时间 RTC_ExitConfigMode(); //退出配置模式 BKP_WriteBackupRegister(BKP_DR1, 0X5050); //向指定的后备寄存器中 //写入用户程序数据 0x5050 } else//系统继续计时 { RTC_WaitForSynchro(); //等待最近一次对 RTC 寄存器的写操作完成 RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能 RTC 秒中断 RTC_WaitForLastTask(); //等待最近一次对 RTC 寄存器的写操作完成 } 313 STM32F1 开发指南(库函数版) RTC_NVIC_Config(); RTC_Get(); ALIENTEK 战舰 STM32F103 V3 开发板教程 //RCT 中断分组设置 //更新时间 return 0; //ok } 该函数用来初始化 RTC 时钟,但是只在第一次的时候设置时间,以后如果重新上电/复位 都不会再进行时间设置了(前提是备份电池有电),在第一次配置的时候,我们是按照上面介绍 的 RTC 初始化步骤来做的,这里就不在多说了,这里我们设置时间是通过时间设置函数 RTC_Set 函数来实现的,该函数将在后续进行介绍。这里我们默认将时间设置为 2015 年 1 月 14 日,17 点 42 分 55 秒。在设置好时间之后,我们通过 BKP_WriteBackupRegister()函数向 BKP->DR1 写 入标志字 0X5050,用于标记时间已经被设置了。这样,再次发生复位的时候,该函数通过 BKP_ReadBackupRegister()读取 BKP->DR1 的值,来判断决定是不是需要重新设置时间,如果 不需要设置,则跳过时间设置,仅仅使能秒钟中断一下,就进行中断分组,然后返回了。这样 不会重复设置时间,使得我们设置的时间不会因复位或者断电而丢失。 该函数还有返回值,返回值代表此次操作的成功与否,如果返回 0,则代表初始化 RTC 成 功,如果返回值非零则代表错误代码了。 介绍完 RTC_Init,我们来介绍一下 RTC_Set 函数,该函数代码如下: //设置时钟 //把输入的时钟转换为秒钟 //以 1970 年 1 月 1 日为基准 //1970~2099 年为合法年份 //返回值:0,成功;其他:错误代码. //月份数据表 u8 const table_week[12]={0,3,3,6,1,4,6,2,5,0,3,5}; //月修正数据表 //平年的月份日期表 const u8 mon_table[12]={31,28,31,30,31,30,31,31,30,31,30,31}; u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec) { u16 t; u32 seccount=0; if(syear<1970||syear>2099)return 1; for(t=1970;tCNTH; //得到计数器中的值(秒钟数) timecount<<=16; timecount+=RTC->CNTL; temp=timecount/86400; if(daycnt!=temp) //得到天数(秒钟数对应的) //超过一天了 { daycnt=temp; temp1=1970; //从 1970 年开始 while(temp>=365) { if(Is_Leap_Year(temp1)) //是闰年 { if(temp>=366)temp-=366; //闰年的秒钟数 else break; } else temp-=365; //平年 temp1++; } calendar.w_year=temp1; //得到年份 temp1=0; while(temp>=28) //超过了一个月 315 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 { if(Is_Leap_Year(calendar.w_year)&&temp1==1)//当年是不是闰年/2 月份 { if(temp>=29)temp-=29;//闰年的秒钟数 else break; } else { if(temp>=mon_table[temp1])temp-=mon_table[temp1];//平年 else break; } temp1++; } calendar.w_month=temp1+1; //得到月份 calendar.w_date=temp+1; //得到日期 } temp=timecount%86400; calendar.hour=temp/3600; calendar.min=(temp%3600)/60; calendar.sec=(temp%3600)%60; //得到秒钟数 //小时 //分钟 //秒钟 calendar.week=RTC_Get_Week(calendar.w_year,calendar.w_month,calendar.w_date); //获取星期 return 0; } 函数其实就是将存储在秒钟寄存器 RTC->CNTH 和 RTC->CNTL 中的秒钟数据(通过函数 RTC_SetCounter 设置)转换为真正的时间和日期。该代码还用到了一个 calendar 的结构体, calendar 是我们在 rtc.h 里面将要定义的一个时间结构体,用来存放时钟的年月日时分秒等信息。 因为 STM32 的 RTC 只有秒钟计数器,而年月日,时分秒这些需要我们自己软件计算。我们把 计算好的值保存在 calendar 里面,方便其他程序调用。 最后,我们介绍一下秒钟中断服务函数,该函数代码如下: //RTC 时钟中断 //每秒触发一次 void RTC_IRQHandler(void) { if (RTC_GetITStatus(RTC_IT_SEC) != RESET) //秒钟中断 { RTC_Get(); //更新时间 } if(RTC_GetITStatus(RTC_IT_ALR)!= RESET) //闹钟中断 { RTC_ClearITPendingBit(RTC_IT_ALR); RTC_Get(); //更新时间 //清闹钟中断 printf("Alarm Time:%d-%d-%d %d:%d:%d\n",calendar.w_year,calendar.w_month, calendar.w_date,calendar.hour,calendar.min,calendar.sec);//输出闹铃时间 316 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 } RTC_ClearITPendingBit(RTC_IT_SEC|RTC_IT_OW); //清闹钟中断 RTC_WaitForLastTask(); } 此部分代码比较简单,我们通过 RTC_GetITStatus 来判断发生的是何种中断,如果是秒钟 中断,则执行一次时间的计算,获得最新时间,结果保存在 calendar 结构体里面,因此,我们 可以在 calendar 里面读到最新的时间、日期等信息。如果是闹钟中断,则更新时间后,将当前 的闹铃时间通过 printf 打印出来,可以在串口调试助手看到当前的闹铃情况。 rtc.c 的其他程序,这里就不再介绍了,请大家直接看光盘的源码。接下来看看 rtc.h 代码, 在 rtc.h 中,我们定义了一个结构体: typedef struct { vu8 hour; vu8 min; vu8 sec; //公历日月年周 vu16 w_year; vu8 w_month; vu8 w_date; vu8 week; }_calendar_obj; 从上面结构体定义可以看到_calendar_obj 结构体所包含的成员变量是一个完整的公历信 息,包括年、月、日、周、时、分、秒等 7 个元素。我们以后要知道当前时间,只需要通过 RTC_Get 函数,执行时钟转换,然后就可以从 calendar 里面读出当前的公历时间了。 最后看看 main.c 里面的代码如下: int main(void) { u8 t=0; delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置 NVIC 中断分组 2 uart_init(115200); //串口初始化波特率为 115200 LED_Init(); //LED 端口初始化 LCD_Init(); //LCD 初始化 usmart_dev.init(72); //初始化 USMART POINT_COLOR=RED; //设置字体为红色 LCD_ShowString(30,50,200,16,16,"WarShip STM32F103 ^_^"); LCD_ShowString(30,70,200,16,16,"RTC TEST"); LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(30,110,200,16,16,"2015/1/14"); while(RTC_Init()) //RTC 初始化 ,一定要初始化成功 { LCD_ShowString(60,130,200,16,16,"RTC ERROR! "); delay_ms(800); 317 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 LCD_ShowString(60,130,200,16,16,"RTC Trying..."); } //显示时间 POINT_COLOR=BLUE; //设置字体为蓝色 LCD_ShowString(60,130,200,16,16," - - "); LCD_ShowString(60,162,200,16,16," : : "); while(1) { if(t!=calendar.sec) { t=calendar.sec; LCD_ShowNum(60,130,calendar.w_year,4,16); LCD_ShowNum(100,130,calendar.w_month,2,16); LCD_ShowNum(124,130,calendar.w_date,2,16); switch(calendar.week) { case 0:LCD_ShowString(60,148,200,16,16,"Sunday "); break; case 1:LCD_ShowString(60,148,200,16,16,"Monday "); break; case 2:LCD_ShowString(60,148,200,16,16,"Tuesday "); break; case 3:LCD_ShowString(60,148,200,16,16,"Wednesday"); break; case 4:LCD_ShowString(60,148,200,16,16,"Thursday "); break; case 5:LCD_ShowString(60,148,200,16,16,"Friday "); break; case 6:LCD_ShowString(60,148,200,16,16,"Saturday "); break; } LCD_ShowNum(60,162,calendar.hour,2,16); LCD_ShowNum(84,162,calendar.min,2,16); LCD_ShowNum(108,162,calendar.sec,2,16); LED0=!LED0; } delay_ms(10); }; } 这部分代码就不再需要详细解释了,在包含了 rtc.h 之后,通过判断 calendar.sec 是否改变 来决定要不要更新时间显示。同时我们设置 LED0 每 2 秒钟闪烁一次,用来提示程序已经开始 318 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 跑了。 为了方便设置时间,我们在 usmart_config.c 里面,修改 usmart_nametab 如下: struct _m_usmart_nametab usmart_nametab[]= { #if USMART_USE_WRFUNS==1 //如果使能了读写操作 (void*)read_addr,"u32 read_addr(u32 addr)", (void*)write_addr,"void write_addr(u32 addr,u32 val)", #endif (void*)delay_ms,"void delay_ms(u16 nms)", (void*)delay_us,"void delay_us(u32 nus)", (void*)RTC_Set,"u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec)", }; 将 RTC_Set 加入了 usmart,同时去掉了上一章的设置(减少代码量),这样通过串口就可 以直接设置 RTC 时间了。 至此,RTC 实时时钟的软件设计就完成了,接下来就让我们来检验一下,我们的程序是否 正确了。 20.4 下载验证 将程序下载到战舰 STM32 后,可以看到 DS0 不停的闪烁,提示程序已经在运行了。同时 可以看到 TFTLCD 模块开始显示时间,实际显示效果如图 20.4.1 所示: 图 20.4.1 RTC 实验效果图 如果时间不正确,大家可以用上一章介绍的方法,通过串口调用 RTC_Set 来设置一下当前 时间。 319 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第二十一章 待机唤醒实验 本章我们将向大家介绍 STM32F1 的待机唤醒功能。在本章中,我们将利用 WK_UP 按键 来实现唤醒和进入待机模式的功能,然后利用 DS0 指示状态。本章将分为如下几个部分: 21.1 STM32 待机模式简介 21.2 硬件设计 21.3 软件设计 21.4 下载验证 320 21.1 STM32 待机模式简介 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 很多单片机都有低功耗模式,STM32 也不例外。在系统或电源复位以后,微控制器处于运 行状态。运行状态下的 HCLK 为 CPU 提供时钟,内核执行程序代码。当 CPU 不需继续运行时, 可以利用多个低功耗模式来节省功耗,例如等待某个外部事件时。用户需要根据最低电源消耗, 最快速启动时间和可用的唤醒源等条件,选定一个最佳的低功耗模式。STM32 的 3 种低功耗模 式我们在 5.2.4 节有粗略介绍,这里我们再回顾一下。 STM32 的低功耗模式有 3 种: 1)睡眠模式(CM3 内核停止,外设仍然运行) 2)停止模式(所有时钟都停止) 3)待机模式(1.8V 内核电源关闭) 在运行模式下,我们也可以通过降低系统时钟关闭 APB 和 AHB 总线上未被使用的外设的 时钟来降低功耗。三种低功耗模式一览表见表 21.1.1 所示: 表 21.1.1 STM32 低功耗一览表 在这三种低功耗模式中,最低功耗的是待机模式,在此模式下,最低只需要 2uA 左右的电 流。停机模式是次低功耗的,其典型的电流消耗在 20uA 左右。最后就是睡眠模式了。用户可 以根据自己的需求来决定使用哪种低功耗模式。 本章,我们仅对 STM32 的最低功耗模式-待机模式,来做介绍。待机模式可实现 STM32 的最低功耗。该模式是在 CM3 深睡眠模式时关闭电压调节器。整个 1.8V 供电区域被断电。PLL、 HSI 和 HSE 振荡器也被断电。SRAM 和寄存器内容丢失。仅备份的寄存器和待机电路维持供电。 那么我们如何进入待机模式呢?其实很简单,只要按图 21.1.1 所示的步骤执行就可以了: 图 21.1.1 STM32 进入及退出待机模式的条件 图 21.1.1 还列出了退出待机模式的操作,从图 21.1.1 可知,我们有 4 种方式可以退出待机 模式,即当一个外部复位(NRST 引脚)、IWDG 复位、WKUP 引脚上的上升沿或 RTC 闹钟事件 发生时,微控制器从待机模式退出。从待机唤醒后,除了电源控制/状态寄存器(PWR_CSR),所 321 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 有寄存器被复位。 从待机模式唤醒后的代码执行等同于复位后的执行(采样启动模式引脚,读取复位向量等)。 电源控制/状态寄存器(PWR_CSR)将会指示内核由待机状态退出。 在进入待机模式后,除了复位引脚以及被设置为防侵入或校准输出时的 TAMPER 引脚和被 使能的唤醒引脚(WK_UP 脚),其他的 IO 引脚都将处于高阻态。 图 21.1.1 已经清楚的说明了进入待机模式的通用步骤,其中涉及到 2 个寄存器,即电源控 制寄存器(PWR_CR)和电源控制/状态寄存器(PWR_CSR)。下面我们介绍一下这两个寄存器: 电源控制寄存器(PWR_CR),该寄存器的各位描述如图 21.1.2 所示: 图 21.1.2 PWR_CR 寄存器各位描述 322 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 这里我们通过设置 PWR_CR 的 PDDS 位,使 CPU 进入深度睡眠时进入待机模式,同时我 们通过 CWUF 位,清除之前的唤醒位。电源控制/状态寄存器(PWR_CSR)的各位描述如图 21.1.3 所示: 图 21.1.3 PWR_ CSR 寄存器各位描述 这里,我们通过设置 PWR_CSR 的 EWUP 位,来使能 WKUP 引脚用于待机模式唤醒。我 们还可以从 WUF 来检查是否发生了唤醒事件。不过本章我们并没有用到。 通过以上介绍,我们了解了进入待机模式的方法,以及设置 WK_UP 引脚用于把 STM32 从待机模式唤醒的方法。具体步骤如下: 1)使能电源时钟。 因为要配置电源控制寄存器,所以必须先使能电源时钟。 在库函数中,使能电源时钟的方法是: RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); //使能 PWR 外设时钟 这个函数非常容易理解。 2) 设置 WK_UP 引脚作为唤醒源。 使能时钟之后后再设置 PWR_CSR 的 EWUP 位,使能 WK_UP 用于将 CPU 从待机模式唤 醒。在库函数中,设置使能 WK_UP 用于唤醒 CPU 待机模式的函数是: 323 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 PWR_WakeUpPinCmd(ENABLE); //使能唤醒管脚功能 3)设置 SLEEPDEEP 位,设置 PDDS 位,执行 WFI 指令,进入待机模式。 进入待机模式,首先要设置 SLEEPDEEP 位(该位在系统控制寄存器(SCB_SCR)的第 二位,详见《CM3 权威指南》,第 182 页表 13.1),接着我们通过 PWR_CR 设置 PDDS 位,使 得 CPU 进入深度睡眠时进入待机模式,最后执行 WFI 指令开始进入待机模式,并等待 WK_UP 中断的到来。在库函数中,进行上面三个功能进入待机模式是在函数 PWR_EnterSTANDBYMode 中实现的: void PWR_EnterSTANDBYMode(void); 4)最后编写 WK_UP 中断函数。 因为我们通过 WK_UP 中断(PA0 中断)来唤醒 CPU,所以我们有必要设置一下该中断函 数,同时我们也通过该函数里面进入待机模式。 通过以上几个步骤的设置,我们就可以使用 STM32 的待机模式了,并且可以通过 WK_UP 来唤醒 CPU,我们最终要实现这样一个功能:通过长按(3 秒)WK_UP 按键开机,并且通过 DS0 的闪烁指示程序已经开始运行,再次长按该键,则进入待机模式,DS0 关闭,程序停止运 行。类似于手机的开关机。 21.2 硬件设计 本实验用到的硬件资源有: 1) 指示灯 DS0 2) WK_UP 按键 本章,我们使用了 WK_UP 按键用于唤醒和进入待机模式。然后通过 DS0 来指示程序是否 在运行。这两个硬件的连接前面均有介绍。 21.3 软件设计 打开待机唤醒实验工程,我们可以发现工程中多了一个 wkup.c 和 wkup.h 文件,相关的用 户代码写在这两个文件中。同时,对于待机唤醒功能,我们需要引入 stm32f10x_pwr.c 和 stm32f0x_pwr.h 文件。 打开 wkup.c,可以看到如下关键代码: void Sys_Standby(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); //使能 PWR 外设时钟 PWR_WakeUpPinCmd(ENABLE); //使能唤醒管脚功能 PWR_EnterSTANDBYMode(); //进入待命(STANDBY)模式 } //系统进入待机模式 void Sys_Enter_Standby(void) { RCC_APB2PeriphResetCmd(0X01FC,DISABLE); //复位所有 IO 口 Sys_Standby(); } //检测 WKUP 脚的信号 //返回值 1:连续按下 3s 以上 // 0:错误的触发 324 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 u8 Check_WKUP(void) { u8 t=0; //记录按下的时间 LED0=0; //亮灯 DS0 while(1) { if(WKUP_KD) { t++; //已经按下了 delay_ms(30); if(t>=100) //按下超过 3 秒钟 { LED0=0; //点亮 DS0 return 1; //按下 3s 以上了 } }else { LED0=1; return 0; //按下不足 3 秒 } } } //中断,检测到 PA0 脚的一个上升沿. //中断线 0 线上的中断检测 void EXTI0_IRQHandler(void) { EXTI_ClearITPendingBit(EXTI_Line0); // 清除 LINE10 上的中断标志位 if(Check_WKUP()) //关机? { Sys_Enter_Standby(); } } //PA0 WKUP 唤醒初始化 void WKUP_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; EXTI_InitTypeDef EXTI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); //使能 GPIOA 和复用功能时钟 GPIO_InitStructure.GPIO_Pin =GPIO_Pin_0; //PA.0 GPIO_InitStructure.GPIO_Mode =GPIO_Mode_IPD; //上拉输入 325 STM32F1 开发指南(库函数版) GPIO_Init(GPIOA, &GPIO_InitStructure); //使用外部中断方式 ALIENTEK 战舰 STM32F103 V3 开发板教程 //初始化 IO GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); //中断线 0 连接 GPIOA.0 EXTI_InitStructure.EXTI_Line = EXTI_Line0; //设置按键所有的外部线路 EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //外部中断模式 EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; //上升沿触发 EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); // 初始化外部中断 NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; //使能外部中断通道 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //先占优先级 2 级 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; //从优先级 2 级 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //外部中断通道使能 NVIC_Init(&NVIC_InitStructure); //初始化 NVIC if(Check_WKUP()==0) Sys_Standby(); //不是开机,进入待机模式 } 注 意 , 这 个 项 目 中 不 要 同 时 引 用 exit.c 文 件 , 因 为 exit.c 里 面 也 有 一 个 void EXTI0_IRQHandler(void)函数,如果不删除,MDK 就会报错。该部分代码比较简单,我们在这 里说明两点: 1,在 void Sys_Enter_Standby(void)函数里面,我们要在进入待机模式前把所有开启的外设 全部关闭,我们这里仅仅复位了所有的 IO 口,使得 IO 口全部为浮空输入。其他外设(比如 ADC 等),大家根据自己所开启的情况进行一一关闭就可,这样才能达到最低功耗! 2,在 void WKUP_Init(void)函数里面,我们要先判断 WK_UP 是否按下了 3 秒钟,来决定 要不要开机,如果没有按下 3 秒钟,程序直接就进入了待机模式。所以在下载完代码的时候, 是看不到任何反应的。我们必须先按 WK_UP 按键 3 秒钟以开机,才能看到 DS0 闪烁。 wkup.h 头文件的代码非常简单,这里我们就不列出来。最后我们看看 main.c 里面 main 函 数代码如下: int main(void) { delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置 NVIC 中断分组 2 uart_init(115200); //串口初始化波特率为 115200 LED_Init(); //LED 端口初始化 WKUP_Init(); //待机唤醒初始化 LCD_Init(); //LCD 初始化 POINT_COLOR=RED; LCD_ShowString(30,50,200,16,16,"Warship STM32"); LCD_ShowString(30,70,200,16,16,"WKUP TEST"); LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(30,110,200,16,16,"2014/1/14"); while(1) 326 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 { LED0=!LED0; delay_ms(250); } } 这里我们先初始化 LED 和 WK_UP 按键(通过 WKUP_Init()函数初始化),如果检测到 有长按 WK_UP 按键 3 秒以上,则开机,并执行 LCD 初始化,在 LCD 上面显示一些内容,如 果没有长按,则在 WKUP_Init 里面,调用 Sys_Enter_Standby 函数,直接进入待机模式了。 开机后,在死循环里面等待 WK_UP 中断的到来,在得到中断后,在中断函数里面判断 WK_UP 按下的时间长短,来决定是否进入待机模式,如果按下时间超过 3 秒,则进入待机, 否则退出中断,继续执行 main 函数的死循环等待,同时不停的取反 LED0,让红灯闪烁。 代码部分就介绍到这里,大家记住下载代码后,一定要长按 WK_UP 按键,来开机,否则 将直接进入待机模式,无任何现象。 21.4 下载与测试 在代码编译成功之后,下载代码到战舰 STM32 V3 上,此时,看不到任何现象,和没下载 代码一样,其实这是正常的,在程序下载完之后,开发板检测不到 WK_UP(即 WK_UP 按键) 的持续按下(3 秒以上),所以直接进入待机模式,看起来和没有下载代码一样。然后,我们长 按 WK_UP 按键 3 秒钟左右(WK_UP 按下时,DS0 会长亮),可以看到 DS0 开始闪烁,液晶也 会显示一些内容。然后再长按 WK_UP,DS0 会灭掉,液晶灭掉,程序再次进入待机模式。 327 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第二十二章 ADC 实验 本章我们将向大家介绍 STM32F1 的 ADC 功能。在本章中,我们将利用 STM32F1 的 ADC1 通道 1 来采样外部电压值,并在 TFTLCD 模块上显示出来。本章将分为如下几个部分: 22.1 STM32 ADC 简介 22.2 硬件设计 22.3 软件设计 22.4 下载验证 328 22.1 STM32 ADC 简介 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 STM32 拥有 1~3 个 ADC(STM32F101/102 系列只有 1 个 ADC),这些 ADC 可以独立使用, 也可以使用双重模式(提高采样率)。STM32 的 ADC 是 12 位逐次逼近型的模拟数字转换器。 它有 18 个通道,可测量 16 个外部和 2 个内部信号源。各通道的 A/D 转换可以单次、连续、扫 描或间断模式执行。ADC 的结果可以左对齐或右对齐方式存储在 16 位数据寄存器中。 模拟看 门狗特性允许应用程序检测输入电压是否超出用户定义的高/低阀值。 STM32F103 系列最少都拥有 2 个 ADC,我们选择的 STM32F103ZET 包含有 3 个 ADC。 STM32 的 ADC 最大的转换速率为 1Mhz,也就是转换时间为 1us(在 ADCCLK=14M,采样周期 为 1.5 个 ADC 时钟下得到),不要让 ADC 的时钟超过 14M,否则将导致结果准确度下降。 STM32 将 ADC 的转换分为 2 个通道组:规则通道组和注入通道组。规则通道相当于你正 常运行的程序,而注入通道呢,就相当于中断。在你程序正常执行的时候,中断是可以打断你 的执行的。同这个类似,注入通道的转换可以打断规则通道的转换, 在注入通道被转换完成之 后,规则通道才得以继续转换。 通过一个形象的例子可以说明:假如你在家里的院子内放了 5 个温度探头,室内放了 3 个 温度探头;你需要时刻监视室外温度即可,但偶尔你想看看室内的温度;因此你可以使用规则 通道组循环扫描室外的 5 个探头并显示 AD 转换结果,当你想看室内温度时,通过一个按钮启 动注入转换组(3 个室内探头)并暂时显示室内温度,当你放开这个按钮后,系统又会回到规则通 道组继续检测室外温度。从系统设计上,测量并显示室内温度的过程中断了测量并显示室外温 度的过程,但程序设计上可以在初始化阶段分别设置好不同的转换组,系统运行中不必再变更 循环转换的配置,从而达到两个任务互不干扰和快速切换的结果。可以设想一下,如果没有规 则组和注入组的划分,当你按下按钮后,需要从新配置 AD 循环扫描的通道,然后在释放按钮 后需再次配置 AD 循环扫描的通道。 上面的例子因为速度较慢,不能完全体现这样区分(规则通道组和注入通道组)的好处,但 在工业应用领域中有很多检测和监视探头需要较快地处理,这样对 AD 转换的分组将简化事件 处理的程序并提高事件处理的速度。 STM32 其 ADC 的规则通道组最多包含 16 个转换,而注入通道组最多包含 4 个通道。关于 这两个通道组的详细介绍,请参考《STM32 参考手册的》第 155 页,第 11 章。 STM32 的 ADC 可以进行很多种不同的转换模式,这些模式在《STM32 参考手册》的第 11 章也都有详细介绍,我们这里就不在一一列举了。我们本章仅介绍如何使用规则通道的单次转 换模式。 STM32 的 ADC 在单次转换模式下,只执行一次转换,该模式可以通过 ADC_CR2 寄存器 的 ADON 位(只适用于规则通道)启动,也可以通过外部触发启动(适用于规则通道和注入通 道),这是 CONT 位为 0。 以规则通道为例,一旦所选择的通道转换完成,转换结果将被存在 ADC_DR 寄存器中, EOC(转换结束)标志将被置位,如果设置了 EOCIE,则会产生中断。然后 ADC 将停止,直 到下次启动。 接下来,我们介绍一下我们执行规则通道的单次转换,需要用到的 ADC 寄存器。第一个 要介绍的是 ADC 控制寄存器(ADC_CR1 和 ADC_CR2)。ADC_CR1 的各位描述如图 22.1.1 所 示: 329 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 22.1.1 ADC_CR1 寄存器各位描述 这里我们不再详细介绍每个位,而是抽出几个我们本章要用到的位进行针对性的介绍,详 细的说明及介绍,请参考《STM32 参考手册》第 11 章的相关章节。 ADC_CR1 的 SCAN 位,该位用于设置扫描模式,由软件设置和清除,如果设置为 1,则 使用扫描模式,如果为 0,则关闭扫描模式。在扫描模式下,由 ADC_SQRx 或 ADC_JSQRx 寄 存器选中的通道被转换。如果设置了 EOCIE 或 JEOCIE,只在最后一个通道转换完毕后才会产 生 EOC 或 JEOC 中断。 ADC_CR1[19:16]用于设置 ADC 的操作模式,详细的对应关系如图 22.1.2 所示: 图 22.1.2 ADC 操作模式 本章我们要使用的是独立模式,所以设置这几位为 0 就可以了。接着我们介绍 ADC_CR2, 该寄存器的各位描述如图 22.1.3 所示: 图 22.1.3 ADC_CR2 寄存器操作模式 该寄存器我们也只针对性的介绍一些位:ADON 位用于开关 AD 转换器。而 CONT 位用于 设置是否进行连续转换,我们使用单次转换,所以 CONT 位必须为 0。CAL 和 RSTCAL 用于 AD 校准。ALIGN 用于设置数据对齐,我们使用右对齐,该位设置为 0。 EXTSEL[2:0]用于选择启动规则转换组转换的外部事件,详细的设置关系如图 22.1.4 所示: 330 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 22.1.4 ADC 选择启动规则转换事件设置 我们这里使用的是软件触发(SWSTART),所以设置这 3 个位为 111。ADC_CR2 的 SWSTART 位用于开始规则通道的转换,我们每次转换(单次转换模式下)都需要向该位写 1。 AWDEN 为用于使能温度传感器和 Vrefint。STM32 内部的温度传感器我们将在下一节介绍。 第二个要介绍的是 ADC 采样事件寄存器(ADC_SMPR1 和 ADC_SMPR2),这两个寄存器 用于设置通道 0~17 的采样时间,每个通道占用 3 个位。ADC_SMPR1 的各位描述如图 22.1.5 所示: 图 22.1.5 ADC_SMPR1 寄存器各位描述 ADC_SMPR2 的各位描述如下图 22.1.6 所示: 331 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 22.1.6 ADC_SMPR2 寄存器各位描述 对于每个要转换的通道,采样时间建议尽量长一点,以获得较高的准确度,但是这样会降 低 ADC 的转换速率。ADC 的转换时间可以由以下公式计算: Tcovn=采样时间+12.5 个周期 其中:Tcovn 为总转换时间,采样时间是根据每个通道的 SMP 位的设置来决定的。例如, 当 ADCCLK=14Mhz 的时候,并设置 1.5 个周期的采样时间,则得到:Tcovn=1.5+12.5=14 个周 期=1us。 第三个要介绍的是 ADC 规则序列寄存器(ADC_SQR1~3),该寄存器总共有 3 个,这几个 寄存器的功能都差不多,这里我们仅介绍一下 ADC_SQR1,该寄存器的各位描述如图 22.1.7 所 示: 图 22.1.7 ADC_ SQR1 寄存器各位描述 L[3:0]用于存储规则序列的长度,我们这里只用了 1 个,所以设置这几个位的值为 0。其 他的 SQ13~16 则存储了规则序列中第 13~16 个通道的编号(0~17)。另外两个规则序列寄存器 332 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 同 ADC_SQR1 大同小异,我们这里就不再介绍了,要说明一点的是:我们选择的是单次转换, 所以只有一个通道在规则序列里面,这个序列就是 SQ1,通过 ADC_SQR3 的最低 5 位(也就 是 SQ1)设置。 第四个要介绍的是 ADC 规则数据寄存器(ADC_DR)。规则序列中的 AD 转化结果都将被存 在这个寄存器里面,而注入通道的转换结果被保存在 ADC_JDRx 里面。ADC_DR 的各位描述 如图 22.1.8: 图 22.1.8 ADC_ DRx 寄存器各位描述 这里要提醒一点的是,该寄存器的数据可以通过 ADC_CR2 的 ALIGN 位设置左对齐还是 右对齐。在读取数据的时候要注意。 最后一个要介绍的 ADC 寄存器为 ADC 状态寄存器(ADC_SR),该寄存器保存了 ADC 转 换时的各种状态。该寄存器的各位描述如图 22.1.9 所示: 333 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 22.1.9 ADC_ SR 寄存器各位描述 这里我们要用到的是 EOC 位,我们通过判断该位来决定是否此次规则通道的 AD 转换已经 完成,如果完成我们就从 ADC_DR 中读取转换结果,否则等待转换完成。 通过以上寄存器的介绍,我们了解了 STM32 的单次转换模式下的相关设置,下面我们介 绍使用库函数的函数来设定使用 ADC1 的通道 1 进行 AD 转换。这里需要说明一下,使用到的 库函数分布在 stm32f10x_adc.c 文件和 stm32f10x_adc.h 文件中。下面讲解其详细设置步骤: 1)开启 PA 口时钟和 ADC1 时钟,设置 PA1 为模拟输入。 STM32F103ZET6 的 ADC 通道 1 在 PA1 上,所以,我们先要使能 PORTA 的时钟和 ADC1 时钟,然后设置 PA1 为模拟输入。使能 GPIOA 和 ADC 时钟用 RCC_APB2PeriphClockCmd 函 数,设置 PA1 的输入方式,使用 GPIO_Init 函数即可。这里我们列出 STM32 的 ADC 通道与 GPIO 对应表: 334 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 22.1.20 ADC 通道与 GPIO 对应表 2)复位 ADC1,同时设置 ADC1 分频因子。 开启 ADC1 时钟之后,我们要复位 ADC1,将 ADC1 的全部寄存器重设为缺省值之后我们 就可以通过 RCC_CFGR 设置 ADC1 的分频因子。分频因子要确保 ADC1 的时钟(ADCCLK) 不要超过 14Mhz。 这个我们设置分频因子位 6,时钟为 72/6=12MHz,库函数的实现方法是: RCC_ADCCLKConfig(RCC_PCLK2_Div6); ADC 时钟复位的方法是: ADC_DeInit(ADC1); 这个函数非常容易理解,就是复位指定的 ADC。 3)初始化 ADC1 参数,设置 ADC1 的工作模式以及规则序列的相关信息。 在设置完分频因子之后,我们就可以开始 ADC1 的模式配置了,设置单次转换模式、触发 方式选择、数据对齐方式等都在这一步实现。同时,我们还要设置 ADC1 规则序列的相关信息, 我们这里只有一个通道,并且是单次转换的,所以设置规则序列中通道数为 1。这些在库函数 中是通过函数 ADC_Init 实现的,下面我们看看其定义: void ADC_Init(ADC_TypeDef* ADCx, ADC_InitTypeDef* ADC_InitStruct); 从函数定义可以看出,第一个参数是指定 ADC 号。这里我们来看看第二个参数,跟其他外设 初始化一样,同样是通过设置结构体成员变量的值来设定参数。 typedef struct { uint32_t ADC_Mode; FunctionalState ADC_ScanConvMode; FunctionalState ADC_ContinuousConvMode; uint32_t ADC_ExternalTrigConv; uint32_t ADC_DataAlign; uint8_t ADC_NbrOfChannel; }ADC_InitTypeDef; 335 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 参数 ADC_Mode 故名是以是用来设置 ADC 的模式。前面讲解过,ADC 的模式非常多,包括独 立模式,注入同步模式等等,这里我们选择独立模式,所以参数为 ADC_Mode_Independent。 参数 ADC_ScanConvMode 用来设置是否开启扫描模式,因为是单次转换,这里我们选择不开 启值 DISABLE 即可。 参数 ADC_ContinuousConvMode 用来设置是否开启连续转换模式,因为是单次转换模式,所以 我们选择不开启连续转换模式,DISABLE 即可。 参数 ADC_ExternalTrigConv 是用来设置启动规则转换组转换的外部事件,这里我们选择软件触 发,选择值为 ADC_ExternalTrigConv_None 即可。 参数 DataAlign 用来设置 ADC 数据对齐方式是左对齐还是右对齐,这里我们选择右对齐方式 ADC_DataAlign_Right。 参数 ADC_NbrOfChannel 用来设置规则序列的长度,这里我们是单次转换,所以值为 1 即可。 通过上面对每个参数的讲解,下面来看看我们的初始化范例: ADC_InitTypeDef ADC_InitStructure; ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //ADC 工作模式:独立模式 ADC_InitStructure.ADC_ScanConvMode = DISABLE; //AD 单通道模式 ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //AD 单次转换模式 ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //转换由软件而不是外部触发启动 ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //ADC 数据右对齐 ADC_InitStructure.ADC_NbrOfChannel = 1; //顺序进行规则转换的 ADC 通道的数目 1 ADC_Init(ADC1, &ADC_InitStructure); //根据指定的参数初始化外设 ADCx 5)使能 ADC 并校准。 在设置完了以上信息后,我们就使能 AD 转换器,执行复位校准和 AD 校准,注意这两步 是必须的!不校准将导致结果很不准确。 使能指定的 ADC 的方法是: ADC_Cmd(ADC1, ENABLE); //使能指定的 ADC1 执行复位校准的方法是: ADC_ResetCalibration(ADC1); 执行 ADC 校准的方法是: ADC_StartCalibration(ADC1); //开始指定 ADC1 的校准状态 记住,每次进行校准之后要等待校准结束。这里是通过获取校准状态来判断是否校准是否结束。 下面我们一一列出复位校准和 AD 校准的等待结束方法: while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束 while(ADC_GetCalibrationStatus(ADC1)); //等待校 AD 准结束 6)读取 ADC 值。 在上面的校准完成之后,ADC 就算准备好了。接下来我们要做的就是设置规则序列 1 里面 的通道,采样顺序,以及通道的采样周期,然后启动 ADC 转换。在转换结束后,读取 ADC 转 换结果值就是了。这里设置规则序列通道以及采样周期的函数是: void ADC_RegularChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel, uint8_t Rank, uint8_t ADC_SampleTime); 我们这里是规则序列中的第 1 个转换,同时采样周期为 239.5,所以设置为: ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5 ); 软件开启 ADC 转换的方法是: 336 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 ADC_SoftwareStartConvCmd(ADC1, ENABLE);//使能指定的 ADC1 的软件转换启动功能 开启转换之后,就可以获取转换 ADC 转换结果数据,方法是: ADC_GetConversionValue(ADC1); 同时在 AD 转换中,我们还要根据状态寄存器的标志位来获取 AD 转换的各个状态信息。库函 数获取 AD 转换的状态信息的函数是: FlagStatus ADC_GetFlagStatus(ADC_TypeDef* ADCx, uint8_t ADC_FLAG) 比如我们要判断 ADC1d 的转换是否结束,方法是: while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));//等待转换结束 这里还需要说明一下 ADC 的参考电压,战舰 STM32 开发板使用的是 STM32F103ZET6, 该芯片有外部参考电压:Vref-和 Vref+,其中 Vref-必须和 VSSA 连接在一起,而 Vref+的输入 范围为:2.4~VDDA。战舰 STM23 开发板通过 P7 端口,设置 Vref-和 Vref+设置参考电压,默 认的我们是通过跳线帽将 Vref-接到 GND,Vref+接到 VDDA,参考电压就是 3.3V。如果大家想 自己设置其他参考电压,将你的参考电压接在 Vref-和 Vref+上就 OK 了。本章我们的参考电压 设置的是 3.3V。 通过以上几个步骤的设置,我们就能正常的使用 STM32 的 ADC1 来执行 AD 转换操作了。 22.2 硬件设计 本实验用到的硬件资源有: 1) 指示灯 DS0 2) TFTLCD 模块 3) ADC 4) 杜邦线 前面 2 个均已介绍过,而 ADC 属于 STM32 内部资源,实际上我们只需要软件设置就可以 正常工作,不过我们需要在外部连接其端口到被测电压上面。本章,我们通过 ADC1 的通道 1 (PA1)来读取外部电压值,战舰 STM32 开发板没有设计参考电压源在上面,但是板上有几个 可以提供测试的地方:1,3.3V 电源。2,GND。3,后备电池。注意:这里不能接到板上 5V 电源上去测试,这可能会烧坏 ADC!。 因为要连接到其他地方测试电压,所以我们需要 1 跟杜邦线,或者自备的连接线也可以, 一头插在多功能端口 P14 的 ADC 插针上(与 PA1 连接),另外一头就接你要测试的电压点(确 保该电压不大于 3.3V 即可)。 22.3 软件设计 打开我们的 ADC 转换实验,可以看到工程中多了一个 adc.c 文件和 adc.h 文件。同时 ADC 相关的库函数是在 stm32f10x_adc.c 文件和 stm32f10x_adc.h 文件中。 打开 adc.c,可以看到代码如下: //初始化 ADC //这里我们仅以规则通道为例 //我们默认将开启通道 0~3 void Adc_Init(void) { ADC_InitTypeDef ADC_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | 337 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 RCC_APB2Periph_ADC1 , ENABLE ); //使能 ADC1 通道时钟 RCC_ADCCLKConfig(RCC_PCLK2_Div6); //设置 ADC 分频因子 6 //72M/6=12,ADC 最大时间不能超过 14M //PA1 作为模拟通道输入引脚 GPIO_InitStructure.GPIO_Pin =GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;//模拟输入 GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 GPIOA.1 ADC_DeInit(ADC1); //复位 ADC1,将外设 ADC1 的全部寄存器重设为缺省值 ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //ADC 独立模式 ADC_InitStructure.ADC_ScanConvMode = DISABLE; //单通道模式 ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //单次转换模式 ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//转换由 //软件而不是外部触发启动 ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //ADC 数据右对齐 ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &ADC_InitStructure); ADC_Cmd(ADC1, ENABLE); //顺序进行规则转换的 ADC 通道的数目 //根据指定的参数初始化外设 ADCx //使能指定的 ADC1 ADC_ResetCalibration(ADC1); //开启复位校准 while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束 ADC_StartCalibration(ADC1); //开启 AD 校准 while(ADC_GetCalibrationStatus(ADC1)); //等待校准结束 } //获得 ADC 值 //ch:通道值 0~3 u16 Get_Adc(u8 ch) { //设置指定 ADC 的规则组通道,设置它们的转化顺序和采样时间 ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5 ); //通道 1,规则采样顺序值为 1,采样时间为 239.5 周期 ADC_SoftwareStartConvCmd(ADC1, ENABLE); //使能软件转换功能 while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));//等待转换结束 return ADC_GetConversionValue(ADC1); //返回最近一次 ADC1 规则组的转换结果 } u16 Get_Adc_Average(u8 ch,u8 times) { u32 temp_val=0; u8 t; for(t=0;t4000)temp_val=4000; return (u8)(100-(temp_val/40)); } 这里就 2 个函数,其中:Lsens_Init 用于初始化光敏传感器,其实就是初始化 PF8 为模拟 输入,然后通过 Adc3_Init 函数初始化 ADC3。Lsens_Get_Val 函数用于获取当前光照强度,该 函数通过 Get_Adc3 得到 ADC3_CH6 转换的电压值,经过简单量化后,处理成 0~100 的光强值。 0 对应最暗,100 对应最亮。 头文件 lsens.h 内容比较简单,主要是一些函数申明以及宏定义敞亮,这里我们就不多说了。 接下来我们打开 adc.c,可以看到我们添加了 Adc3_Init 和 Get_Adc3 两个函数。Adc3_Init 函数和 ADC_Init 函数几乎是一模一样,但是没有设置对应 IO 为模拟输入,因为这个在 Lsens_Init 函数已经实现。Get_Adc3 用于获取 ADC3 某个通道的转换结果。 最后我们来看看主函数内容: int main(void) { u8 adcx; delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组 2 uart_init(115200); //串口初始化为 115200 348 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 LED_Init(); //初始化与 LED 连接的硬件接口 LCD_Init(); //初始化 LCD Lsens_Init(); //初始化光敏传感器 POINT_COLOR=RED;//设置字体为红色 //显示提示信息 LCD_ShowString(30,50,200,16,16,"WarShip STM32"); LCD_ShowString(30,70,200,16,16,"LSENS TEST"); LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(30,110,200,16,16,"2015/1/14"); POINT_COLOR=BLUE;//设置字体为蓝色 LCD_ShowString(30,130,200,16,16,"LSENS_VAL:"); while(1) { adcx=Lsens_Get_Val(); LCD_ShowxNum(30+10*8,130,adcx,3,16,0);//显示 ADC 的值 LED0=!LED0; delay_ms(250); } } 此部分代码也比较简单,初始化各个外设之后,进入死循环,通过 Lsens_Get_Val 获取光 敏传感器得到的光强值(0~100),并显示在 TFTLCD 上面。 代码设计部分就为大家讲解到这里,下面我们开始下载验证。 24.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK 战舰 STM32F103 上,可以看到 LCD 显示如图 24.4.1 所示: 图 24.4.1 光敏传感器实验测试图 伴随 DS0 的不停闪烁,提示程序在运行。此时,我们可以通过给 LS1 不同的光照强度,来 观察 LSENS_VAL 值的变化,光照越强,该值越大,光照越弱,该值越小。 349 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第二十五章 DAC 实验 上两章,我们介绍了 STM32 的 ADC 使用,本章我们将向大家介绍 STM32 的 DAC 功能。 在本章中,我们将利用按键(或 USMART)控制 STM32 内部 DAC 模块的通道 1 来输出电压, 通过 ADC1 的通道 1 采集 DAC 的输出电压,在 LCD 模块上面显示 ADC 获取到的电压值以及 DAC 的设定输出电压值等信息。本章将分为如下几个部分: 25.1 STM32 DAC 简介 25.2 硬件设计 25.3 软件设计 25.4 下载验证 350 25.1 STM32 DAC 简介 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 大容量的 STM32F103 具有内部 DAC,战舰 STM32 选择的是 STM32F103ZET6 属于大容量 产品,所以是带有 DAC 模块的。 STM32 的 DAC 模块(数字/模拟转换模块)是 12 位数字输入,电压输出型的 DAC。DAC 可 以配置为 8 位或 12 位模式,也可以与 DMA 控制器配合使用。DAC 工作在 12 位模式时,数据 可以设置成左对齐或右对齐。DAC 模块有 2 个输出通道,每个通道都有单独的转换器。在双 DAC 模式下,2 个通道可以独立地进行转换,也可以同时进行转换并同步地更新 2 个通道的输 出。DAC 可以通过引脚输入参考电压 VREF+以获得更精确的转换结果。 STM32 的 DAC 模块主要特点有: ① 2 个 DAC 转换器:每个转换器对应 1 个输出通道 ② 8 位或者 12 位单调输出 ③ 12 位模式下数据左对齐或者右对齐 ④ 同步更新功能 ⑤ 噪声波形生成 ⑥ 三角波形生成 ⑦ 双 DAC 通道同时或者分别转换 ⑧ 每个通道都有 DMA 功能 单个 DAC 通道的框图如图 25.1.1 所示: 图 25.1.1 DAC 通道模块框图 图中 VDDA 和 VSSA 为 DAC 模块模拟部分的供电,而 Vref+则是 DAC 模块的参考电压。 351 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 DAC_OUTx 就是 DAC 的输出通道了(对应 PA4 或者 PA5 引脚)。 从图 25.1.1 可以看出,DAC 输出是受 DORx 寄存器直接控制的,但是我们不能直接往 DORx 寄存器写入数据,而是通过 DHRx 间接的传给 DORx 寄存器,实现对 DAC 输出的控制。前面 我们提到,STM32 的 DAC 支持 8/12 位模式,8 位模式的时候是固定的右对齐的,而 12 位模式 又可以设置左对齐/右对齐。单 DAC 通道 x,总共有 3 种情况: ① 8 位数据右对齐:用户将数据写入 DAC_DHR8Rx[7:0]位(实际是存入 DHRx[11:4]位)。 ② 12 位数据左对齐:用户将数据写入 DAC_DHR12Lx[15:4]位(实际是存入 DHRx[11:0] 位)。 ③ 12 位数据右对齐:用户将数据写入 DAC_DHR12Rx[11:0]位(实际是存入 DHRx[11:0] 位)。 我们本章使用的就是单 DAC 通道 1,采用 12 位右对齐格式,所以采用第③种情况。 如果没有选中硬件触发(寄存器 DAC_CR1 的 TENx 位置’0’),存入寄存器 DAC_DHRx 的数据会在一个 APB1 时钟周期后自动传至寄存器 DAC_DORx。如果选中硬件触发(寄存器 DAC_CR1 的 TENx 位置’1’),数据传输在触发发生以后 3 个 APB1 时钟周期后完成。 一旦 数据从 DAC_DHRx 寄存器装入 DAC_DORx 寄存器,在经过时间 之后,输出即有效, 这段时间的长短依电源电压和模拟输出负载的不同会有所变化。我们可以从 STM32F103ZET6 的数据手册查到 的典型值为 3us,最大是 4us。所以 DAC 的转换速度最快是 250K 左 右。 本章我们将不使用硬件触发(TEN=0),其转换的时间框图如图 25.1.2 所示: 图 25.1.2 TEN=0 时 DAC 模块转换时间框图 当 DAC 的参考电压为 Vref+的时候,DAC 的输出电压是线性的从 0~Vref+,12 位模式下 DAC 输出电压与 Vref+以及 DORx 的计算公式如下: DACx 输出电压=Vref*(DORx/4095) 接下来,我们介绍一下要实现 DAC 的通道 1 输出,需要用到的一些寄存器。首先是 DAC 控制寄存器 DAC_CR,该寄存器的各位描述如图 25.1.3 所示: 352 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 25.1.3 寄存器 DAC_CR 各位描述 DAC_CR 的低 16 位用于控制通道 1,而高 16 位用于控制通道 2,我们这里仅列出比较重 要的最低 8 位的详细描述,如图 25.1.4 所示: 图 25.1.4 寄存器 DAC_CR 低八位详细描述 首先,我们来看 DAC 通道 1 使能位(EN1),该位用来控制 DAC 通道 1 使能的,本章我们 就是用的 DAC 通道 1,所以该位设置为 1。 再看关闭 DAC 通道 1 输出缓存控制位(BOFF1),这里 STM32 的 DAC 输出缓存做的有些 不好,如果使能的话,虽然输出能力强一点,但是输出没法到 0,这是个很严重的问题。所以 本章我们不使用输出缓存。即设置该位为 1。 DAC 通道 1 触发使能位(TEN1),该位用来控制是否使用触发,里我们不使用触发,所以 设置该位为 0。 DAC 通道 1 触发选择位(TSEL1[2:0]),这里我们没用到外部触发,所以设置这几个位为 0 就行了。 353 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 DAC 通道 1 噪声/三角波生成使能位(WAVE1[1:0]),这里我们同样没用到波形发生器,故 也设置为 0 即可。 DAC 通道 1 屏蔽/幅值选择器(MAMP[3:0]),这些位仅在使用了波形发生器的时候有用, 本章没有用到波形发生器,故设置为 0 就可以了。 最后是 DAC 通道 1 DMA 使能位(DMAEN1),本章我们没有用到 DMA 功能,故还是设 置为 0。 通道 2 的情况和通道 1 一模一样,这里就不不细说了。在 DAC_CR 设置好之后,DAC 就 可以正常工作了,我们仅需要再设置 DAC 的数据保持寄存器的值,就可以在 DAC 输出通道得 到你想要的电压了(对应 IO 口设置为模拟输入)。本章,我们用的是 DAC 通道 1 的 12 位右对 齐数据保持寄存器:DAC_DHR12R1,该寄存器各位描述如图 25.1.5 所示: 图 25.1.5 寄存器 DAC_DHR12R1 各位描述 该寄存器用来设置 DAC 输出,通过写入 12 位数据到该寄存器,就可以在 DAC 输出通道 1 (PA4)得到我们所要的结果。 通过以上介绍,我们了解了 STM32 实现 DAC 输出的相关设置,本章我们将使用库函数的 方法来设置 DAC 模块的通道 1 来输出模拟电压,其详细设置步骤如下: 1)开启 PA 口时钟,设置 PA4 为模拟输入。 STM32F103ZET6 的 DAC 通道 1 在 PA4 上,所以,我们先要使能 PORTA 的时钟,然后设 置 PA4 为模拟输入。DAC 本身是输出,但是为什么端口要设置为模拟输入模式呢?因为一但 使能 DACx 通道之后,相应的 GPIO 引脚(PA4 或者 PA5)会自动与 DAC 的模拟输出相连,设 置为输入,是为了避免额外的干扰。 使能 GPIOA 时钟: RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE ); //使能 PORTA 时钟 设置 PA1 为模拟输入只需要设置初始化参数即可: GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //模拟输入 2)使能 DAC1 时钟。 同其他外设一样,要想使用,必须先开启相应的时钟。STM32 的 DAC 模块时钟是由 APB1 提供的,所以我们调用函数 RCC_APB1PeriphClockCmd()设置 DAC 模块的时钟使能。 RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE ); //使能 DAC 通道时钟 3)初始化 DAC,设置 DAC 的工作模式。 该部分设置全部通过 DAC_CR 设置实现,包括:DAC 通道 1 使能、DAC 通道 1 输出缓存 关闭、不使用触发、不使用波形发生器等设置。这里 DMA 初始化是通过函数 DAC_Init 完成 的: void DAC_Init(uint32_t DAC_Channel, DAC_InitTypeDef* DAC_InitStruct) 跟前面一样,首先我们来看看参数设置结构体类型 DAC_InitTypeDef 的定义: 354 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 typedef struct { uint32_t DAC_Trigger; uint32_t DAC_WaveGeneration; uint32_t DAC_LFSRUnmask_TriangleAmplitude; uint32_t DAC_OutputBuffer; }DAC_InitTypeDef; 这个结构体的定义还是比较简单的,只有四个成员变量,下面我们一一讲解。 第一个参数 DAC_Trigger 用来设置是否使用触发功能,前面已经讲解过这个的含义,这里我们 不是用触发功能,所以值为 DAC_Trigger_None。 第二个参数 DAC_WaveGeneratio 用来设置是否使用波形发生,这里我们前面同样讲解过不使 用。所以值为 DAC_WaveGeneration_None。 第三个参数 DAC_LFSRUnmask_TriangleAmplitude 用来设置屏蔽/幅值选择器,这个变量只在使 用波形发生器的时候才有用,这里我们设置为 0 即可,值为 DAC_LFSRUnmask_Bit0。 第四个参数 DAC_OutputBuffer 是用来设置输出缓存控制位,前面讲解过,我们不使用输出缓存, 所以值为 DAC_OutputBuffer_Disable。到此四个参数设置完毕。看看我们的实例代码: DAC_InitTypeDef DAC_InitType; DAC_InitType.DAC_Trigger=DAC_Trigger_None; //不使用触发功能 TEN1=0 DAC_InitType.DAC_WaveGeneration=DAC_WaveGeneration_None;//不使用波形发生 DAC_InitType.DAC_LFSRUnmask_TriangleAmplitude=DAC_LFSRUnmask_Bit0; DAC_InitType.DAC_OutputBuffer=DAC_OutputBuffer_Disable ; //DAC1 输出缓存关闭 DAC_Init(DAC_Channel_1,&DAC_InitType); //初始化 DAC 通道 1 4)使能 DAC 转换通道 初始化 DAC 之后,理所当然要使能 DAC 转换通道,库函数方法是: DAC_Cmd(DAC_Channel_1, ENABLE); //使能 DAC1 5)设置 DAC 的输出值。 通过前面 4 个步骤的设置,DAC 就可以开始工作了,我们使用 12 位右对齐数据格式,所 以我们通过设置 DHR12R1,就可以在 DAC 输出引脚(PA4)得到不同的电压值了。库函数的 函数是: DAC_SetChannel1Data(DAC_Align_12b_R, 0); 第一个参数设置对齐方式,可以为 12 位右对齐 DAC_Align_12b_R,12 位左对齐 DAC_Align_12b_L 以及 8 位右对齐 DAC_Align_8b_R 方式。 第二个参数就是 DAC 的输入值了,这个很好理解,初始化设置为 0。 这里,还可以读出 DAC 的数值,函数是: DAC_GetDataOutputValue(DAC_Channel_1); 设置和读出一一对应很好理解,这里就不多讲解了。 最后,再提醒一下大家,本例程,我们使用的是 3.3V 的参考电压,即 Vref+连接 VDDA。 通过以上几个步骤的设置,我们就能正常的使用 STM32 的 DAC 通道 1 来输出不同的模拟 电压了。 25.2 硬件设计 本章用到的硬件资源有: 355 STM32F1 开发指南(库函数版) 1)指示灯 DS0 2) WK_UP 和 KEY1 按键 3) 串口 4) TFTLCD 模块 ALIENTEK 战舰 STM32F103 V3 开发板教程 5) ADC 6) DAC 本章,我们使用 DAC 通道 1 输出模拟电压,然后通过 ADC1 的通道 1 对该输出电压进行 读取,并显示在 LCD 模块上面,DAC 的输出电压,我们通过按键(或 USMART)进行设置。 我们需要用到 ADC 采集 DAC 的输出电压,所以需要在硬件上把他们短接起来。ADC 和 DAC 的连接原理图如图 25.2.1 所示: 图 25.2.1 ADC、DAC 与 STM32 连接原理图 注意:STM_DAC 和 GBC_KEY 共用 PA4,所以,如果您的开发板 ATK MODULE 位置插 了其他模块,那么可能影响 DAC 的输出结果,建议在做 DAC 实验的时候,ATK MODULE 位 置,不要插任何其他模块。 P10 是多功能端口,我们只需要通过跳线帽短接 P10 的 ADC 和 DAC,就可以开始做本章 实验了。如图 25.2.2 所示: 图 25.2.2 硬件连接示意图 25.3 软件设计 打开光盘的 DAC 实验可以看到,项目中添加了 dac.c 文件以及头文件 dac.h。同时,dac 相 关的函数分布在固件库文件 stm32f10x_dac.c 文件和 stm32f10x_dac.h 头文件中。 打开 dac.c,代码如下: 356 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 #include "dac.h" //DAC 通道 1 输出初始化 void Dac1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; DAC_InitTypeDef DAC_InitType; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE ); //①使能 PA 时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE ); //②使能 DAC 时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_SetBits(GPIOA,GPIO_Pin_4) ; // 端口配置 //模拟输入 //①初始化 GPIOA //PA.4 输出高 DAC_InitType.DAC_Trigger=DAC_Trigger_None; //不使用触发功能 DAC_InitType.DAC_WaveGeneration=DAC_WaveGeneration_None;//不使用波形发生 DAC_InitType.DAC_LFSRUnmask_TriangleAmplitude=DAC_LFSRUnmask_Bit0; DAC_InitType.DAC_OutputBuffer=DAC_OutputBuffer_Disable ; //DAC1 输出缓存关 DAC_Init(DAC_Channel_1,&DAC_InitType); //③初始化 DAC 通道 1 DAC_Cmd(DAC_Channel_1, ENABLE); //④使能 DAC1 DAC_SetChannel1Data(DAC_Align_12b_R, 0); //⑤12 位右对齐,设置 DAC 初始值 } //设置通道 1 输出电压 //vol:0~3300,代表 0~3.3V void Dac1_Set_Vol(u16 vol) { float temp=vol; temp/=1000; temp=temp*4096/3.3; DAC_SetChannel1Data(DAC_Align_12b_R,temp);// 12 位右对齐设置 DAC 值 } 此部分代码就 2 个函数,Dac1_Init 函数用于初始化 DAC 通道 1。步骤①~⑤基本上是按我 们上面的步骤来初始化的,经过这个初始化之后,我们就可以正常使用 DAC 通道 1 了。第二 个函数 Dac1_Set_Vol,用于设置 DAC 通道 1 的输出电压,通过 USMART 调用该函数,就可以 随意设置 DAC 通道 1 的输出电压了。 接下来我们看看 main 函数如下: int main(void) { u16 adcx; float temp; u8 t=0; 357 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 u16 dacval=0; u8 key; delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置 NVIC 中断分组 2 uart_init(115200); //串口初始化波特率为 115200 KEY_Init(); //初始化按键程序 LED_Init(); //LED 端口初始化 LCD_Init(); //LCD 初始化 usmart_dev.init(72); //初始化 USMART Adc_Init(); //ADC 初始化 Dac1_Init(); //DAC 初始化 POINT_COLOR=RED;//设置字体为红色 LCD_ShowString(30,50,200,16,16,"WarShip STM32"); LCD_ShowString(30,70,200,16,16,"DAC TEST"); LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(30,110,200,16,16,"2015/1/15"); LCD_ShowString(30,130,200,16,16,"WK_UP:+ KEY1:-"); //显示提示信息 POINT_COLOR=BLUE;//设置字体为蓝色 LCD_ShowString(60,150,200,16,16,"DAC VAL:"); LCD_ShowString(60,170,200,16,16,"DAC VOL:0.000V"); LCD_ShowString(60,190,200,16,16,"ADC VOL:0.000V"); DAC_SetChannel1Data(DAC_Align_12b_R, 0); //初始值为 0 while(1) { t++; key=KEY_Scan(0); if(key== WKUP_PRES) { if(dacval<4000)dacval+=200; DAC_SetChannel1Data(DAC_Align_12b_R, dacval); //设置 DAC 值 }else if(key== KEY1_PRES) { if(dacval>200)dacval-=200; else dacval=0; DAC_SetChannel1Data(DAC_Align_12b_R, dacval); //设置 DAC 值 } if(t==10||key==KEY1_PRES||key==WKUP_PRES) { adcx=DAC_GetDataOutputValue(DAC_Channel_1); //读取前面设置 DAC 的值 LCD_ShowxNum(124,150,adcx,4,16,0); //显示 DAC 寄存器值 358 STM32F1 开发指南(库函数版) temp=(float)adcx*(3.3/4096); ALIENTEK 战舰 STM32F103 V3 开发板教程 //得到 DAC 电压值 adcx=temp; LCD_ShowxNum(124,170,temp,1,16,0); //显示电压值整数部分 temp-=adcx; temp*=1000; LCD_ShowxNum(140,170,temp,3,16,0X80); adcx=Get_Adc_Average(ADC_Channel_1,10); temp=(float)adcx*(3.3/4096); //显示电压值的小数部分 //得到 ADC 转换值 //得到 ADC 电压值 adcx=temp; LCD_ShowxNum(124,190,temp,1,16,0); //显示电压值整数部分 temp-=adcx; temp*=1000; LCD_ShowxNum(140,190,temp,3,16,0X80); //显示电压值的小数部分 LED0=!LED0; t=0; } delay_ms(10); } } 此部分代码,我们先对需要用到的模块进行初始化,然后显示一些提示信息,本章我们通 过 WK_UP 和 KEY1(也就是上下键)来实现对 DAC 输出的幅值控制。按下 WK_UP 增加,按 KEY1 减小。同时在 LCD 上面显示 DHR12R1 寄存器的值、DAC 设计输出电压以及 ADC 采集 到的 DAC 输出电压。 本章,我们还可以利用 USMART 来设置 DAC 的输出电压值,故需要将 Dac1_Set_Vol 函数 加入 USMART 控制,方法前面已经有详细的介绍了,大家这里自行添加,或者直接查看我们 光盘的源码。 从 main 函数代码可以看出,按键设置输出电压的时候,每次都是以 0.161V 递增或递减的, 而通过 USMART 调用 Dac1_Set_Vol 函数,则可以实现任意电平输出控制(当然得在 DAC 可控 范围内)。 25.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK 战舰 STM32 开发板上,可以看到 LCD 显示如图 25.4.1 所示: 359 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 25.4.1 ADC 实验实际测试图 同时伴随 DS0 的不停闪烁,提示程序在运行。此时,我们通过按 WK_UP 按键,可以看到 输出电压增大,按 KEY1 则变小。 大家可以试试在 USMART 调用 Dac1_Set_Vol 函数,来设置 DAC 通道 1 的输出电压,如图 25.4.2 所示: 图 25.4.2 通过 usmart 设置 DAC 通道 1 的电压输出 360 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第二十六章 PWM DAC 实验 上一章,我们介绍了 STM32F1 自带 DAC 模块的使用,但有时候,可能两个 DAC 不够用,, 我们可以通过 PWM+RC 滤波来实一个 PWM DAC。本章我们将向大家介绍如何利用 STM32 的 PWM 来设计一个 DAC。我们将利用按键(或 USMART)控制 STM32 的 PWM 输出,从而控 制 PWM DAC 的输出电压,通过 ADC1 的通道 1 采集 PWM DAC 的输出电压,并在 LCD 模块 上面显示 ADC 获取到的电压值以及 PWM DAC 的设定输出电压值等信息。本章将分为如下几 个部分: 26.1 PWM DAC 简介 26.2 硬件设计 26.3 软件设计 26.4 下载验证 361 26.1 PWM DAC 简介 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 虽然大容量的 STM32F103 具有内部 DAC,但是更多的型号是没有 DAC 的,不过 STM32 所有的芯片都有 PWM 输出,因此,我们可以用 PWM+简单的 RC 滤波来实现 DAC 输出, 从而节省成本。 PWM 本质上其实就是是一种周期一定,而高低电平占空比可调的方波。实际电路的典 型 PWM 波形,如图 26.1.1 所示: 图 26.1.1 实际电路典型 PWM 波形 图 26.1.1 的 PWM 波形可以用分段函数表示为式①: 其中:T 是单片机中计数脉冲的基本周期,也就是 STM32 定时器的计数频率的倒数。 N 是 PWM 波一个周期的计数脉冲个数,也就是 STM32 的 ARR-1 的值。n 是 PWM 波一个 周期中高电平的计数脉冲个数,也就是 STM32 的 CCRx 的值。VH 和 VL 分别是 PWM 波的 高低电平电压值,k 为谐波次数,t 为时间。我们将①式展开成傅里叶级数,得到公式②: 从②式可以看出,式中第 1 个方括弧为直流分量,第 2 项为 1 次谐波分量,第 3 项为大 于 1 次的高次谐波分量。式②中的直流分量与 n 成线性关系,并随着 n 从 0 到 N,直流分量 从 VL 到 VL+VH 之间变化。这正是电压输出的 DAC 所需要的。因此,如果能把式②中除 直流分量外的谐波过滤掉,则可以得到从 PWM 波到电压输出 DAC 的转换,即:PWM 波 可以通过一个低通滤波器进行解调。式②中的第 2 项的幅度和相角与 n 有关,频率为 1(/ NT), 其实就是 PWM 的输出频率。该频率是设计低通滤波器的依据。如果能把 1 次谐波很好过滤 掉,则高次谐波就应该基本不存在了。 通过上面的了解,我们可以得到 PWM DAC 的分辨率,计算公式如下: 分辨率=log2(N) 这里假设 n 的最小变化为 1,当 N=256 的时候,分辨率就是 8 位。而 STM32 的定时器 都是 16 位的,可以很容易得到更高的分辨率,分辨率越高,速度就越慢。不过我们在本章 要设计的 DAC 分辨率为 8 位。 在 8 位分辨条件下,我们一般要求 1 次谐波对输出电压的影响不要超过 1 个位的精度, 362 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 也就是 3.3/256=0.01289V。假设 VH 为 3.3V,VL 为 0V,那么一次谐波的最大值是 2*3.3/ π=2.1V,这就要求我们的 RC 滤波电路提供至少-20lg(2.1/0.01289)=-44dB 的衰减。 STM32 的定 时 器 最 快 的 计 数 频 率 是 72Mhz ,8 为 分 辨 率 的 时 候 , PWM 频 率 为 72M/256=281.25Khz。如果是 1 阶 RC 滤波,则要求截止频率为 1.77Khz,如果为 2 阶 RC 滤 波,则要求截止频率为 22.34Khz。 战舰 STM32 开发板的 PWM DAC 输出采用二阶 RC 滤波,该部分原理图如图 26.1.2 所 示: 图 26.1.2 PWM DAC 二阶 RC 滤波原理图 二阶 RC 滤波截止频率计算公式为: f=1/2πRC 以上公式要求 R1=R2=R,C2=C2=C。根据这个公式,我们计算出图 26.1.2 的截止频率 为:33.8Khz 超过了 22.34Khz,这个和我们前面提到的要求有点出入,原因是该电路我们还 需要用作 PWM DAC 音频输出,而音频信号带宽是 22.05Khz,为了让音频信号能够通过该 低通滤波,同时为了标准化参数选取,所以确定了这样的参数。实测精度在 0.5LSB 以内。 PWM DAC 的原理部分,就为大家介绍到这里。 26.2 硬件设计 本章用到的硬件资源有: 1) 指示灯 DS0 2) WK_UP 和 KEY1 按键 3) 串口 4) TFTLCD 模块 5) ADC 6) PWM DAC 本章,我们使用 STM32 的定时器 1 的通道 1(PA8)输出 PWM,经过二阶 RC 滤波后, 转换为直流输出,实现 PWM DAC。同上一章一样,我们通过 ADC1 的通道 1(PA1)读取 PWM DAC 的输出,并在 LCD 模块上显示相关数值,通过按键和 USMART 控制 PWM DAC 的输出值。我们需要用到 ADC 采集 DAC 的输出电压,所以需要在硬件上将 PWM DAC 和 ADC 短接起来,PWM DAC 部分原理图如图 26.2.1 所示: 363 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 26.2.1 PWM DAC 原理图 从上图可知 PWM_DAC 的连接关系,但是这里有个特别需要注意的地方 :因为 PWM_DAC 和 OV_VSYNC 共用了 PA8 引脚,所以在做本例程的时候,不能插摄像头模块 或 OLED 模块,否则可能会影响 PWM 转换结果!!! 在硬件上,我们还需要用跳线帽短接多功能端口的 PDC 和 ADC,如图 26.2.2 所示: 图 26.2.2 硬件连接示意图 26.3 软件设计 找到 PWM DAC 实验工程,打开 timer.c 文件,可以看到我们在文件的最后添加了一个 新的函数:TIM1_PWM_Init,该函数代码如下: //TIM1 CH1 PWM 输出设置 //PWM 输出初始化 //arr:自动重装值 //psc:时钟预分频数 void TIM1_PWM_Init(u16 arr,u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; 364 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); //使能 TIMx 外设 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //使能 PA 时钟 //设置该引脚为复用输出功能,输出 TIM1 CH1 的 PWM 脉冲波形 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; //TIM1_CH1 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用功能输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 GPIO TIM_TimeBaseStructure.TIM_Period = arr; //设置自动重装载周期值 TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置预分频值 不分频 TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数 TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); //初始化 TIMx TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; //CH1 PWM2 模式 TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能 TIM_OCInitStructure.TIM_Pulse = 0; //设置待装入捕获比较寄存器的脉冲值 TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low; //OC1 低电平有效 TIM_OC1Init(TIM1, &TIM_OCInitStructure); //根据指定的参数初始化外设 TIMx TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); //CH1 预装载使能 TIM_ARRPreloadConfig(TIM1, ENABLE); //使能 TIMx 在 ARR 上的预装载寄存器 TIM_CtrlPWMOutputs(TIM1,ENABLE); //MOE 主输出使能,高级定时器必须开启 TIM_Cmd(TIM1, ENABLE); //使能 TIMx } 该函数用来初始化 TIM1_CH1 的 PWM 输出(PA8),其原理同之前介绍的 PWM 输出 一模一样,只是换过一个定时器而已。不过这里 TIM1 是高级定时器,高级定时器的 PWM 输出,与普通定时器稍有区别,必须通过函数 TIM_CtrlPWMOutputs 来设置 BDTR 寄存器 的 MOE 位为 1,才可以正常输出 PWM,这点要特别注意。 接下来我们打开 main.c 文件,代码如下: //设置输出电压 //vol:0~330,代表 0~3.3V void PWM_DAC_Set(u16 vol) { float temp=vol; temp/=100; temp=temp*256/3.3; TIM_SetCompare1(TIM1,temp); } int main(void) { u16 adcx; 365 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 float temp; u8 t=0; u16 pwmval=0; u8 key; delay_init(); //延时函数初始化 NVIC_Configuration(); //设置 NVIC 中断分组 2:2 位抢占优先级,2 位响应优先级 uart_init(115200); //串口初始化为 115200 KEY_Init(); //KEY 初始化 LED_Init(); //LED 端口初始化 usmart_dev.init(72); //初始化 USMART LCD_Init(); //LCD 初始化 Adc_Init(); //ADC 初始化 TIM1_PWM_Init(255,0); //TIM1 PWM 初始化, Fpwm=72M/256=281.25Khz. TIM_SetCompare1(TIM1,100);//初始值为 0 POINT_COLOR=RED;//设置字体为红色 LCD_ShowString(60,50,200,16,16,"WarShip STM32"); LCD_ShowString(60,70,200,16,16,"PWM DAC TEST"); LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(60,110,200,16,16,"2015/1/14"); LCD_ShowString(60,130,200,16,16,"WK_UP:+ KEY1:-"); //显示提示信息 POINT_COLOR=BLUE;//设置字体为蓝色 LCD_ShowString(60,150,200,16,16,"PWM VAL:"); LCD_ShowString(60,170,200,16,16,"DAC VOL:0.000V"); LCD_ShowString(60,190,200,16,16,"ADC VOL:0.000V"); TIM_SetCompare1(TIM1,pwmval);//初始值 while(1) { t++; key=KEY_Scan(0); if(key==WKUP_PRES) { if(pwmval<250)pwmval+=10; TIM_SetCompare1(TIM1,pwmval); //输出 }else if(key==KEY1_PRES) { if(pwmval>10)pwmval-=10; else pwmval=0; TIM_SetCompare1(TIM1,pwmval); //输出 } if(t==10||key==KEY1_PRES||key==WKUP_PRES) { 366 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 adcx=TIM_GetCapture1(TIM1); LCD_ShowxNum(124,150,adcx,4,16,0); temp=(float)adcx*(3.3/256); //显示 DAC 寄存器值 //得到 DAC 电压值 adcx=temp; LCD_ShowxNum(124,170,temp,1,16,0); //显示电压值整数部分 temp-=adcx; temp*=1000; LCD_ShowxNum(140,170,temp,3,16,0x80); //显示电压值的小数部分 adcx=Get_Adc_Average(ADC_Channel_1,20); //得到 ADC 转换值 temp=(float)adcx*(3.3/4096); //得到 ADC 电压值 adcx=temp; LCD_ShowxNum(124,190,temp,1,16,0); //显示电压值整数部分 temp-=adcx; temp*=1000; LCD_ShowxNum(140,190,temp,3,16,0x80); //显示电压值的小数部分 t=0; LED0=!LED0; } delay_ms(10); } } 此部分代码,同上一章的基本一样,先对需要用到的模块进行初始化,然后显示一些提 示信息,本章我们通过 WK_UP 和 KEY1(也就是上下键)来实现对 PWM 脉宽的控制,经 过 RC 滤波,最终实现对 DAC 输出幅值的控制。按下 WK_UP 增加,按 KEY1 减小。同时 在 LCD 上面显示 TIM1_CCR1 寄存器的值、PWM DAC 设计输出电压以及 ADC 采集到的实 际输出电压。同时 DS0 闪烁,提示程序运行状况。 不过此部分代码还有一个 PWM_DAC_Set 函数,用于 USMART 调用,从而通过串口控 制 PWM DAC 的输出,所以还需要将 PWM_DAC_Set 函数加入 USMART 控制,方法前面 已经有详细的介绍了,大家这里自行添加,或者直接查看我们光盘的源码。 26.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK 战舰 STM32 开发板上,可以看 到 LCD 显示如图 26.4.1 所示: 367 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 26.4.1 ADC 实验实际测试图 同时伴随 DS0 的不停闪烁,提示程序在运行。此时,我们通过按 WK_UP 按键,可以 看到输出电压增大,按 KEY1 则变小。特别提醒:此时不要插任何模块到 P6 接口(OLED/ 摄像头模块接口)上面,否则可能导致结果有误差。 大家可以试试在 USMART 调用 PWM_DAC_Set 函数,来设置 PWM DAC 的输出电压, 如图 26.4.2 所示: 图 26.4.2 通过 usmart 设置 PWM DAC 的电压输出 368 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第二十七章 DMA 实验 本章我们将向大家介绍 STM32F1 的 DMA。在本章中,我们将利用 STM32F1 的 DMA 来实现串口数据传送,并在 TFTLCD 模块上显示当前的传送进度。本章分为如下几个部分: 27.1 STM32 DMA 简介 27.2 硬件设计 27.3 软件设计 27.4 下载验证 369 27.1 STM32 DMA 简介 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 DMA,全称为:Direct Memory Access,即直接存储器访问,DMA 传输将数据从一个 地址空间复制到另外一个地址空间。当 CPU 初始化这个传输动作,传输动作本身是由 DMA 控制器 来实行和完成。典型的例子就是移动一个外部内存的区块到芯片内部更快的 内存区。像是这样的操作并没有让处理器工作拖延,反而可以被重新排程去处理其他的工 作。DMA 传输对于高效能嵌入式系统算法和网络是很重要的。DMA 传输方式无需 CPU 直接 控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为 RAM 与 I/O 设备 开辟一条直接传送数据的通路,能使 CPU 的效率大为提高。 STM32 最多有 2 个 DMA 控制器(DMA2 仅存在大容量产品中),DMA1 有 7 个通道。DMA2 有 5 个通道。每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁起 来协调各个 DMA 请求的优先权。 STM32 的 DMA 有以下一些特性: ●每个通道都直接连接专用的硬件 DMA 请求,每个通道都同样支持软件触发。这些功能 通过软件来配置。 ●在七个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),假如 在相等优先权时由硬件决定(请求 0 优先于请求 1,依此类推) 。 ●独立的源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和 目标地址必须按数据传输宽度对齐。 ●支持循环的缓冲器管理 ●每个通道都有 3 个事件标志(DMA 半传输,DMA 传输完成和 DMA 传输出错),这 3 个 事件标志逻辑或成为一个单独的中断请求。 ●存储器和存储器间的传输 ●外设和存储器,存储器和外设的传输 ●闪存、SRAM、外设的 SRAM、APB1 APB2 和 AHB 外设均可作为访问的源和目标。 ●可编程的数据传输数目:最大为 65536 STM32F103ZET6 有两个 DMA 控制器,DMA1 和 DMA2,本章,我们仅针对 DMA1 进行 介绍。 从外设(TIMx、ADC、SPIx、I2Cx 和 USARTx)产生的 DMA 请求,通过逻辑或输入到 DMA 控制器,这就意味着同时只能有一个请求有效。外设的 DMA 请求,可以通过设置相应的 外设寄存器中的控制位,被独立地开启或关闭。 表 27.1.1 是 DMA1 各通道一览表: 表 27.1.1 DMA1 个通道一览表 370 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 这里解释一下上面说的逻辑或,例如通道 1 的几个 DMA1 请求(ADC1、TIM2_CH3、TIM4_CH1), 这几个是通过逻辑或到通道 1 的,这样我们在同一时间,就只能使用其中的一个。其他通道也 是类似的。 这里我们要使用的是串口 1 的 DMA 传送,也就是要用到通道 4。接下来,我们介绍一下 DMA 设置相关的几个寄存器。 第一个是 DMA 中断状态寄存器(DMA_ISR)。该寄存器的各位描述如图 27.1.1 所示: 图 27.1.1 DMA_ISR 寄存器各位描述 我们如果开启了 DMA_ISR 中这些中断,在达到条件后就会跳到中断服务函数里面去,即使 没开启,我们也可以通过查询这些位来获得当前 DMA 传输的状态。这里我们常用的是 TCIFx, 即通道 DMA 传输完成与否的标志。注意此寄存器为只读寄存器,所以在这些位被置位之后,只 能通过其他的操作来清除。 第二个是 DMA 中断标志清除寄存器(DMA_IFCR)。该寄存器的各位描述如图 27.1.2 所示: 371 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 27.1.2 DMA_IFCR 寄存器各位描述 DMA_IFCR 的各位就是用来清除 DMA_ISR 的对应位的,通过写 0 清除。在 DMA_ISR 被置位后, 我们必须通过向该位寄存器对应的位写入 0 来清除。 第三个是 DMA 通道 x 配置寄存器(DMA_CCRx)(x=1~7,下同)。该寄存器的我们在这里就不 贴出来了,见《STM32 参考手册》第 150 页 10.4.3 一节。该寄存器控制着 DMA 的很多相关信息, 包括数据宽度、外设及存储器的宽度、通道优先级、增量模式、传输方向、中断允许、使能等 都是通过该寄存器来设置的。所以 DMA_CCRx 是 DMA 传输的核心控制寄存器。 第四个是 DMA 通道 x 传输数据量寄存器(DMA_CNDTRx)。这个寄存器控制 DMA 通道 x 的每次 传输所要传输的数据量。其设置范围为 0~65535。并且该寄存器的值会随着传输的进行而减少, 当该寄存器的值为 0 的时候就代表此次数据传输已经全部发送完成了。所以可以通过这个寄存 器的值来知道当前 DMA 传输的进度。 第五个是 DMA 通道 x 的外设地址寄存器(DMA_CPARx)。该寄存器用来存储 STM32 外设的地 址,比如我们使用串口 1,那么该寄存器必须写入 0x40013804(其实就是&USART1_DR)。如果使 用其他外设,就修改成相应外设的地址就行了。 最后一个是 DMA 通道 x 的存储器地址寄存器(DMA_CMARx),该寄存器和 DMA_CPARx 差不多, 但是是用来放存储器的地址的。比如我们使用 SendBuf[5200]数组来做存储器,那么我们在 DMA_CMARx 中写入&SendBuff 就可以了。 DMA 相关寄存器就为大家介绍到这里,此节我们要用到串口 1 的发送,属于 DMA1 的通道 4 (表 27.1.1),接下来我们就介绍库函数下 DMA1 通道 4 的配置步骤: 1)使能 DMA 时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能 DMA 时钟 2)初始化 DMA 通道 4 参数 372 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 前面讲解过,DMA 通道配置参数种类比较繁多,包括内存地址,外设地址,传输数据长度, 数据宽度,通道优先级等等。这些参数的配置在库函数中都是在函数 DMA_Init 中完成,下面我 们看看函数定义: void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct) 函数的第一个参数是指定初始化的 DMA 通道号,这个很容易理解,下面我们主要看看第二 个参数。跟其他外设一样,同样是通过初始化结构体成员变量值来达到初始化的目的,下面我 们来看看 DMA_InitTypeDef 结构体的定义: typedef struct { uint32_t DMA_PeripheralBaseAddr; uint32_t DMA_MemoryBaseAddr; uint32_t DMA_DIR; uint32_t DMA_BufferSize; uint32_t DMA_PeripheralInc; uint32_t DMA_MemoryInc; uint32_t DMA_PeripheralDataSize; uint32_t DMA_MemoryDataSize; uint32_t DMA_Mode; uint32_t DMA_Priority; uint32_t DMA_M2M; }DMA_InitTypeDef; 这个结构体的成员比较多,但是每个成员变量的意义我们在前面基本都已经讲解过,这里 我们一一做个简要的介绍。 第一个参数 DMA_PeripheralBaseAddr 用来设置 DMA 传输的外设基地址,比如要进行串口 DMA 传输,那么外设基地址为串口接受发送数据存储器 USART1->DR 的地址,表示方法为 &USART1->DR。 第二个参数 DMA_MemoryBaseAddr 为内存基地址,也就是我们存放 DMA 传输数据的内存地址。 第三个参数 DMA_DIR 设置数据传输方向,决定是从外设读取数据到内存还送从内存读取数 据发送到外设,也就是外设是源地还是目的地,这里我们设置为从内存读取数据发送到串口, 所以外设自然就是目的地了,所以选择值为 DMA_DIR_PeripheralDST。 第四个参数 DMA_BufferSize 设置一次传输数据量的大小,这个很容易理解。 第五个参数 DMA_PeripheralInc 设置传输数据的时候外设地址是不变还是递增。如果设置 为递增,那么下一次传输的时候地址加 1,这里因为我们是一直往固定外设地址&USART1->DR 发送数据,所以地址不递增,值为 DMA_PeripheralInc_Disable; 第 六 个 参 数 DMA_MemoryInc 设 置 传 输 数 据 时 候 内 存 地 址 是 否 递 增 。 这 个 参 数 和 DMA_PeripheralInc 意思接近,只不过针对的是内存。这里我们的场景是将内存中连续存储单 元的数据发送到串口,毫无疑问内存地址是需要递增的,所以值为 DMA_MemoryInc_Enable。 第七个参数 DMA_PeripheralDataSize 用来设置外设的数据长度是为字节传输(8bits),半 字 传 输 (16bits) 还 是 字 传 输 (32bits) , 这 里 我 们 是 8 位 字 节 传 输 , 所 以 值 设 置 为 DMA_PeripheralDataSize_Byte。 第八个参数 DMA_MemoryDataSize 是用来设置内存的数据长度,和第七个参数意思接近,这 里我们同样设置为字节传输 DMA_MemoryDataSize_Byte。 373 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第九个参数 DMA_Mode 用来设置 DMA 模式是否循环采集,也就是说,比如我们要从内存中采 集 64 个字节发送到串口,如果设置为重复采集,那么它会在 64 个字节采集完成之后继续从内 存的第一个地址采集,如此循环。这里我们设置为一次连续采集完成之后不循环。所以设置值 为 DMA_Mode_Normal。在我们下面的实验中,如果设置此参数为循环采集,那么你会看到串口 不停的打印数据,不会中断,大家在实验中可以修改这个参数测试一下。 第十个参数是设置 DMA 通道的优先级,有低,中,高,超高三种模式,这个在前面讲解过, 这里我们设置优先级别为中级,所以值为 DMA_Priority_Medium。如果要开启多个通道,那么 这个值就非常有意义。 第 十 一 个 参 数 DMA_M2M 设 置 是 否 是 存 储 器 到 存 储 器 模 式 传 输 , 这 里 我 们 选 择 DMA_M2M_Disable。 这里我们给出上面场景的实例代码: DMA_InitTypeDef DMA_InitStructure; DMA_InitStructure.DMA_PeripheralBaseAddr = &USART1->DR; //DMA 外设 ADC 基地址 DMA_InitStructure.DMA_MemoryBaseAddr = cmar; //DMA 内存基地址 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; //从内存读取发送到外设 DMA_InitStructure.DMA_BufferSize = 64; //DMA 通道的 DMA 缓存的大小 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设地址不变 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址递增 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //8 位 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // 8 位 DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //工作在正常缓存模式 DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //DMA 通道 x 拥有中优先级 DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //非内存到内存传输 DMA_Init(DMA_CHx, &DMA_InitStructure); //根据指定的参数初始化 3)使能串口 DMA 发送 进行 DMA 配置之后,我们就要开启串口的 DMA 发送功能,使用的函数是: USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE); 如果是要使能串口 DMA 接受,那么第二个参数修改为 USART_DMAReq_Rx 即可。 4)使能 DMA1 通道 4,启动传输。 使能串口 DMA 发送之后,我们接着就要使能 DMA 传输通道: DMA_Cmd(DMA_CHx, ENABLE); 通过以上 3 步设置,我们就可以启动一次 USART1 的 DMA 传输了。 5)查询 DMA 传输状态 在 DMA 传输过程中,我们要查询 DMA 传输通道的状态,使用的函数是: FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG) 比如我们要查询 DMA 通道 4 传输是否完成,方法是: DMA_GetFlagStatus(DMA2_FLAG_TC4); 这里还有一个比较重要的函数就是获取当前剩余数据量大小的函数: uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx) 比如我们要获取 DMA 通道 4 还有多少个数据没有传输,方法是: DMA_GetCurrDataCounter(DMA1_Channel4); DMA 相关的库函数我们就讲解到这里,大家可以查看固件库中文手册详细了解。 374 27.2 硬件设计 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 所以本章用到的硬件资源有: 1) 指示灯 DS0 2) KEY0 按键 3) 串口 4) TFTLCD 模块 5) DMA 本章我们将利用外部按键 KEY0 来控制 DMA 的传送,每按一次 KEY0,DMA 就传送一次数据到 USART1,然后在 TFTLCD 模块上显示进度等信息。DS0 还是用来做为程序运行的指示灯。 本章实验需要注意 P6 口的 RXD 和 TXD 是否和 PA9 和 PA10 连接上,如果没有,请先连接。 27.3 软件设计 打开我们的 DMA 传输实验,可以发现,我们的实验中多了 dma.c 文件和其头文件 dma.h, 同时我们要引入 dma 相关的库函数文件 stm32f10x_dma.c 和 stm32f10x_dma.h。 打开 dma.c 文件,代码如下: #include "dma.h" DMA_InitTypeDef DMA_InitStructure; u16 DMA1_MEM_LEN;//保存 DMA 每次数据传送的长度 //DMA1 的各通道配置 //这里的传输形式是固定的,这点要根据不同的情况来修改 //从存储器->外设模式/8 位数据宽度/存储器增量模式 //DMA_CHx:DMA 通道 CHx //cpar:外设地址 //cmar:存储器地址 //cndtr:数据传输量 void MYDMA_Config(DMA_Channel_TypeDef* DMA_CHx,u32 cpar,u32 cmar,u16 cndtr) { RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能 DMA 时钟 DMA_DeInit(DMA_CHx); //将 DMA 的通道 1 寄存器重设为缺省值 DMA1_MEM_LEN=cndtr; DMA_InitStructure.DMA_PeripheralBaseAddr = cpar; //DMA 外设 ADC 基地址 DMA_InitStructure.DMA_MemoryBaseAddr = cmar; //DMA 内存基地址 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; //数据传输方向内存到外设 DMA_InitStructure.DMA_BufferSize = cndtr; //DMA 通道的 DMA 缓存的大小 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址不变 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//内存地址寄存器递增 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //数据宽度为 8 位 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //数据宽度 //为 8 位 DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //工作在正常缓存模式 DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //DM 通道拥有中优先级 375 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //非内存到内存传输 DMA_Init(DMA_CHx, &DMA_InitStructure); //初始化 DMA 的通道 } //开启一次 DMA 传输 void MYDMA_Enable(DMA_Channel_TypeDef*DMA_CHx) { DMA_Cmd(DMA_CHx, DISABLE ); //关闭 USART1 TX DMA1 所指示的通道 DMA_SetCurrDataCounter(DMA1_Channel4,DMA1_MEM_LEN);//设置 DMA 缓存的大小 DMA_Cmd(DMA_CHx, ENABLE); //使能 USART1 TX DMA1 所指示的通道 } 该部分代码仅仅 2 个函数,MYDMA_Config 函数,基本上就是按照我们上面介绍的步骤来初 始化 DMA 的,该函数在外部只能修改通道、源地址、目标地址和传输数据量等几个参数,更多 的其他设置只能在该函数内部修改。MYDMA_Enable 函数就是设置 DMA 缓存大小并且使能 DMA 通道。对照前面的配置步骤的详细讲解看看这部分代码即可。 最后我们看看 main 函数如下: #define SEND_BUF_SIZE 8200 u8 SendBuff[SEND_BUF_SIZE]; //发送数据缓冲区 const u8 TEXT_TO_SEND[]={"ALIENTEK WarShip STM32F1 DMA 串口实验"}; int main(void) { u16 i; u8 t=0; u8 j,mask=0; float pro=0;//进度 delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组 2 uart_init(115200); //串口初始化为 115200 LED_Init(); //初始化与 LED 连接的硬件接口 LCD_Init(); //初始化 LCD KEY_Init(); //按键初始化 MYDMA_Config(DMA1_Channel4,(u32)&USART1->DR, (u32)SendBuff,SEND_BUF_SIZE); POINT_COLOR=RED;//设置字体为红色 LCD_ShowString(30,50,200,16,16,"WarShip STM32"); LCD_ShowString(30,70,200,16,16,"DMA TEST"); LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(30,110,200,16,16,"2015/1/15"); LCD_ShowString(30,130,200,16,16,"KEY0:Start"); //显示提示信息 j=sizeof(TEXT_TO_SEND); for(i=0;i=j)//加入换行符 ALIENTEK 战舰 STM32F103 V3 开发板教程 { if(mask) { SendBuff[i]=0x0a; t=0; }else { SendBuff[i]=0x0d; mask++; } }else//复制 TEXT_TO_SEND 语句 { mask=0; SendBuff[i]=TEXT_TO_SEND[t]; t++; } } POINT_COLOR=BLUE;//设置字体为蓝色 i=0; while(1) { t=KEY_Scan(0); if(t==KEY0_PRES)//KEY0 按下 { LCD_ShowString(30,150,200,16,16,"Start Transimit...."); LCD_ShowString(30,170,200,16,16," %");//显示百分号 printf("\r\nDMA DATA:\r\n"); USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE); //使能串口 1 的 DMA 发送 MYDMA_Enable(DMA1_Channel4);//开始一次 DMA 传输! //等待 DMA 传输完成,此时我们来做另外一些事,点灯 //实际应用中,传输数据期间,可以执行另外的任务 while(1) { if(DMA_GetFlagStatus(DMA1_FLAG_TC4)!=RESET)//判断通道 4 传输完成 { DMA_ClearFlag(DMA1_FLAG_TC4);//清除通道 4 传输完成标志 break; } pro=DMA_GetCurrDataCounter(DMA1_Channel4);//得到当前剩余数据量 pro=1-pro/SEND_BUF_SIZE;//得到百分比 pro*=100; //扩大 100 倍 377 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 LCD_ShowNum(30,170,pro,3,16); } LCD_ShowNum(30,170,100,3,16);//显示 100% LCD_ShowString(30,150,200,16,16,"Transimit Finished!");//提示传送完成 } i++; delay_ms(10); if(i==20) { LED0=!LED0;//提示系统正在运行 i=0; } } } main 函数的流程大致是:先初始化内存 SendBuff 的值,然后通过 KEY0 开启串口 DMA 发送, 在发送过程中,通过 DMA_GetCurrDataCounter()函数获取当前还剩余的数据量来计算传输百分 比,最后在传输结束之后清除相应标志位,提示已经传输完成。这里还有一点要注意,因为是 使用的串口 1 DMA 发送,所以代码中使用 USART_DMACmd 函数开启串口的 DMA 发送: USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE); //使能串口 1 的 DMA 发送 至此,DMA 串口传输的软件设计就完成了。 27.4 下载验证 在代码编译成功之后,我们通过串口下载代码到 ALIENTEK 战舰 STM32 开发板上,可以 看到 LCD 显示如图 27.4.1 所示: 图 27.4.1 DMA 串口实验实物测试图 伴随 DS0 的不停闪烁,提示程序在运行。我们打开串口调试助手,然后按 KEY0,可以看 到串口显示如图 27.4.2 所示的内容: 378 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 27.4.2 串口收到的数据内容 可以看到串口收到了战舰 STM32F103 发送过来的数据,同时可以看到 TFTLCD 上显示了进 度等信息,如图 27.4.3 所示: 27.4.3 DMA 串口数据传输中 至此,我们整个 DMA 串口实验就结束了,希望大家通过本章的学习,掌握 STM32 的 DMA 使用。DMA 是个非常好的功能,它不但能减轻 CPU 负担,还能提高数据传输速度,合理的应 用 DMA,往往能让你的程序设计变得简单。 379 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第二十八章 IIC 实验 本章我们将向大家介绍如何利用 STM32F1 的普通 IO 口模拟 IIC 时序,并实现和 24C02 之 间的双向通信。在本章中,我们将利用 STM32F1 的普通 IO 口模拟 IIC 时序,来实现 24C02 的 读写,并将结果显示在 TFTLCD 模块上。本章分为如下几个部分: 28.1 IIC 简介 28.2 硬件设计 28.3 软件设计 28.4 下载验证 380 28.1 IIC 简介 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 IIC(Inter-Integrated Circuit)总线是一种由 PHILIPS 公司开发的两线式串行总线,用于连接 微控制器及其外围设备。它是由数据线 SDA 和时钟 SCL 构成的串行总线,可发送和接收数据。 在 CPU 与被控 IC 之间、IC 与 IC 之间进行双向传送,高速 IIC 总线一般可达 400kbps 以上。 I2C 总线在传送数据过程中共有三种类型信号, 它们分别是:开始信号、结束信号和应答 信号。 开始信号:SCL 为高电平时,SDA 由高电平向低电平跳变,开始传送数据。 结束信号:SCL 为高电平时,SDA 由低电平向高电平跳变,结束传送数据。 应答信号:接收数据的 IC 在接收到 8bit 数据后,向发送数据的 IC 发出特定的低电平脉冲, 表示已收到数据。CPU 向受控单元发出一个信号后,等待受控单元发出一个应答信号,CPU 接 收到应答信号后,根据实际情况作出是否继续传递信号的判断。若未收到应答信号,由判断为 受控单元出现故障。 这些信号中,起始信号是必需的,结束信号和应答信号,都可以不要。IIC 总线时序图如 图 28.1.1 所示: 图 28.1.1 IIC 总线时序图 ALIENTEK 战舰 STM32 开发板板载的 EEPROM 芯片型号为 24C02。该芯片的总容量是 256 个字节,该芯片通过 IIC 总线与外部连接,我们本章就通过 STM32 来实现 24C02 的读写。 目前大部分 MCU 都带有 IIC 总线接口,STM32 也不例外。但是这里我们不使用 STM32 的硬件 IIC 来读写 24C02,而是通过软件模拟。STM32 的硬件 IIC 非常复杂,更重要的是不稳 定,故不推荐使用。所以我们这里就通过模拟来实现了。有兴趣的读者可以研究一下 STM32 的硬件 IIC。 本章实验功能简介:开机的时候先检测 24C02 是否存在,然后在主循环里面用 1 个按键 (KEY0)用来执行写入 24C02 的操作,另外一个按键(WK_UP)用来执行读出操作,在 TFTLCD 模块上显示相关信息。同时用 DS0 提示程序正在运行。 28.2 硬件设计 本章需要用到的硬件资源有: 1) 指示灯 DS0 2) WK_UP 和 KEY1 按键 3) 串口(USMART 使用) 4) TFTLCD 模块 5) 24C02 381 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 前面 4 部分的资源,我们前面已经介绍了,请大家参考相关章节。这里只介绍 24C02 与 STM32 的连接,24C02 的 SCL 和 SDA 分别连在 STM32 的 PB6 和 PB7 上的,连接关系如图 28.2.1 所示: 图 28.2.1 STM32 与 24C02 连接图 28.3 软件设计 打开 IIC 实验工程,我们可以看到工程中加入了两个源文件分别是 myiic.c 和 24cxx.c, myiic.c 文件存放 iic 驱动代码,24cxx.c 文件存放 24C02 驱动代码: 打开 myiic.c 文件,代码如下: #include "myiic.h" #include "delay.h" //初始化 IIC void IIC_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE );//PB 时钟使能 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(GPIOB, &GPIO_InitStructure); //初始化 GPIO GPIO_SetBits(GPIOB,GPIO_Pin_6|GPIO_Pin_7); //PB6,PB7 输出高 } //产生 IIC 起始信号 void IIC_Start(void) { SDA_OUT(); //sda 线输出 IIC_SDA=1; IIC_SCL=1; delay_us(4); IIC_SDA=0; //START:when CLK is high,DATA change form high to low delay_us(4); 382 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 IIC_SCL=0; //钳住 I2C 总线,准备发送或接收数据 } //产生 IIC 停止信号 void IIC_Stop(void) { SDA_OUT(); //sda 线输出 IIC_SCL=0; IIC_SDA=0; //STOP:when CLK is high DATA change form low to high delay_us(4); IIC_SCL=1; IIC_SDA=1; //发送 I2C 总线结束信号 delay_us(4); } //等待应答信号到来 //返回值:1,接收应答失败 // 0,接收应答成功 u8 IIC_Wait_Ack(void) { u8 ucErrTime=0; SDA_IN(); //SDA 设置为输入 IIC_SDA=1;delay_us(1); IIC_SCL=1;delay_us(1); while(READ_SDA) { ucErrTime++; if(ucErrTime>250) { IIC_Stop(); return 1; } } IIC_SCL=0; //时钟输出 0 return 0; } //产生 ACK 应答 void IIC_Ack(void) { IIC_SCL=0; SDA_OUT(); IIC_SDA=0; delay_us(2); IIC_SCL=1; delay_us(2); IIC_SCL=0; } //不产生 ACK 应答 383 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 void IIC_NAck(void) { IIC_SCL=0; SDA_OUT(); IIC_SDA=1; delay_us(2); IIC_SCL=1; delay_us(2); IIC_SCL=0; } //IIC 发送一个字节 //返回从机有无应答 //1,有应答 //0,无应答 void IIC_Send_Byte(u8 txd) { u8 t; SDA_OUT(); IIC_SCL=0;//拉低时钟开始数据传输 for(t=0;t<8;t++) { IIC_SDA=(txd&0x80)>>7; txd<<=1; delay_us(2); //对 TEA5767 这三个延时都是必须的 IIC_SCL=1; delay_us(2); IIC_SCL=0; delay_us(2); } } //读 1 个字节,ack=1 时,发送 ACK,ack=0,发送 nACK u8 IIC_Read_Byte(unsigned char ack) { unsigned char i,receive=0; SDA_IN(); //SDA 设置为输入 for(i=0;i<8;i++ ) { IIC_SCL=0; delay_us(2); IIC_SCL=1; receive<<=1; if(READ_SDA)receive++; delay_us(1); } if (!ack) IIC_NAck(); //发送 nACK else IIC_Ack(); //发送 ACK 384 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 return receive; } 该部分为 IIC 驱动代码,实现包括 IIC 的初始化(IO 口)、IIC 开始、IIC 结束、ACK、IIC 读写等功能,在其他函数里面,只需要调用相关的 IIC 函数就可以和外部 IIC 器件通信了,这 里并不局限于 24C02,该段代码可以用在任何 IIC 设备上。 下面我们看看头文件 myiic.h 的代码,里面有两行代码为直接通过寄存器操作设置 IO 口的 模式为输入还是输出,代码如下: #define SDA_IN() {GPIOB->CRH&=0XFFFF0FFF;GPIOB->CRH|=8<<12;} #define SDA_OUT() {GPIOB->CRH&=0XFFFF0FFF;GPIOB->CRH|=3<<12;} 其他部分都是一些函数申明之类的,这里不做过多解释。 接下来我们看看 24cxx.c 文件代码: #include "24cxx.h" #include "delay.h" //初始化 IIC 接口 void AT24CXX_Init(void) { IIC_Init(); } //在 AT24CXX 指定地址读出一个数据 //ReadAddr:开始读数的地址 //返回值 :读到的数据 u8 AT24CXX_ReadOneByte(u16 ReadAddr) { u8 temp=0; IIC_Start(); if(EE_TYPE>AT24C16) { IIC_Send_Byte(0XA0); //发送写命令 IIC_Wait_Ack(); IIC_Send_Byte(ReadAddr>>8); }else IIC_Send_Byte(0XA0+((ReadAddr/256)<<1)); //发送高地址 //发送器件地址 0XA0,写数据 IIC_Wait_Ack(); IIC_Send_Byte(ReadAddr%256); //发送低地址 IIC_Wait_Ack(); IIC_Start(); IIC_Send_Byte(0XA1); //进入接收模式 IIC_Wait_Ack(); temp=IIC_Read_Byte(0); IIC_Stop(); //产生一个停止条件 return temp; } //在 AT24CXX 指定地址写入一个数据 //WriteAddr :写入数据的目的地址 //DataToWrite:要写入的数据 385 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 void AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite) { IIC_Start(); if(EE_TYPE>AT24C16) { IIC_Send_Byte(0XA0); //发送写命令 IIC_Wait_Ack(); IIC_Send_Byte(WriteAddr>>8);//发送高地址 }else IIC_Send_Byte(0XA0+((WriteAddr/256)<<1)); //发送器件地址 0XA0,写数据 IIC_Wait_Ack(); IIC_Send_Byte(WriteAddr%256); //发送低地址 IIC_Wait_Ack(); IIC_Send_Byte(DataToWrite); //发送字节 IIC_Wait_Ack(); IIC_Stop(); //产生一个停止条件 delay_ms(10); } //在 AT24CXX 里面的指定地址开始写入长度为 Len 的数据 //该函数用于写入 16bit 或者 32bit 的数据. //WriteAddr :开始写入的地址 //DataToWrite:数据数组首地址 //Len :要写入数据的长度 2,4 void AT24CXX_WriteLenByte(u16 WriteAddr,u32 DataToWrite,u8 Len) { u8 t; for(t=0;t>(8*t))&0xff); } } //在 AT24CXX 里面的指定地址开始读出长度为 Len 的数据 //该函数用于读出 16bit 或者 32bit 的数据. //ReadAddr :开始读出的地址 //返回值 :数据 //Len :要读出数据的长度 2,4 u32 AT24CXX_ReadLenByte(u16 ReadAddr,u8 Len) { u8 t; u32 temp=0; for(t=0;tCR1&=0XFFC7; SPI2->CR1|=SPI_BaudRatePrescaler; //设置 SPI2 速度 SPI_Cmd(SPI2,ENABLE); } //SPIx 读写一个字节 //TxData:要写入的字节 //返回值:读取到的字节 u8 SPI2_ReadWriteByte(u8 TxData) { u8 retry=0; while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET) //等待发送区空 { retry++; if(retry>200)return 0; } SPI_I2S_SendData(SPI2, TxData); //通过外设 SPIx 发送一个数据 retry=0; while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET) //等待接收 //完一个 byte { retry++; if(retry>200)return 0; } return SPI_I2S_ReceiveData(SPI2); //返回通过 SPIx 最近接收的数据 } 此部分代码主要初始化 SPI,这里我们选择的是 SPI2,所以在 SPI2_Init 函数里面,其相关 的操作都是针对 SPI2 的,其初始化步骤和我们上面介绍步骤 1-5 一样,我们在代码中也使用了 ①~⑤标注。在初始化之后,我们就可以开始使用 SPI2 了,在 SPI2_Init 函数里面,把 SPI2 的 波 特 率 设置成 了 最 低( 36Mhz , 256 分 频 为 140.625KHz )。 在 外 部函数 里 面 ,我们 通 过 SPI2_SetSpeed 来设置 SPI2 的速度,而我们的数据发送和接收则是通过 SPI2_ReadWriteByte 函 397 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 数来实现的。SPI2_SetSpeed 函数我们是通过寄存器设置方式来实现的,因为固件库并没有提供 单独的设置分频系数的函数 ,当然,我们也可以勉强的调用 SPI_Init 初始化函数来实现分频系 数修改。要读懂这段代码,可以直接查找中文参考手册中 SPI 章节的寄存器 CR1 的描述即可。 这里特别注意,SPI 初始化函数的最后有一个启动传输,这句话最大的作用就是维持 MOSI 为高电平,而且这句话也不是必须的,可以去掉。 下面我们打开 w25qxx.c,里面编写的是与 W25Q128 操作相关的代码,由于篇幅所限,详 细代码,这里就不贴出了。我们仅介绍几个重要的函数,首先是 W25QXX_Read 函数,该函数 用于从 W25Q128 的指定地址读出指定长度的数据。其代码如下: //读取 SPI FLASH //在指定地址开始读取指定长度的数据 //pBuffer:数据存储区 //ReadAddr:开始读取的地址(24bit) //NumByteToRead:要读取的字节数(最大 65535) void W25QXX_Read (u8* pBuffer,u32 ReadAddr,u16 NumByteToRead) { u16 i; SPI_FLASH_CS=0; SPI2_ReadWriteByte(W25X_ReadData); SPI2_ReadWriteByte((u8)((ReadAddr)>>16)); //使能器件 //发送读取命令 //发送 24bit 地址 SPI2_ReadWriteByte((u8)((ReadAddr)>>8)); SPI2_ReadWriteByte((u8)ReadAddr); for(i=0;i4096)secremain=4096;//下一个扇区还是写不完 else secremain=NumByteToWrite; //下一个扇区可以写完了 } }; } 该函数可以在 W25Q128 的任意地址开始写入任意长度(必须不超过 W25Q128 的容量)的 数据。我们这里简单介绍一下思路:先获得首地址(WriteAddr)所在的扇区,并计算在扇区内 的偏移,然后判断要写入的数据长度是否超过本扇区所剩下的长度,如果不超过,再先看看是 否要擦除,如果不要,则直接写入数据即可,如果要则读出整个扇区,在偏移处开始写入指定 长度的数据,然后擦除这个扇区,再一次性写入。当所需要写入的数据长度超过一个扇区的长 度的时候,我们先按照前面的步骤把扇区剩余部分写完,再在新扇区内执行同样的操作,如此 循环,直到写入结束。 399 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 其他的代码就比较简单了,我们这里不介绍了。接着打开 w25qxx.h 文件可以看到,这里面 就定义了一些与 W25Q128 操作相关的命令(部分省略了),这些命令在 W25Q128 的数据手册 上都有详细的介绍,感兴趣的读者可以参考该数据手册,其他的就没啥好说的了。。最后,我们 看看 main.c 里面代码如下: //要写入到 W25Q64 的字符串数组 const u8 TEXT_Buffer[]={"WarShipSTM32 SPI TEST"}; #define SIZE sizeof(TEXT_Buffer) int main(void) { u8 key; u16 i=0; u8 datatemp[SIZE]; u32 FLASH_SIZE; delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组 2 uart_init(115200); //串口初始化为 115200 LED_Init(); //初始化与 LED 连接的硬件接口 LCD_Init(); //初始化 LCD KEY_Init(); //按键初始化 W25QXX_Init(); //W25QXX 初始化 POINT_COLOR=RED;//设置字体为红色 LCD_ShowString(30,50,200,16,16,"WarShip STM32"); LCD_ShowString(30,70,200,16,16,"SPI TEST"); LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(30,110,200,16,16,"2015/1/15"); LCD_ShowString(30,130,200,16,16,"KEY1:Write KEY0:Read"); //显示提示信息 while(W25QXX_ReadID()!=W25Q128)//检测不到 W25Q128 { LCD_ShowString(30,150,200,16,16,"W25Q128 Check Failed!"); delay_ms(500); LCD_ShowString(30,150,200,16,16,"Please Check! "); delay_ms(500); LED0=!LED0;//DS0 闪烁 } LCD_ShowString(30,150,200,16,16,"W25Q128 Ready!"); FLASH_SIZE=128*1024*1024; //FLASH 大小为 16M 字节 POINT_COLOR=BLUE;//设置字体为蓝色 while(1) { key=KEY_Scan(0); 400 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 if(key==KEY1_PRES) //KEY1 按下,写入 W25QXX { LCD_Fill(0,170,239,319,WHITE);//清除半屏 LCD_ShowString(30,170,200,16,16,"Start Write W25Q128...."); W25QXX_Write((u8*)TEXT_Buffer,FLASH_SIZE-100,SIZE); //从倒数第 100 个地址处开始,写入 SIZE 长度的数据 LCD_ShowString(30,170,200,16,16,"W25Q128 Write Finished!"); //传送完成 } if(key==KEY0_PRES) //KEY0 按下,读取字符串并显示 { LCD_ShowString(30,170,200,16,16,"Start Read W25Q128.... "); W25QXX_Read(datatemp,FLASH_SIZE-100,SIZE); //从倒数第 100 个地址处开始,读出 SIZE 个字节 LCD_ShowString(30,170,200,16,16,"The Data Readed Is: "); //提示传送完成 LCD_ShowString(30,190,200,16,16,datatemp);//显示读到的字符串 } i++; delay_ms(10); if(i==20) { LED0=!LED0;//提示系统正在运行 i=0; } } } 这部分代码和 IIC 实验那部分代码大同小异,我们就不多说了,实现的功能就和 IIC 差不 多,不过此次写入和读出的是 SPI FLASH,而不是 EEPROM。 29.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK 战舰 STM32 开发板上,通过先按 KEY1 按键写入数据,然后按 KEY0 读取数据,得到如图 29.4.1 所示: 401 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 29.4.1 程序运行效果图 伴随 DS0 的不停闪烁,提示程序在运行。程序在开机的时候会检测 W25Q128 是否存在, 如果不存在则会在 TFTLCD 模块上显示错误信息,同时 DS0 慢闪。大家可以把 PB14 和 GND 短接就可以看到报错了。 402 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第三十章 485 实验 本章我们将向大家介绍如何利用 STM32F1 的串口实现 485 通信(半双工)。在本章中,我 们将利用 STM32F1 的串口 2 来实现两块开发板之间的 485 通信,并将结果显示在 TFTLCD 模 块上。本章分为如下几个部分: 30.1 485 简介 30.2 硬件设计 30.3 软件设计 30.4 下载验证 403 30.1 485 简介 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 485(一般称作 RS485/EIA-485)是隶属于 OSI 模型物理层的电气特性规定为 2 线,半双工, 多点通信的标准。它的电气特性和 RS-232 大不一样。用缆线两端的电压差值来表示传递信号。 RS485 仅仅规定了接受端和发送端的电气特性。它没有规定或推荐任何数据协议。 RS485 的特点包括: 1) 接口电平低,不易损坏芯片。RS485 的电气特性:逻辑“1”以两线间的电压差为+(2~6)V 表示;逻辑“0”以两线间的电压差为-(2~6)V 表示。接口信号电平比 RS232 降低了, 不易损坏接口电路的芯片,且该电平与 TTL 电平兼容,可方便与 TTL 电路连接。 2) 传输速率高。10 米时,RS485 的数据最高传输速率可达 35Mbps,在 1200m 时,传输 速度可达 100Kbps。 3) 抗干扰能力强。RS485 接口是采用平衡驱动器和差分接收器的组合,抗共模干扰能力 增强,即抗噪声干扰性好。 4) 传输距离远,支持节点多。RS485 总线最长可以传输 1200m 以上(速率≤100Kbps) 一般最大支持 32 个节点,如果使用特制的 485 芯片,可以达到 128 个或者 256 个节点, 最大的可以支持到 400 个节点。 RS485 推荐使用在点对点网络中,线型,总线型,不能是星型,环型网络。理想情况下 RS485 需要 2 个匹配电阻,其阻值要求等于传输电缆的特性阻抗(一般为 120Ω)。没有特性阻抗的话, 当所有的设备都静止或者没有能量的时候就会产生噪声,而且线移需要双端的电压差。没有终 接电阻的话,会使得较快速的发送端产生多个数据信号的边缘,导致数据传输出错。485 推荐 的连接方式如图 30.1.1 所示: 图 30.1.1 RS485 连接 在上面的连接中,如果需要添加匹配电阻,我们一般在总线的起止端加入,也就是主机和 设备 4 上面各加一个 120Ω的匹配电阻。 由于 RS485 具有传输距离远、传输速度快、支持节点多和抗干扰能力更强等特点,所以 RS485 有很广泛的应用。 战舰 STM32 开发板采用 SP3485 作为收发器,该芯片支持 3.3V 供电,最大传输速度可达 10Mbps,支持多达 32 个节点,并且有输出短路保护。该芯片的框图如图 30.1.2 所示: 图 30.1.2 SP3485 框图 图中 A、B 总线接口,用于连接 485 总线。RO 是接收输出端,DI 是发送数据收入端,RE 是接收使能信号(低电平有效),DE 是发送使能信号(高电平有效)。 本章,我们通过该芯片连接 STM32 的串口 2,实现两个开发板之间的 485 通信。本章将实 404 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 现这样的功能:通过连接两个战舰 STM32 开发板的 RS485 接口,然后由 KEY0 控制发送,当 按下一个开发板的 KEY0 的时候,就发送 5 个数据给另外一个开发板,并在两个开发板上分别 显示发送的值和接收到的值。 本章,我们只需要配置好串口 2,就可以实现正常的 485 通信了,串口 2 的配置和串口 1 基本类似,只是串口的时钟来自 APB1,最大频率为 36Mhz。 30.2 硬件设计 本章要用到的硬件资源如下: 1) 指示灯 DS0 2) KEY0 按键 3) TFTLCD 模块 4) 串口 2 5) RS485 收发芯片 前面 3 个都有详细介绍,这里我们介绍 RS485 和串口 2 的连接关系,如图 30.2.1 所示: 图 30.2.1 STM32 与 SP3485 连接电路图 从上图可以看出:STM32F1 的串口 2 通过 P7 端口设置,连接到 SP3485,通过 STM32F1 的 PD7 控制 SP3485 的收发,当 PD7=0 的时候,为接收模式;当 PD7=1 的时候,为发送模式。 这里需要注意,RS485_RE 信号和 DM9000_RST 共用 PD7,所以他们也不可以同时使用,只能 分时复用。 另外,图中的 R19 和 R22 是两个偏置电阻,用来保证总线空闲时,A、B 之间的电压差都 会大于 200mV(逻辑 1)。从而避免因总线空闲时,A、B 压差不定,引起逻辑错乱,可能出 现的乱码。 然后,我们要设置好开发板上 P7 排针的连接,通过跳线帽将 PA2 和 PA3 分别连接到 485_RX 和 485_TX 上面,如图 30.2.2 所示: 图 30.2.2 硬件连接示意图 最后,我们用 2 根导线将两个开发板 RS485 端子的 A 和 A,B 和 B 连接起来。这里注意不 要接反了(A 接 B),接反了会导致通讯异常!! 405 30.3 软件设计 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 打开我们的 485 实验例程,可以发现项目中加入了一个 rs485.c 文件以及其头文件 rs485 文 件,同时 485 通信调用的库函数和定义分布在 stm32f10x_usart.c 文件和头文件 stm32f10x_usart.h 文件中。 打开 rs485.c 文件,代码如下: #include "sys.h" #include "rs485.h" #include "delay.h" #ifdef EN_USART2_RX //如果使能了接收 //接收缓存区 u8 RS485_RX_BUF[64]; //接收到的数据长度 //接收缓冲,最大 64 个字节. u8 RS485_RX_CNT=0; void USART2_IRQHandler(void) { u8 res; if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) //接收到数据 { res =USART_ReceiveData(USART2); //读取接收到的数据 if(RS485_RX_CNT<64) { RS485_RX_BUF[RS485_RX_CNT]=res; RS485_RX_CNT++; //记录接收到的值 //接收数据增加 1 } } } #endif //初始化 IO 串口 2 //bound:波特率 void RS485_Init(u32 bound) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA| RCC_APB2Periph_GPIOG, ENABLE); //使能 GPIOA,G 时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE);//使能串口 2 时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOG, &GPIO_InitStructure); //PG9 端口配置 //推挽输出 406 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //PA2 //复用推挽 GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;//PA3 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入 GPIO_Init(GPIOA, &GPIO_InitStructure); RCC_APB1PeriphResetCmd(RCC_APB1Periph_USART2,ENABLE); //复位串口 2 RCC_APB1PeriphResetCmd(RCC_APB1Periph_USART2,DISABLE);//停止复位 #ifdef EN_USART2_RX //如果使能了接收 USART_InitStructure.USART_BaudRate = bound; //波特率设置; USART_InitStructure.USART_WordLength = USART_WordLength_8b;//8 位数据长度 USART_InitStructure.USART_StopBits = USART_StopBits_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_Init(USART2, &USART_InitStructure); //初始化串口 NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn; //使能串口 2 中断 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3; //先占优先级 2 级 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //从优先级 2 级 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道 NVIC_Init(&NVIC_InitStructure); //初始化 NVIC 寄存器 USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); //开启中断 USART_Cmd(USART2, ENABLE); //使能串口 #endif RS485_TX_EN=0; //默认为接收模式 } //RS485 发送 len 个字节. //buf:发送区首地址 //len:发送的字节数(为了和本代码的接收匹配,这里建议不要超过 64 个字节) void RS485_Send_Data(u8 *buf,u8 len) { u8 t; RS485_TX_EN=1; //设置为发送模式 for(t=0;t5)key=5;//最大是 5 个数据. for(i=0;i=0xfff)return 1; return 0; } //can 口接收数据查询 //buf:数据缓存区; //返回值:0,无数据被收到; // 其他,接收的数据长度; u8 Can_Receive_Msg(u8 *buf) { u32 i; CanRxMsg RxMessage; if( CAN_MessagePending(CAN1,CAN_FIFO0)==0)return 0;//没有接收到数据,直接退出 CAN_Receive(CAN1, CAN_FIFO0, &RxMessage); //读取数据 for(i=0;i<8;i++) buf[i]=RxMessage.Data[i]; return RxMessage.DLC; } 此部分代码总共 3 个函数,首先是 CAN_Mode_Init 函数。该函数用于 CAN 的初始化,该 函数带有 5 个参数,可以设置 CAN 通信的波特率和工作模式等,在该函数中,我们就是按 31.1 节末尾的介绍来初始化的,本章中,我们设计滤波器组 0 工作在 32 位标识符屏蔽模式,从设计 值可以看出,该滤波器是不会对任何标识符进行过滤的,因为所有的标识符位都被设置成不需 要关心,这样设计,主要是方便大家实验。 第二个函数,Can_Send_Msg 函数。该函数用于 CAN 报文的发送,主要是设置标识符 ID 等信息,写入数据长度和数据,并请求发送,实现一次报文的发送。 第三个函数,Can_Receive_Msg 函数。用来接受数据并且将接受到的数据存放到 buf 中。 can.c 里面,还包含了中断接收的配置,通过 can.h 的 CAN_RX0_INT_ENABLE 宏定义, 来配置是否使能中断接收,本章我们不开启中断接收的。其他函数我们就不一一介绍了,都比 较简单,大家自行理解即可。 最后,我们来看看 main.c 文件的内容: int main(void) 435 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 { u8 key; u8 i=0,t=0; u8 cnt=0; u8 canbuf[8]; u8 res; u8 mode=CAN_Mode_LoopBack;//CAN 工作模式;CAN_Mode_Normal(0): //普通模式,CAN_Mode_LoopBack(1):环回模式 delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置 NVIC 中断分组 2 uart_init(115200); //串口初始化波特率为 115200 LED_Init(); //初始化与 LED 连接的硬件接口 LCD_Init(); //初始化 LCD KEY_Init(); //按键初始化 CAN_Mode_Init(CAN_SJW_1tq,CAN_BS2_8tq,CAN_BS1_7tq,5, CAN_Mode_LoopBack);//CAN 初始化环回模式,波特率 450Kbps POINT_COLOR=RED; //设置字体为红色 LCD_ShowString(60,50,200,16,16,"WarShip STM32"); LCD_ShowString(60,70,200,16,16,"CAN TEST"); LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(30,110,200,16,16,"2015/1/15"); LCD_ShowString(60,130,200,16,16,"LoopBack Mode"); LCD_ShowString(60,150,200,16,16,"KEY0:Send WK_UP:Mode");//显示提示信息 POINT_COLOR=BLUE; //设置字体为蓝色 LCD_ShowString(60,170,200,16,16,"Count:"); //显示当前计数值 LCD_ShowString(60,190,200,16,16,"Send Data:"); //提示发送的数据 LCD_ShowString(60,250,200,16,16,"Receive Data:"); //提示接收到的数据 while(1) { key=KEY_Scan(0); if(key== KEY0_PRES) //KEY0 按下,发送一次数据 { for(i=0;i<8;i++) { canbuf[i]=cnt+i; //填充发送缓冲区 if(i<4)LCD_ShowxNum(60+i*32,210,canbuf[i],3,16,0X80); //显示数据 else LCD_ShowxNum(60+(i-4)*32,230,canbuf[i],3,16,0X80); //显示数据 } res=Can_Send_Msg(canbuf,8); if(res)LCD_ShowString(60+80,190,200,16,16,"Failed"); else LCD_ShowString(60+80,190,200,16,16,"OK "); //发送 8 个字节 //提示发送失败 //提示发送成功 436 STM32F1 开发指南(库函数版) }else if(key== WKUP_PRES) ALIENTEK 战舰 STM32F103 V3 开发板教程 //WK_UP 按下,改变 CAN 的工作模式 { mode=!mode; CAN_Mode_Init(CAN_SJW_1tq,CAN_BS2_8tq,CAN_BS1_7tq,5,mode); //CAN 普通模式初始化, 波特率 450Kbps POINT_COLOR=RED; //设置字体为红色 if(mode==0) //普通模式,需要 2 个开发板 { LCD_ShowString(60,130,200,16,16,"Nnormal Mode "); }else //回环模式,一个开发板就可以测试了. { LCD_ShowString(60,130,200,16,16,"LoopBack Mode"); } POINT_COLOR=BLUE; //设置字体为蓝色 } key=Can_Receive_Msg(canbuf); if(key) //接收到有数据 { LCD_Fill(60,270,130,310,WHITE);//清除之前的显示 for(i=0;i1.05||d1==0||d2==0)//不合格 { cnt=0; TP_Drow_Touch_Point(lcddev.width-20,lcddev.height-20,WHITE); //清除点 4 TP_Drow_Touch_Point(20,20,RED); //画点 1 TP_Adj_Info_Show(pos_temp[0][0],pos_temp[0][1],pos_temp[1] [0],pos_temp[1][1],pos_temp[2][0],pos_temp[2][1],pos_temp[3] [0],pos_temp[3][1],fac*100);//显示数据 continue; } tem1=abs(pos_temp[0][0]-pos_temp[2][0]);//x1-x3 tem2=abs(pos_temp[0][1]-pos_temp[2][1]);//y1-y3 tem1*=tem1; tem2*=tem2; d1=sqrt(tem1+tem2);//得到 1,3 的距离 tem1=abs(pos_temp[1][0]-pos_temp[3][0]);//x2-x4 tem2=abs(pos_temp[1][1]-pos_temp[3][1]);//y2-y4 tem1*=tem1; tem2*=tem2; d2=sqrt(tem1+tem2);//得到 2,4 的距离 fac=(float)d1/d2; if(fac<0.95||fac>1.05)//不合格 { cnt=0; TP_Drow_Touch_Point(lcddev.width-20,lcddev.height-20, WHITE); //清除点 4 TP_Drow_Touch_Point(20,20,RED); //画点 1 TP_Adj_Info_Show(pos_temp[0][0],pos_temp[0][1],pos_temp[1] [0],pos_temp[1][1],pos_temp[2][0],pos_temp[2][1],pos_temp[3] [0],pos_temp[3][1],fac*100);//显示数据 continue; }//正确了 //对角线相等 449 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 tem1=abs(pos_temp[1][0]-pos_temp[2][0]);//x1-x3 tem2=abs(pos_temp[1][1]-pos_temp[2][1]);//y1-y3 tem1*=tem1; tem2*=tem2; d1=sqrt(tem1+tem2);//得到 1,4 的距离 tem1=abs(pos_temp[0][0]-pos_temp[3][0]);//x2-x4 tem2=abs(pos_temp[0][1]-pos_temp[3][1]);//y2-y4 tem1*=tem1; tem2*=tem2; d2=sqrt(tem1+tem2);//得到 2,3 的距离 fac=(float)d1/d2; if(fac<0.95||fac>1.05)//不合格 { cnt=0; TP_Drow_Touch_Point(lcddev.width-20,lcddev.height-20, WHITE); //清除点 4 TP_Drow_Touch_Point(20,20,RED);//画点 1 TP_Adj_Info_Show(pos_temp[0][0],pos_temp[0][1],pos_temp[1] [0],pos_temp[1][1],pos_temp[2][0],pos_temp[2][1],pos_temp[3] [0],pos_temp[3][1],fac*100);//显示数据 continue; }//正确了 //计算结果 tp_dev.xfac=(float)(lcddev.width-40)/(pos_temp[1][0]-pos_temp[0][0]); //得到 xfac tp_dev.xoff=(lcddev.width-tp_dev.xfac*(pos_temp[1][0]+pos_temp[0] [0]))/2;//得到 xoff tp_dev.yfac=(float)(lcddev.height-40)/(pos_temp[2][1]-pos_temp[0][1] );//得到 yfac tp_dev.yoff=(lcddev.height-tp_dev.yfac*(pos_temp[2][1]+pos_temp[0] [1]))/2;//得到 yoff if(abs(tp_dev.xfac)>2||abs(tp_dev.yfac)>2)//触屏和预设的相反了. { cnt=0; TP_Drow_Touch_Point(lcddev.width-20,lcddev.height-20,WHITE );//清除点 4 TP_Drow_Touch_Point(20,20,RED); //画点 1 LCD_ShowString(40,26,lcddev.width,lcddev.height,16,"TP Need readjust!"); tp_dev.touchtype=!tp_dev.touchtype;//修改触屏类型. if(tp_dev.touchtype)//X,Y 方向与屏幕相反 { CMD_RDX=0X90; 450 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 CMD_RDY=0XD0; }else //X,Y 方向与屏幕相同 { CMD_RDX=0XD0; CMD_RDY=0X90; } continue; } POINT_COLOR=BLUE; LCD_Clear(WHITE);//清屏 LCD_ShowString(35,110,lcddev.width,lcddev.height,16,"Touch Screen Adjust OK!");//校正完成 delay_ms(1000); TP_Save_Adjdata(); LCD_Clear(WHITE);//清屏 return;//校正完成 } } delay_ms(10); outtime++; if(outtime>1000) { TP_Get_Adjdata(); break; } } } TP_Adjust 是此部分最核心的代码,在这里,给大家介绍一下我们这里所使用的触摸屏校 正原理:我们传统的鼠标是一种相对定位系统,只和前一次鼠标的位置坐标有关。而触摸屏则 是一种绝对坐标系统,要选哪就直接点哪,与相对定位系统有着本质的区别。绝对坐标系统的 特点是每一次定位坐标与上一次定位坐标没有关系,每次触摸的数据通过校准转为屏幕上的坐 标,不管在什么情况下,触摸屏这套坐标在同一点的输出数据是稳定的。不过由于技术原理的 原因,并不能保证同一点触摸每一次采样数据相同,不能保证绝对坐标定位,点不准,这就是 触摸屏最怕出现的问题:漂移。对于性能质量好的触摸屏来说,漂移的情况出现并不是很严重。 所以很多应用触摸屏的系统启动后,进入应用程序前,先要执行校准程序。 通常应用程序中使 用的 LCD 坐标是以像素为单位的。比如说:左上角的坐标是一组非 0 的数值,比如(20,20), 而右下角的坐标为(220,300)。这些点的坐标都是以像素为单位的,而从触摸屏中读出的是点 的物理坐标,其坐标轴的方向、XY 值的比例因子、偏移量都与 LCD 坐标不同,所以,需要在 程序中把物理坐标首先转换为像素坐标,然后再赋给 POS 结构,达到坐标转换的目的。 校正思路:在了解了校正原理之后,我们可以得出下面的一个从物理坐标到像素坐标的转 换关系式: LCDx=xfac*Px+xoff; LCDy=yfac*Py+yoff; 451 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 其中(LCDx,LCDy)是在 LCD 上的像素坐标,(Px,Py)是从触摸屏读到的物理坐标。xfac, yfac 分别是 X 轴方向和 Y 轴方向的比例因子,而 xoff 和 yoff 则是这两个方向的偏移量。 这样我们只要事先在屏幕上面显示 4 个点(这四个点的坐标是已知的),分别按这四个点就 可以从触摸屏读到 4 个物理坐标,这样就可以通过待定系数法求出 xfac、yfac、xoff、yoff 这四 个参数。我们保存好这四个参数,在以后的使用中,我们把所有得到的物理坐标都按照这个关 系式来计算,得到的就是准确的屏幕坐标。达到了触摸屏校准的目的。 TP_Adjust 就 是 根 据 上 面 的 原 理 设 计 的 校 准 函 数 , 注 意 该 函 数 里 面 多 次 使 用 了 lcddev.width 和 lcddev.height,用于坐标设置,主要是为了兼容不同尺寸的 LCD(比如 320*240、 480*320 和 800*480 的屏都可以兼容)。 接下来看看触摸屏初始化函数:TP_Init,该函数根据 LCD 的 ID(即 lcddev.id)判别是电 阻屏还是电容屏,执行不同的初始化,该函数代码如下: //触摸屏初始化 //返回值:0,没有进行校准 // 1,进行过校准 u8 TP_Init(void) { if(lcddev.id==0X5510) //4.3 寸电容触摸屏 { if(GT9147_Init()==0) //是 GT9147 { tp_dev.scan=GT9147_Scan; //扫描函数指向 GT9147 触摸屏扫描 }else { OTT2001A_Init(); tp_dev.scan=OTT2001A_Scan; //扫描函数指向 OTT2001A 触摸屏扫描 } tp_dev.touchtype|=0X80; //电容屏 tp_dev.touchtype|=lcddev.dir&0X01;//横屏还是竖屏 return 0; }else if(lcddev.id==0X1963) //7 寸电容触摸屏 { FT5206_Init(); tp_dev.scan=FT5206_Scan; //扫描函数指向 GT9147 触摸屏扫描 tp_dev.touchtype|=0X80; //电容屏 tp_dev.touchtype|=lcddev.dir&0X01;//横屏还是竖屏 return 0; }else { GPIO_InitTypeDef GPIO_InitStructure; //注意,时钟使能之后,对 GPIO 的操作才有效 //所以上拉之前,必须使能时钟.才能实现真正的上拉输出 452 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOF, ENABLE); //使能 PB,PF 端口时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure);//B1 推挽输出 GPIO_SetBits(GPIOB,GPIO_Pin_1);//上拉 // PB1 端口配置 //推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(GPIOB, &GPIO_InitStructure);//B2 上拉输入 GPIO_SetBits(GPIOB,GPIO_Pin_2);//上拉 // PB2 端口配置 //上拉输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11|GPIO_Pin_9;// F9,PF11 端口配置 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOF, &GPIO_InitStructure);//PF9,PF11 推挽输出 GPIO_SetBits(GPIOF, GPIO_Pin_11|GPIO_Pin_9);//上拉 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; // PF10 端口配置 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入 GPIO_Init(GPIOF, &GPIO_InitStructure);//PF10 上拉输入 GPIO_SetBits(GPIOF,GPIO_Pin_10);//上拉 TP_Read_XY(&tp_dev.x[0],&tp_dev.y[0]);//第一次读取初始化 AT24CXX_Init(); //初始化 24CXX if(TP_Get_Adjdata())return 0;//已经校准 else //未校准? { LCD_Clear(WHITE); //清屏 TP_Adjust(); //屏幕校准 } TP_Get_Adjdata(); } return 1; } 该函数比较简单,重点说一下:tp_dev.scan,这个结构体函数指针,默认是指向 TP_Scan 的,如果是电阻屏则用默认的即可,如果是电容屏,则指向新的扫描函数 GT9147_Scan、 OTT2001A_Scan 或 FT5206_Scan(根据芯片 ID 判断到底指向那个),执行电容触摸屏的扫描函 数,这两个函数在后续会介绍。 接下来打开 touch.h 文件可以看到,除了一些函数申明和宏定义标识符之外,我们还定义 了一个非常重要的结构体_m_tp_dev,定义如下: 453 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 typedef struct { u8 (*init)(void); u8 (*scan)(u8); void (*adjust)(void); u16 x0; //初始化触摸屏控制器 //扫描触摸屏.0,屏幕扫描;1,物理坐标; //触摸屏校准 //原始坐标(第一次按下时的坐标) u16 y0; u16 x; //当前坐标(此次扫描时,触屏的坐标) u16 y; u8 sta; //笔的状态 :b7:按下 1/松开 0; //b6:0,没有按键按下;1,有按键按下. ////////////////////////触摸屏校准参数///////////////////////// float xfac; float yfac; short xoff; short yoff; //新增的参数,当触摸屏的左右上下完全颠倒时需要用到. //touchtype=0 的时候,适合左右为 X 坐标,上下为 Y 坐标的 TP. //touchtype=1 的时候,适合左右为 Y 坐标,上下为 X 坐标的 TP. u8 touchtype; }_m_tp_dev; extern _m_tp_dev tp_dev; //触屏控制器在 touch.c 里面定义 #endif 该结构体用于管理和记录触摸屏相关信息,在外部调用的时候,我们一般直接调用 tp_dev 的相关成员函数/变量屏即可达到需要的效果。这样种设计简化了接口,另外管理和维护也比较 方便,大家可以效仿一下。 ctiic.c 和 ctiic.h 是电容触摸屏的 IIC 接口部分代码,与第二十八章的 myiic.c 和 myiic.h 基本 一样,这里就不单独介绍了。接下来我们重点看看接下来看看 ott2001a.c 文件,代码如下: //向 OTT2001A 写入一次数据 //reg:起始寄存器地址 //buf:数据缓缓存区 //len:写数据长度 //返回值:0,成功;1,失败. u8 OTT2001A_WR_Reg(u16 reg,u8 *buf,u8 len) { u8 i; u8 ret=0; CT_IIC_Start(); CT_IIC_Send_Byte(OTT_CMD_WR); //发送写命令 CT_IIC_Wait_Ack(); CT_IIC_Send_Byte(reg>>8); //发送高 8 位地址 CT_IIC_Wait_Ack(); CT_IIC_Send_Byte(reg&0XFF); //发送低 8 位地址 454 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 CT_IIC_Wait_Ack(); for(i=0;i>8); //发送高 8 位地址 CT_IIC_Wait_Ack(); CT_IIC_Send_Byte(reg&0XFF); //发送低 8 位地址 CT_IIC_Wait_Ack(); CT_IIC_Start(); CT_IIC_Send_Byte(OTT_CMD_RD); //发送读命令 CT_IIC_Wait_Ack(); for(i=0;i240)t=10;//重新从 10 开始计数 return res; } 457 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 此部分总共 5 个函数,其中 OTT2001A_WR_Reg 和 OTT2001A_RD_Reg 分别用于读写 OTT2001A 芯片,这里特别注意寄存器地址是 16 位的,与 OTT2001A 手册介绍的是有出入的, 必须 16 位才能正常操作。另外,重点介绍下 OTT2001A_Scan 函数,OTT2001A_Scan 函数用 于扫描电容触摸屏是否有按键按下,由于我们不是用的中断方式来读取 OTT2001A 的数据的, 而是采用查询的方式,所以这里使用了一个静态变量来提高效率,当无触摸的时候,尽量减少 对 CPU 的占用,当有触摸的时候,又保证能迅速检测到。至于对 OTT2001A 数据的读取,则 完全是我们在上面介绍的方法,先读取手势 ID 寄存器(OTT_GSTID_REG),判断是不是有有 效数据,如果有,则读取,否则直接忽略,继续后面的处理。 其他的函数我们这里就不多介绍了,接下来看下 gt9147.c 里面的代码,这里我们仅介绍 GT9147_Init 和 GT9147_Scan 两个函数,代码如下: //初始化 GT9147 触摸屏 //返回值:0,初始化成功;1,初始化失败 u8 GT9147_Init(void) { u8 temp[5]; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOF, ENABLE);//使能 PF 端口时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOF, &GPIO_InitStructure);//PF11 推挽输出 GPIO_SetBits(GPIOF,GPIO_Pin_1);//上拉 // PF11 端口配置 //推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(GPIOF, &GPIO_InitStructure);//PF10 上拉输入 GPIO_SetBits(GPIOF,GPIO_Pin_10);//上拉 // PB2 端口配置 //上拉输入 CT_IIC_Init(); GT_RST=0; delay_ms(10); GT_RST=1; delay_ms(10); //初始化电容屏的 I2C 总线 //复位 //释放复位 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; // PB2 端口配置 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //下拉输入 GPIO_Init(GPIOF, &GPIO_InitStructure);//PF10 下拉输入 GPIO_ResetBits(GPIOF,GPIO_Pin_10);//下拉 delay_ms(100); GT9147_RD_Reg(GT_PID_REG,temp,4); //读取产品 ID 458 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 temp[4]=0; printf("CTP ID:%s\r\n",temp); //打印 ID if(strcmp((char*)temp,"9147")==0) //ID==9147 { temp[0]=0X02; GT9147_WR_Reg(GT_CTRL_REG,temp,1);//软复位 GT9147 GT9147_RD_Reg(GT_CFGS_REG,temp,1);//读取 GT_CFGS_REG 寄存器 if(temp[0]<0X60)//默认版本比较低,需要更新 flash 配置 { printf("Default Ver:%d\r\n",temp[0]); GT9147_Send_Cfg(1);//更新并保存配置 } delay_ms(10); temp[0]=0X00; GT9147_WR_Reg(GT_CTRL_REG,temp,1); //结束复位 return 0; } return 1; } const u16 GT9147_TPX_TBL[5]= {GT_TP1_REG,GT_TP2_REG,GT_TP3_REG,GT_TP4_REG,GT_TP5_REG}; //扫描触摸屏(采用查询方式) //mode:0,正常扫描. //返回值:当前触屏状态. //0,触屏无触摸;1,触屏有触摸 u8 GT9147_Scan(u8 mode) { u8 buf[4]; u8 i=0; u8 res=0; u8 temp; static u8 t=0;//控制查询间隔,从而降低 CPU 占用率 t++; if((t%10)==0||t<10)//空闲时,每进入 10 次 CTP_Scan 函数才检测 1 次,节省 CPU 使用率 { GT9147_RD_Reg(GT_GSTID_REG,&mode,1);//读取触摸点的状态 if((mode&0XF)&&((mode&0XF)<6)) { temp=0XFF<<(mode&0XF);//将点的个数转换为 1 的位数,匹配 tp_dev.sta 定义 tp_dev.sta=(~temp)|TP_PRES_DOWN|TP_CATH_PRES; for(i=0;i<5;i++) { if(tp_dev.sta&(1<240)t=10;//重新从 10 开始计数 return res; } 以上代码,GT9147_Init 用于初始化 GT9147,该函数通过读取 0X8140~0X8143 这 4 个寄存 器,并判断是否是:“9147”,来确定是不是 GT9147 芯片,在读取到正确的 ID 后,软复位 GT9147, 然后根据当前芯片版本号,确定是否需要更新配置,通过 GT9147_Send_Cfg 函数,发送配置信 息(一个数组),配置完后,结束软复位,即完成 GT9147 初始化。GT9147_Scan 函数,用于读 460 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 取触摸屏坐标数据,这个和前面的 OTT2001A_Scan 大同小异,大家看源码即可。另外,ft5206.c 和 ft5206.h 的代码,我们就不再介绍了,请大家参考光盘本例程源码。 最后我们打开 main.c 文件,这里就不全部贴出来了,仅介绍三个重要的函数: //5 个触控点的颜色(电容触摸屏用) const u16 POINT_COLOR_TBL[5]={RED,GREEN,BLUE,BROWN,GRED}; //电阻触摸屏测试函数 void rtp_test(void) { u8 key; u8 i=0; while(1) { key=KEY_Scan(0); tp_dev.scan(0); if(tp_dev.sta&TP_PRES_DOWN) //触摸屏被按下 { if(tp_dev.x[0](lcddev.width-24)&&tp_dev.y[0]<16)Load_Drow_Dialog(); else TP_Draw_Big_Point(tp_dev.x[0],tp_dev.y[0],RED); //画图 } }else delay_ms(10); //没有按键按下的时候 if(key==KEY0_PRES) //KEY0 按下,则执行校准程序 { LCD_Clear(WHITE);//清屏 TP_Adjust(); //屏幕校准 Load_Drow_Dialog(); } i++; if(i%20==0)LED0=!LED0; } } //电容触摸屏测试函数 void ctp_test(void) { u8 t=0; u8 i=0; u16 lastpos[5][2]; //最后一次的数据 while(1) { tp_dev.scan(0); for(t=0;t(lcddev.width-24)&&tp_dev.y[t]<16) { Load_Drow_Dialog();//清除 } } }else lastpos[t][0]=0XFFFF; } delay_ms(5);i++; if(i%20==0)LED0=!LED0; } } int main(void) { delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组 2 uart_init(115200); //串口初始化为 115200 LED_Init(); //初始化与 LED 连接的硬件接口 LCD_Init(); //初始化 LCD KEY_Init(); //按键初始化 tp_dev.init(); //触摸屏初始化 POINT_COLOR=RED;//设置字体为红色 LCD_ShowString(30,50,200,16,16,"WarShip STM32"); LCD_ShowString(30,70,200,16,16,"TOUCH TEST"); LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(30,110,200,16,16,"2015/1/15"); if(tp_dev.touchtype&0X80==0)//仅电阻屏显示校准提示信息,电容屏不提示 LCD_ShowString(30,130,200,16,16,"Press KEY0 to Adjust"); delay_ms(1500); Load_Drow_Dialog(); if(tp_dev.touchtype&0X80)ctp_test(); //电容屏测试 else rtp_test(); //电阻屏测试 } 462 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 下面分别介绍一下这三个函数。 rtp_test,该函数用于电阻触摸屏的测试,该函数代码比较简单,就是扫描按键和触摸屏, 如果触摸屏有按下,则在触摸屏上面划线,如果按中“RST”区域,则执行清屏。如果按键 KEY0 按下,则执行触摸屏校准。 ctp_test,该函数用于电容触摸屏的测试,由于我们采用 tp_dev.sta 来标记当前按下的触摸 屏点数,所以判断是否有电容触摸屏按下,也就是判断 tp_dev.sta 的最低 5 位,如果有数据, 则划线,如果没数据则忽略,且 5 个点划线的颜色各不一样,方便区分。另外,电容触摸屏不 需要校准,所以没有校准程序。 main 函数,则比较简单,初始化相关外设,然后根据触摸屏类型,去选择执行 ctp_test 还 是 rtp_test。 软件部分就介绍到这里,接下来看看下载验证。 32.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK 战舰 STM32F103 上,电阻触摸屏 测试如图 32.4.1 所示界面: 图 32.4.1 电阻触摸屏测试程序运行效果 图中我们在电阻屏上画了一些内容,右上角的 RST 可以用来清屏,点击该区域,即可清屏 重画。另外,按 KEY0 可以进入校准模式,如果发现触摸屏不准,则可以按 KEY0,进入校准, 重新校准一下,即可正常使用。 463 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 如果是电容触摸屏,测试界面如图 32.4.2 所示: 图 32.4.2 电容触摸屏测试界面 图中,同样输入了一些内容。电容屏支持多点触摸,每个点的颜色都不一样,图中的波浪 线就是三点触摸画出来的,最多可以 5 点触摸。注意:电容触摸屏支持:ALIENTEK 4.3 寸电 容触摸屏模块或者 ALIENTEK 新款 7 寸电容触摸屏模块(SSD1963+FT5206 方案),老款的 7 寸电容触摸屏模块(CPLD+GT811 方案)本例程不支持!! 同样,按右上角的 RST 标志,可以清屏。电容屏无需校准,所以按 KEY0 无效。KEY0 校 准仅对电阻屏有效。 464 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第三十三章 红外遥控实验 本章,我们将向大家介绍如何通过 STM32 来解码红外遥控器的信号。ALIENTK 战舰 STM32F103 标配了红外接收头和一个很小巧的红外遥控器。在本章中,我们将利用 STM32F1 的输入捕获功能,解码开发板标配的这个红外遥控器的编码信号,并将解码后的键值 TFTLCD 模块上显示出来。本章分为如下几个部分: 33.1 红外遥控简介 33.2 硬件设计 33.3 软件设计 33.4 下载验证 465 33.1 红外遥控简介 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 红外遥控是一种无线、非接触控制技术,具有抗干扰能力强,信息传输可靠,功耗低,成 本低,易实现等显著优点,被诸多电子设备特别是家用电器广泛采用,并越来越多的应用到计 算机系统中。 由于红外线遥控不具有像无线电遥控那样穿过障碍物去控制被控对象的能力,所以,在设 计红外线遥控器时,不必要像无线电遥控器那样,每套(发射器和接收器)要有不同的遥控频率 或编码(否则,就会隔墙控制或干扰邻居的家用电器),所以同类产品的红外线遥控器,可以有 相同的遥控频率或编码,而不会出现遥控信号“串门”的情况。这对于大批量生产以及在家用 电器上普及红外线遥控提供了极大的方面。由于红外线为不可见光,因此对环境影响很小,再 由红外光波动波长远小于无线电波的波长,所以红外线遥控不会影响其他家用电器,也不会影 响临近的无线电设备。 红外遥控的编码目前广泛使用的是:NEC Protocol 的 PWM(脉冲宽度调制)和 Philips RC-5 Protocol 的 PPM(脉冲位置调制)。ALIENTEK 战舰 STM32 开发板配套的遥控器使用 的是 NEC 协议,其特征如下: 1、8 位地址和 8 位指令长度; 2、地址和命令 2 次传输(确保可靠性) 3、PWM 脉冲位置调制,以发射红外载波的占空比代表“0”和“1”; 4、载波频率为 38Khz; 5、位时间为 1.125ms 或 2.25ms; NEC 码的位定义:一个脉冲对应 560us 的连续载波,一个逻辑 1 传输需要 2.25ms(560us 脉冲+1680us 低电平),一个逻辑 0 的传输需要 1.125ms(560us 脉冲+560us 低电平)。而遥控 接收头在收到脉冲的时候为低电平,在没有脉冲的时候为高电平,这样,我们在接收头端收到 的信号为:逻辑 1 应该是 560us 低+1680us 高,逻辑 0 应该是 560us 低+560us 高。 NEC 遥控指令的数据格式为:同步码头、地址码、地址反码、控制码、控制反码。同步码 由一个 9ms 的低电平和一个 4.5ms 的高电平组成,地址码、地址反码、控制码、控制反码均是 8 位数据格式。按照低位在前,高位在后的顺序发送。采用反码是为了增加传输的可靠性(可 用于校验)。 我们遥控器的按键“▽”按下时,从红外接收头端收到的波形如图 33.1.1 所示: 图 33.1.1 按键“▽”所对应的红外波形 从图 33.1.1 中可以看到,其地址码为 0,控制码为 168。可以看到在 100ms 之后,我们还 收到了几个脉冲,这是 NEC 码规定的连发码(由 9ms 低电平+2.5m 高电平+0.56ms 低电平 +97.94ms 高电平组成),如果在一帧数据发送完毕之后,按键仍然没有放开,则发射重复码, 即连发码,可以通过统计连发码的次数来标记按键按下的长短/次数。 第十五章我们曾经介绍过利用输入捕获来测量高电平的脉宽,本章解码红外遥控信号,刚 好可以利用输入捕获的这个功能来实现遥控解码。关于输入捕获的介绍,请参考第十五章的内 容。 466 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 33.2 硬件设计 本实验采用定时器的输入捕获功能实现红外解码,本章实验功能简介:开机在 LCD 上显示 一些信息之后,即进入等待红外触发,如过接收到正确的红外信号,则解码,并在 LCD 上显示 键值和所代表的意义,以及按键次数等信息。同样我们也是用 LED0 来指示程序正在运行。 所要用到的硬件资源如下: 1) 指示灯 DS0 2) TFTLCD 模块(带触摸屏) 3) 红外接收头 4) 红外遥控器 前两个,在之前的实例已经介绍过了,遥控器属于外部器件,遥控接收头在板子上,与 MCU 的连接原理图如 33.2.1 所示: 图 33.2.1 红外遥控接收头与 STM32 的连接电路图 红外遥控接收头连接在 STM32 的 PB9(TIM4_CH4)上。硬件上不需要变动,只要程序将 TIM4_CH4 设计为输入捕获,然后将收到的脉冲信号解码就可以了。开发板配套的红外遥控器 外观如图 33.2.2 所示: 图 33.2.2 红外遥控器 467 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 33.3 软件设计 打开我们光盘的红外遥控器实验工程,可以看到我们添加了 remote.c 和 remote.h 两个文件, 同 时 因 为 我 们 使 用 的 是 输 入 捕 获 , 所 以 还 用 到 库 函 数 stm32f10x_tim.c 和 头 文 件 stm32f10x_tim.h。 打开 remote.c 文件,代码如下: #include "remote.h" #include "delay.h" #include "usart.h" //红外遥控初始化 //设置 IO 以及定时器 4 的输入捕获 void Remote_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); //使能 PORTB 时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4,ENABLE); //TIM4 时钟使能 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_SetBits(GPIOB,GPIO_Pin_9); //PB9 输入 //上拉输入 //初始化 GPIOB.9 //GPIOB.9 输出高 TIM_TimeBaseStructure.TIM_Period = 10000; //设定计数器自动重装值 最大 10ms 溢出 TIM_TimeBaseStructure.TIM_Prescaler =(72-1); //预分频器,1M 的计数频率,1us TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM 向上计数模式 TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure); //根据指定的参数初始化 TIMx TIM_ICInitStructure.TIM_Channel = TIM_Channel_4; // 选择输入 IC4 映射到 TI4 上 TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;//上升沿捕获 TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //配置输入分频,不分频 TIM_ICInitStructure.TIM_ICFilter = 0x03;//IC4F=0011 8 个定时器时钟周期滤波 TIM_ICInit(TIM4, &TIM_ICInitStructure);//初始化定时器输入捕获通道 TIM_Cmd(TIM4,ENABLE ); //使能定时器 4 468 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 NVIC_InitStructure.NVIC_IRQChannel = TIM4_IRQn; //TIM3 中断 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //先占优先级 0 级 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //从优先级 3 级 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道被使能 NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化外设 NVIC 寄存器 TIM_ITConfig( TIM4,TIM_IT_Update|TIM_IT_CC4,ENABLE);//允许更新中断 , //允许 CC4IE 捕获中断 } //遥控器接收状态 //[7]:收到了引导码标志 //[6]:得到了一个按键的所有信息 //[5]:保留 //[4]:标记上升沿是否已经被捕获 //[3:0]:溢出计时器 u8 RmtSta=0; u16 Dval; //下降沿时计数器的值 u32 RmtRec=0; //红外接收到的数据 u8 RmtCnt=0; //按键按下的次数 //定时器 2 中断服务程序 void TIM4_IRQHandler(void) { if(TIM_GetITStatus(TIM4,TIM_IT_Update)!=RESET) { if(RmtSta&0x80)//上次有数据被接收到了 { RmtSta&=~0X10; //取消上升沿已经被捕获标记 if((RmtSta&0X0F)==0X00)RmtSta|=1<<6; //标记已经完成一次键值信息采集 if((RmtSta&0X0F)<14)RmtSta++; else { RmtSta&=~(1<<7);//清空引导标识 RmtSta&=0XF0; //清空计数器 } } } if(TIM_GetITStatus(TIM4,TIM_IT_CC4)!=RESET) { if(RDATA)//上升沿捕获 { TIM_OC4PolarityConfig(TIM4,TIM_ICPolarity_Falling); //下降沿捕获 TIM_SetCounter(TIM4,0); //清空定时器值 RmtSta|=0X10; //标记上升沿已经被捕获 }else //下降沿捕获 { Dval=TIM_GetCapture4(TIM4); //读取 CCR1 也可以清 CC1IF 标志位 469 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 TIM_OC4PolarityConfig(TIM4,TIM_ICPolarity_Rising); //上升沿捕获 if(RmtSta&0X10) //完成一次高电平捕获 { if(RmtSta&0X80)//接收到了引导码 { if(Dval>300&&Dval<800) //560 为标准值,560us { RmtRec<<=1; //左移一位. RmtRec|=0; //接收到 0 }else if(Dval>1400&&Dval<1800) //1680 为标准值,1680us { RmtRec<<=1; //左移一位. RmtRec|=1; //接收到 1 }else if(Dval>2200&&Dval<2600) //得到按键键值增加的信息 //2500 为标准值 2.5ms { RmtCnt++; //按键次数增加 1 次 RmtSta&=0XF0; //清空计时器 } }else if(Dval>4200&&Dval<4700) //4500 为标准值 4.5ms { RmtSta|=1<<7; RmtCnt=0; //标记成功接收到了引导码 //清除按键次数计数器 } } RmtSta&=~(1<<4); } } TIM_ClearITPendingBit (TIM4,TIM_IT_Update|TIM_IT_CC4); } //处理红外键盘 //返回值: // 0,没有任何按键按下 //其他,按下的按键键值. u8 Remote_Scan(void) { u8 sta=0; u8 t1,t2; if(RmtSta&(1<<6))//得到一个按键的所有信息了 { 470 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 t1=RmtRec>>24; //得到地址码 t2=(RmtRec>>16)&0xff; //得到地址反码 if((t1==(u8)~t2)&&t1==REMOTE_ID)//检验遥控识别码(ID)及地址 { t1=RmtRec>>8; t2=RmtRec; if(t1==(u8)~t2)sta=t1;//键值正确 } if((sta==0)||((RmtSta&0X80)==0))//按键数据错误/遥控已经没有按下了 { RmtSta&=~(1<<6);//清除接收到有效按键标识 RmtCnt=0; //清除按键次数计数器 } } return sta; } 该部分代码包含 3 个函数,首先是 Remote_Init 函数,该函数用于初始化 IO 口,并配置 TIM4_CH4 为输入捕获,并设置其相关参数, 这里的配置跟输入捕获实验的配置基本接近,大 家可以参考一下输入捕获实验的讲解。TIM4_IRQHandler 函数是 TIM4 的中断服务函数,在该 函数里面,实现对红外信号的高电平脉冲的捕获,同时根据我们之前简介的协议内容来解码 , 该函数用到几个全局变量,用于辅助解码,并存储解码结果。 这里简单介绍一下高电平捕获思路:首先输入捕获设置的是捕获上升沿,在上升沿捕获到 以后,立即设置输入捕获模式为捕获下降沿(以便捕获本次高电平),然后,清零定时器的计数 器值,并标记捕获到上升沿。当下降沿到来时,再次进入捕获中断服务函数,立即更改输入捕 获模式为捕获上升沿(以便捕获下一次高电平),然后处理此次捕获到的高电平。 最后是 Remote_Scan 函数,该函用来扫描解码结果,相当于我们的按键扫描,输入捕获解 码的红外数据,通过该函数传送给其他程序。 接下来打开 remote.h,该文件代码比较简单,宏定义的标识符 REMOTE_ID 就是我们开发 板配套的遥控器的识别码,对于其他遥控器可能不一样,只要修改这个为你所使用的遥控器的一 致就可以了。其他是一些函数的声明,我们不做讲解。下面我们看看 main.c 里面主函数代码: int main(void) { u8 key; u8 t=0; u8 *str=0; delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组 2 uart_init(115200); //串口初始化为 115200 LED_Init(); //LED 端口初始化 LCD_Init(); KEY_Init(); Remote_Init(); //红外接收初始化 471 STM32F1 开发指南(库函数版) POINT_COLOR=RED; ALIENTEK 战舰 STM32F103 V3 开发板教程 //设置字体为红色 LCD_ShowString(30,50,200,16,16,"WarShip STM32"); LCD_ShowString(30,70,200,16,16,"REMOTE TEST"); LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(30,110,200,16,16,"2015/1/15"); LCD_ShowString(30,130,200,16,16,"KEYVAL:"); LCD_ShowString(30,150,200,16,16,"KEYCNT:"); LCD_ShowString(30,170,200,16,16,"SYMBOL:"); while(1) { key=Remote_Scan(); if(key) { LCD_ShowNum(86,130,key,3,16); //显示键值 LCD_ShowNum(86,150,RmtCnt,3,16); //显示按键次数 switch(key) { case 0:str="ERROR";break; case 162:str="POWER";break; case 98:str="UP";break; case 2:str="PLAY";break; case 226:str="ALIENTEK";break; case 194:str="RIGHT";break; case 34:str="LEFT";break; case 224:str="VOL-";break; case 168:str="DOWN";break; case 144:str="VOL+";break; case 104:str="1";break; case 152:str="2";break; case 176:str="3";break; case 48:str="4";break; case 24:str="5";break; case 122:str="6";break; case 16:str="7";break; case 56:str="8";break; case 90:str="9";break; case 66:str="0";break; case 82:str="DELETE";break; } LCD_Fill(86,170,116+8*8,170+16,WHITE); //清楚之前的显示 LCD_ShowString(86,170,200,16,16,str); //显示 SYMBOL 472 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 }else delay_ms(10); t++; if(t==20) { t=0; LED0=!LED0; } } } main 函数代码比较简单,主要是通过 Remote_Scan 函数获得红外遥控输入的数据(键值), 然后显示在 LCD 上面。至此,我们的软件设计部分就结束了。 33.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK 战舰 STM32F103 上,可以看到 LCD 显示如图 33.4.1 所示的内容: 图 33.4.1 程序运行效果图 此时我们通过遥控器按下不同的按键,则可以看到 LCD 上显示了不同按键的键值以及按键 次数和对应的遥控器上的符号。如图 33.4.2 所示: 图 33.4.2 解码成功 473 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第三十四章 游戏手柄实验 相信 80 后小时候都有玩过 FC 游戏机(又称:红白机/小霸王游戏机),那是一代经典,给 我们的童年带了了无限乐趣。本章,我们将向大家介绍如何通过 STM32 来驱动 FC 游戏机手柄, 将 FC 游戏机的手柄作为战舰 STM32 开发板的输入设备(综合实验可以直接通过这个手柄来玩 FC 游戏)。 在本章中,我们将使用 STM32 驱动 FC 手柄,将手柄的按键键值等信息通过 TFTLCD 模 块显示出来。本章分为如下几个部分: 34.1 游戏手柄简介 34.2 硬件设计 34.3 软件设计 34.4 下载验证 474 34.1 游戏手柄简介 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 FC 游戏机曾今是一统天下(现在也还是很多人玩),红极一时,那时任天堂单是 FC 机的 主机的发售收入就超过全美国的电视台的收入的总和。本章,我们将使用 STM32 来驱动 FC 手柄,实现手柄控制信号的读取,我们先来了解一下 FC 手柄。 FC 手柄,大致可分为两种:一种手柄插口是 11 针的,一种是 9 针的。但 11 针的现在市 面上很少了(因为 11 针手柄是早期 FC 组装兼容机最主要的周边),现在几乎都是 9 针 FC 组 装手柄的天下,所以我们本章使用的是 9 针 FC 手柄,该手柄还有一个特点,就是可以直接和 DR9 的串口头对插!这样同开发板的连接就简单了。FC 手柄的外观如图 34.1.1 所示: 图 34.1.1 FC 手柄外观图 这种手柄一般有 10 个按键(实际是 8 个键值):上、下、左、右、Start、Select、A、B、A 连发、B 连发。这里的 A 和 A 连发是一个键值,而 B 和 B 连发也是一个键值,只是连发按键 当你一直按下的时候,会不停的发送(方便快速按键,比如发炮弹之类的功能)。 FC 手柄的控制电路,由 1 个 8 位并入串出的移位寄存器(CD4021),外加一个时基集成 电路(NE555,用于连发)构成。不过现在的手柄,为了节约成本,直接就在 PCB 上做绑定 了,所以你拆开手柄,一般是看不到里面有四四方方的 IC,而只有一个黑色的小点,所有电路 都集成到这个里面了,但是他们的控制和读取方法还是一样的。 9 针手柄的读取时序和接线图如图 34.1.2 所示: 475 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 34.1.2 FC 手柄读取时序和接线图 从上图可看出,读取手柄按键值的信息十分简单:先 Latch(锁存键值),然后就得到了第 一个按键值(A),之后在 Clock 的作用下,依次读取其他按键的键值,总共 8 个按键键值。 有了以上了解,我们就可以通过 STM32 的 IO 来驱动 FC 手柄了。 34.2 硬件设计 本实验采用 STM32 的 3 个普通 IO 连接 FC 手柄的 Clock、Data 和 Latch 信号,本章实验功 能简介:在主函数不停的查询手柄输入,一旦检测到输入信号,则在 LCD 模块上面显示键值和 对应的按键符号。同样我们也是用 LED0 来指示程序正在运行。 所要用到的硬件资源如下: 1) 指示灯 DS0 2) TFTLCD 模块 3) FC 手柄一个 前两个,在之前的实例已经介绍过了,FC 手柄属于外部器件。战舰 STM32 开发板板载了 一个 FC 手柄接口(COM3),其实就是一个 DR9 接头,FC 手柄接口和 COM3 公用一个接口, 通过开发板上的 K1 开关来选择,如图 34.2.1 所示: 图 34.2.1 COM3 功能选择示意图 476 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 当 K1 打到上面(JOYPAD)时,COM3 作为 FC 手柄接口,当 K1 打到下面(RS232)时, COM3 作为 RS232 串口。COM3 接口与 MCU 的连接原理图如 34.2.2 所示: 图 34.2.2 FC 手柄接头与 STM32 的连接电路图 图中,COM3 就是用来连接 FC 手柄的,该接头采用标准的 DR9 座,当 K1 开关打到上面 的时候,COM_TX 连接 U3_TX、COM_RX 连接 U3_RX,然后通过 P8,连接在 PB11 和 PB10 上面。所以要将 FC 手柄通过 COM3 连接在 STM32 上面,必须 K1 开关打到上面,并且 P8 需 要用跳线帽连接 PB10(TX)和 COM3_RX、PB11(RX)和 COM3_TX。 另外,图中的 D3 稳压二极管,是为了防止 COM3 做 RS232 使用时,高压烧坏 MCU 的 IO 口。 当 COM3 作为 FC 手柄接口(JOYPAD)时,COM_TX 是 LAT(Latch)信号,COM_RX 是 DAT(Data)信号,JOY_CLK 是 CLK(Clock)信号,分别连接在 STM32 的 PB11、PB10 和 PD3 上面,这里 JOY_CLK 和 OV_SCL 信号线共用 PD3,所以 FC 手柄和摄像头模块得分时 服用 PD3 才可以。 在设置好开发板的连接后(P8 跳线帽:PB10(TX)和 COM3_RX 连接、PB11(RX)和 COM3_TX 连接,K1 开关打 JOYPAD 位置),将 FC 手柄插入 COM3 插口即可。 477 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 34.3 软件设计 打开我们的游戏手柄实验工程,可以看到我们的工程中添加了 joypad.c 文件以及其头文件 joypad.h 文件。 打开 joypad.c 文件,代码如下: #include "joypad.h" //初始化手柄接口. void JOYPAD_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOD, ENABLE); //使能 PB,PD 端口时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); //初始化 GPIO GPIO_SetBits(GPIOB,GPIO_Pin_11); //上拉 // //推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOD, &GPIO_InitStructure); //初始化 GPIO GPIO_SetBits(GPIOD,GPIO_Pin_3); //上拉 // //推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//上拉输入 GPIO_Init(GPIOB, &GPIO_InitStructure); //初始化 GPIO GPIO_SetBits(GPIOB,GPIO_Pin_10); //上拉 } //读取手柄按键值. //FC 手柄数据输出格式: //每给一个脉冲,输出一位数据,输出顺序: //A->B->SELECT->START->UP->DOWN->LEFT->RIGHT. //总共 8 位,对于有 C 按钮的手柄,按下 C 其实就等于 A+B 同时按下. //按下是 0,松开是 1. //返回值: //[0]:右 [1]:左 [2]:下 [3]:上 [1]:Start [5]:Select [6]:B [7]:A u8 JOYPAD_Read(void) { vu8 temp=0; 478 STM32F1 开发指南(库函数版) vu8 dt=0; ALIENTEK 战舰 STM32F103 V3 开发板教程 //延时用,否则-O2 优化可能读不到手柄值 u8 t; JOYPAD_LAT=1; //锁存当前状态 dt++;dt++;dt++; JOYPAD_LAT=0; for(t=0;t<8;t++) { temp>>=1; if(JOYPAD_DAT==0)temp|=0x80;//LOAD 之后,就得到第一个数据 JOYPAD_CLK=1; //每给一次脉冲,收到一个数据 dt++;dt++;dt++; JOYPAD_CLK=0; dt++;dt++;dt++; } return temp; } 该部分代码仅 2 个函数,都比较简单,JOYPAD_Init 函数用于初始化 IO,即把 PB10、PB11 和 PD3 设置为正确的状态,以便同 FC 手柄通信。另外一个函数 JOYPAD_Read 就是按照图 34.1.2 所示的时序读取 FC 手柄,该函数的返回值就是手柄的状态,注意,JOYPAD_Read 函数里面的 dt++,完全是用于延时,当优化等级较高时(-O2),如果不加这个延时,可能导致读手柄数据 异常。 接下来打开 joypad.h 可以看到该文件里代码主要是定义位带操作实现 IO 控制,当然,你 也可以跟 LED 试验一样通过库函数设置。 最后我们看看 main.c 主函数代码: const u8*JOYPAD_SYMBOL_TBL[8]= {"Right","Left","Down","Up","Start","Select","A","B"};//手柄按键符号定义 int main(void) { u8 key; u8 t=0,i=0; delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组 2 uart_init(115200); //串口初始化为 115200 LED_Init(); //初始化与 LED 连接的硬件接口 LCD_Init(); //初始化 LCD JOYPAD_Init(); //手柄初始化 POINT_COLOR=RED;//设置字体为红色 LCD_ShowString(30,50,200,16,16,"WarShip STM32"); LCD_ShowString(30,70,200,16,16,"JOYPAD TEST"); LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(30,110,200,16,16,"2015/1/16"); LCD_ShowString(30,130,200,16,16,"KEYVAL:"); LCD_ShowString(30,150,200,16,16,"SYMBOL:"); 479 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 POINT_COLOR=BLUE;//设置字体为红色 while(1) { key=JOYPAD_Read(); if(key) { LCD_ShowNum(116,130,key,3,16);//显示键值 for(i=0;i<8;i++) { if(key&(0X80>>i)) { LCD_Fill(30+56,150,30+56+48,150+16,WHITE);//清除之前的显示 LCD_ShowString(30+56,150,200,16,16,(u8*)JOYPAD_SYMBOL_TBL[i]);//显示符号 } } } delay_ms(10); t++; if(t==20) { t=0; LED0=!LED0; } } } 此部分代码也比较简单,初始化 JOYPAD 之后,就一直扫描 FC 手柄(通过 JOYPAD_Read 函数实现),然后只要接收到手柄的有效信号,就在 LCD 模块上面显示出来。 至此,我们的软件设计部分就结束了。 34.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK 战舰 STM32 开发板上,可以看到 LCD 显示如图 34.4.1 所示的内容: 图 34.4.1 程序运行时 LCD 显示内容 480 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 此时我们按下 FC 手柄的按键,则可以看到 LCD 上显示了对应按键的键值以及对应的符号。 如图 34.4.2 所示: 图 34.4.2 解码成功 481 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第三十五章 DS18B20 数字温度传感器实验 STM32 虽然内部自带了温度传感器,但是因为芯片温升较大等问题,与实际温度差别较大, 所以,本章我们将向大家介绍如何通过 STM32 来读取外部数字温度传感器的温度,来得到较 为准确的环境温度。在本章中,我们将学习使用单总线技术,通过它来实现 STM32 和外部温 度传感器(DS18B20)的通信,并把从温度传感器得到的温度显示在 TFTLCD 模块上。本章分 为如下几个部分: 35.1 DS18B20 简介 35.2 硬件设计 35.3 软件设计 35.4 下载验证 482 35.1 DS18B20 简介 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 DS18B20 是由 DALLAS 半导体公司推出的一种的“一线总线”接口的温度传感器。与传 统的热敏电阻等测温元件相比,它是一种新型的体积小、适用电压宽、与微处理器接口简单的 数字化温度传感器。一线总线结构具有简洁且经济的特点,可使用户轻松地组建传感器网络, 从而为测量系统的构建引入全新概念,测量温度范围为-55~+125℃ ,精度为±0.5℃。现场温 度直接以“一线总线”的数字方式传输,大大提高了系统的抗干扰性。它能直接读出被测温度, 并且可根据实际要求通过简单的编程实现 9~l2 位的数字值读数方式。它工作在 3—5.5 V 的电 压范围,采用多种封装形式,从而使系统设计灵活、方便,设定分辨率及用户设定的报警温度 存储在 EEPROM 中,掉电后依然保存。其内部结构如图 35.1.1 所示: 图 35.1.1 DS18B20 内部结构图 ROM 中的 64 位序列号是出厂前被光记好的,它可以看作是该 DS18B20 的地址序列码,每 DS18B20 的 64 位序列号均不相同。64 位 ROM 的排列是:前 8 位是产品家族码,接着 48 位是 DS18B20 的序列号,最后 8 位是前面 56 位的循环冗余校验码(CRC=X8+X5 +X4 +1)。ROM 作 用是使每一个 DS18B20 都各不相同,这样就可实现一根总线上挂接多个。 所有的单总线器件要求采用严格的信号时序,以保证数据的完整性。DS18B20 共有 6 种信 号类型:复位脉冲、应答脉冲、写 0、写 1、读 0 和读 1。所有这些信号,除了应答脉冲以外, 都由主机发出同步信号。并且发送所有的命令和数据都是字节的低位在前。这里我们简单介绍 这几个信号的时序: 1)复位脉冲和应答脉冲 单总线上的所有通信都是以初始化序列开始。主机输出低电平,保持低电平时间至少 480 us,,以产生复位脉冲。接着主机释放总线,4.7K 的上拉电阻将单总线拉高,延时 15~60 us, 并进入接收模式(Rx)。接着 DS18B20 拉低总线 60~240 us,以产生低电平应答脉冲, 若为低电平,再延时 480 us。 2)写时序 写时序包括写 0 时序和写 1 时序。所有写时序至少需要 60us,且在 2 次独立的写时序之间 至少需要 1us 的恢复时间,两种写时序均起始于主机拉低总线。写 1 时序:主机输出低电平, 延时 2us,然后释放总线,延时 60us。写 0 时序:主机输出低电平,延时 60us,然后释放总线, 延时 2us。 3)读时序 单总线器件仅在主机发出读时序时,才向主机传输数据,所以,在主机发出读数据命令后, 必须马上产生读时序,以便从机能够传输数据。所有读时序至少需要 60us,且在 2 次独立的读 483 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 时序之间至少需要 1us 的恢复时间。每个读时序都由主机发起,至少拉低总线 1us。主机在读 时序期间必须释放总线,并且在时序起始后的 15us 之内采样总线状态。典型的读时序过程为: 主机输出低电平延时 2us,然后主机转入输入模式延时 12us,然后读取单总线当前的电平,然 后延时 50us。 在了解了单总线时序之后,我们来看看 DS18B20 的典型温度读取过程,DS18B20 的典型 温度读取过程为:复位发 SKIP ROM 命令(0XCC)发开始转换命令(0X44)延时复 位发送 SKIP ROM 命令(0XCC)发读存储器命令(0XBE)连续读出两个字节数据(即 温度)结束。 DS18B20 的介绍就到这里,更详细的介绍,请大家参考 DS18B20 的技术手册。 35.2 硬件设计 由于开发板上标准配置是没有 DS18B20 这个传感器的,只有接口,所以要做本章的实验, 大家必须找一个 DS18B20 插在预留的 18B20 接口上。 本章实验功能简介:开机的时候先检测是否有 DS18B20 存在,如果没有,则提示错误。 只有在检测到 DS18B20 之后才开始读取温度并显示在 LCD 上,如果发现了 DS18B20,则程 序每隔 100ms 左右读取一次数据,并把温度显示在 LCD 上。同样我们也是用 DS0 来指示程序 正在运行。 所要用到的硬件资源如下: 1) 指示灯 DS0 2) TFTLCD 模块 3) DS18B20 接口 4) DS18B20 温度传感器 前两部分,在之前的实例已经介绍过了,而DS18B20温度传感器属于外部器件(板上没有 直接焊接),这里也不介绍。本章,我们仅介绍DS18B20接口和STM32的连接电路,如图35.2.1 所示: 图 35.2.1 DS18B20 接口与 STM32 的连接电路图 从上图可以看出,我们使用的是 STM32 的 PG11 来连接 U13 的 DQ 引脚,图中 U13 为 DHT11 (数字温湿度传感器)和 DS18B20 共用的一个接口,DHT11 我们将在下一章介绍。 DS18B20 只用到 U6 的 3 个引脚(U6 的 1、2 和 3 脚),将 DS18B20 传感器插入到这个上 面就可以通过 STM32 来读取 DS18B20 的温度了。连接示意图如图 35.2.2 所示: 484 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 35.2.2 DS18B20 连接示意图 从上图可以看出,DS18B20 的平面部分(有字的那面)应该朝内,而曲面部分朝外。然后 插入如图所示的三个孔内。 35.3 软件设计 打开我们的 DS18B20 数字温度传感器实验工程可以看到我们添加了 ds18b20.c 文件以及其 头文件 ds18b20.h 文件,所有 ds18b20 驱动代码和相关定义都分布在这两个文件中。 打开 ds18b20.c,该文件代码如下: #include "ds18b20.h" #include "delay.h" //复位 DS18B20 void DS18B20_Rst(void) { DS18B20_IO_OUT(); DS18B20_DQ_OUT=0; delay_us(750); //SET PA0 OUTPUT //拉低 DQ //拉低 750us DS18B20_DQ_OUT=1; //DQ=1 delay_us(15); //15US } //等待 DS18B20 的回应 //返回 1:未检测到 DS18B20 的存在 //返回 0:存在 u8 DS18B20_Check(void) { u8 retry=0; DS18B20_IO_IN();//SET PA0 INPUT while (DS18B20_DQ_IN&&retry<200) { retry++; delay_us(1); }; if(retry>=200)return 1; else retry=0; while (!DS18B20_DQ_IN&&retry<240) { retry++; 485 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 delay_us(1); }; if(retry>=240)return 1; return 0; } //从 DS18B20 读取一个位 //返回值:1/0 u8 DS18B20_Read_Bit(void) // read one bit { u8 data; DS18B20_IO_OUT();//SET PA0 OUTPUT DS18B20_DQ_OUT=0; delay_us(2); DS18B20_DQ_OUT=1; DS18B20_IO_IN();//SET PA0 INPUT delay_us(12); if(DS18B20_DQ_IN)data=1; else data=0; delay_us(50); return data; } //从 DS18B20 读取一个字节 //返回值:读到的数据 u8 DS18B20_Read_Byte(void) // read one byte { u8 i,j,dat; dat=0; for (i=1;i<=8;i++) { j=DS18B20_Read_Bit(); dat=(j<<7)|(dat>>1); } return dat; } //写一个字节到 DS18B20 //dat:要写入的字节 void DS18B20_Write_Byte(u8 dat) { u8 j; u8 testb; DS18B20_IO_OUT();//SET PA0 OUTPUT; for (j=1;j<=8;j++) { 486 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 testb=dat&0x01; dat=dat>>1; if (testb) { DS18B20_DQ_OUT=0;// Write 1 delay_us(2); DS18B20_DQ_OUT=1; delay_us(60); } else { DS18B20_DQ_OUT=0;// Write 0 delay_us(60); DS18B20_DQ_OUT=1; delay_us(2); } } } //开始温度转换 void DS18B20_Start(void)// ds1820 start convert { DS18B20_Rst(); DS18B20_Check(); DS18B20_Write_Byte(0xcc);// skip rom DS18B20_Write_Byte(0x44);// convert } //初始化 DS18B20 的 IO 口 DQ 同时检测 DS 的存在 //返回 1:不存在 //返回 0:存在 u8 DS18B20_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOG, ENABLE); //使能 PG 口时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11; //PORTG.11 推挽输出 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOG, &GPIO_InitStructure); //初始化 GPIO GPIO_SetBits(GPIOG,GPIO_Pin_11); //输出 1 DS18B20_Rst(); return DS18B20_Check(); } //从 ds18b20 得到温度值 //精度:0.1C //返回值:温度值 (-550~1250) short DS18B20_Get_Temp(void) 487 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 { u8 temp; u8 TL,TH; short tem; DS18B20_Start (); // ds1820 start convert DS18B20_Rst(); DS18B20_Check(); DS18B20_Write_Byte(0xcc);// skip rom DS18B20_Write_Byte(0xbe);// convert TL=DS18B20_Read_Byte(); // LSB TH=DS18B20_Read_Byte(); // MSB if(TH>7) { TH=~TH; TL=~TL; temp=0; }else temp=1; tem=TH; //温度为负 //温度为正 //获得高八位 tem<<=8; tem+=TL; //获得底八位 tem=(float)tem*0.625; //转换 if(temp)return tem; //返回温度值 else return -tem; } 该部分代码就是根据我们前面介绍的单总线操作时序来读取 DS18B20 的温度值的,DS18B20 的温度通过 DS18B20_Get_Temp 函数读取,该函数的返回值为带符号的短整形数据,返回值的 范围为-550~1250,其实就是温度值扩大了 10 倍。 然后我们打开 ds18b20.h,该文件下面主要是一些 IO 口位带操作定义以及函数申明,没有 什么特别需要讲解的地方。最后打开 main.c,该文件代码如下: int main(void) { u8 t=0; short temperature; delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组 2 uart_init(115200); LED_Init(); LCD_Init(); //串口初始化为 115200 //初始化与 LED 连接的硬件接口 //初始化 LCD POINT_COLOR=RED; //设置字体为红色 LCD_ShowString(30,50,200,16,16,"WarShip STM32"); LCD_ShowString(30,70,200,16,16,"DS18B20 TEST"); LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK"); 488 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 LCD_ShowString(30,110,200,16,16,"2015/1/16"); while(DS18B20_Init()) //DS18B20 初始化 { LCD_ShowString(30,130,200,16,16,"DS18B20 Error"); delay_ms(200); LCD_Fill(30,130,239,130+16,WHITE); delay_ms(200); } LCD_ShowString(30,130,200,16,16,"DS18B20 OK"); POINT_COLOR=BLUE;//设置字体为蓝色 LCD_ShowString(30,150,200,16,16,"Temp: . C"); while(1) { if(t%10==0) //每 100ms 读取一次 { temperature=DS18B20_Get_Temp(); if(temperature<0) { LCD_ShowChar(30+40,150,'-',16,0); //显示负号 temperature=-temperature; //转为正数 }else LCD_ShowChar(30+40,150,' ',16,0); //去掉负号 LCD_ShowNum(30+40+8,150,temperature/10,2,16); //显示正数部分 LCD_ShowNum(30+40+32,150,temperature%10,1,16); //显示小数部分 } delay_ms(10); t++; if(t==20) { t=0; LED0=!LED0; } } } 主函数代码很简单,一系列初始化之后,就是每 100ms 读取一次 18B20 的值,然后转化为 温度后显示在 LCD 上。至此,我们本章的软件设计就结束了。 35.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK 战舰 STM32 开发板上,可以看到 LCD 显示开始显示当前的温度值(假定 DS18B20 已经接上去了),如图 35.4.1 所示: 489 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 35.4.1 DS18B20 读取到的温度值 该程序还可以读取并显示负温度值的,具备条件的读者可以测试一下。 490 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第三十六章 DHT11 数字温湿度传感器实验 上一章,我们介绍了数字温度传感器 DS18B20 的使用,本章我们将介绍数字温湿度传感器 DHT11 的使用,该传感器不但能测温度,还能测湿度。本章我们将向大家介绍如何使用 STM32 来读取 DHT11 数字温湿度传感器,从而得到环境温度和湿度等信息,并把从温湿度值显示在 TFTLCD 模块上。本章分为如下几个部分: 36.1 DHT11 简介 36.2 硬件设计 36.3 软件设计 36.4 下载验证 491 36.1 DHT11 简介 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 DHT11 是一款湿温度一体化的数字传感器。该传感器包括一个电阻式测湿元件和一个 NTC 测温元件,并与一个高性能 8 位单片机相连接。通过单片机等微处理器简单的电路连接就能够 实时的采集本地湿度和温度。DHT11 与单片机之间能采用简单的单总线进行通信,仅仅需要一 个 I/O 口。传感器内部湿度和温度数据 40Bit 的数据一次性传给单片机,数据采用校验和方式 进行校验,有效的保证数据传输的准确性。DHT11 功耗很低,5V 电源电压下,工作平均最大 电流 0.5mA。 DHT11 的技术参数如下:  工作电压范围:3.3V-5.5V  工作电流 :平均 0.5mA  输出:单总线数字信号  测量范围:湿度 20~90%RH,温度 0~50℃  精度 :湿度±5%,温度±2℃  分辨率 :湿度 1%,温度 1℃ DHT11 的管脚排列如图 36.1.1 所示: 图 36.1.1 DHT11 管脚排列图 虽然 DHT11 与 DS18B20 类似,都是单总线访问,但是 DHT11 的访问,相对 DS18B20 来 说要简单很多。下面我们先来看看 DHT11 的数据结构。 DHT11 数字湿温度传感器采用单总线数据格式。即,单个数据引脚端口完成输入输出双向 传输。其数据包由 5Byte(40Bit)组成。数据分小数部分和整数部分,一次完整的数据传输为 40bit,高位先出。DHT11 的数据格式为:8bit 湿度整数数据+8bit 湿度小数数据+8bit 温度整数 数据+8bit 温度小数数据+8bit 校验和。其中校验和数据为前四个字节相加。 传感器数据输出的是未编码的二进制数据。数据(湿度、温度、整数、小数)之间应该分开 处理。例如,某次从 DHT11 读到的数据如图 36.1.2 所示: 图 36.1.2 某次读取到 DHT11 的数据 由以上数据就可得到湿度和温度的值,计算方法: 湿度= byte4 . byte3=45.0 (%RH) 492 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 温度= byte2 . byte1=28.0 ( ℃) 校验= byte4+ byte3+ byte2+ byte1=73(=湿度+温度)(校验正确) 可以看出,DHT11 的数据格式是十分简单的,DHT11 和 MCU 的一次通信最大为 3ms 左右, 建议主机连续读取时间间隔不要小于 100ms。 下面,我们介绍一下 DHT11 的传输时序。DHT11 的数据发送流程如图 36.1.3 所示: 图 36.1.3 DHT11 数据发送流程 首先主机发送开始信号,即:拉低数据线,保持 t1(至少 18ms)时间,然后拉高数据线 t2 (20~40us)时间,然后读取 DHT11 的响应,正常的话,DHT11 会拉低数据线,保持 t3(40~50us) 时间,作为响应信号,然后 DHT11 拉高数据线,保持 t4(40~50us)时间后,开始输出数据。 DHT11 输出数字‘0’的时序如图 36.1.4 所示: 图 36.1.4 DHT11 数字‘0’时序 DHT11 输出数字‘1’的时序如图 36.1.5 所示: 图 36.1.5 DHT11 数字‘1’时序 通过以上了解,我们就可以通过 STM32 来实现对 DHT11 的读取了。DHT11 的介绍就到这 493 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 里,更详细的介绍,请参考 DHT11 的数据手册。 36.2 硬件设计 由于开发板上标准配置是没有 DHT11 这个传感器的,只有接口,所以要做本章的实验, 大家必须找一个 DHT11 插在预留的 DHT11 接口上。 本章实验功能简介:开机的时候先检测是否有 DHT11 存在,如果没有,则提示错误。只 有在检测到 DHT11 之后才开始读取温湿度值,并显示在 LCD 上,如果发现了 DHT11,则程 序每隔 100ms 左右读取一次数据,并把温湿度显示在 LCD 上。同样我们也是用 DS0 来指示程 序正在运行。 所要用到的硬件资源如下: 1) 指示灯 DS0 2) TFTLCD 模块 3) DHT11 温湿度传感器 这些我们都已经介绍过了,DHT11 和 DS18B20 的接口是共用一个的,不过 DHT11 有 4 条 腿,需要把 U6 的 4 个接口都用上,将 DHT11 传感器插入到这个上面就可以通过 STM32F1 来 读取温湿度值了。连接示意图如图 36.2.1 所示: 图 36.2.1 DHT11 连接示意图 这里要注意,将 DHT11 贴有字的一面朝内,而有很多孔的一面(网面)朝外,然后然后插入 如图所示的四个孔内就可以了。 36.3 软件设计 打开 DHT11 数字温湿度传感器实验工程可以发现,我们在工程中添加了 dht11.c 文件和 dht11.h 文件,所有 DHT11 相关的驱动代码和定义都在这两个文件中。 打开 dht11.c 代码如下: #include "dht11.h" #include "delay.h" //复位 DHT11 void DHT11_Rst(void) { DHT11_IO_OUT(); //SET OUTPUT DHT11_DQ_OUT=0; //拉低 DQ delay_ms(20); //拉低至少 18ms DHT11_DQ_OUT=1; //DQ=1 delay_us(30); //主机拉高 20~40us } //等待 DHT11 的回应 494 STM32F1 开发指南(库函数版) //返回 1:未检测到 DHT11 的存在 //返回 0:存在 ALIENTEK 战舰 STM32F103 V3 开发板教程 u8 DHT11_Check(void) { u8 retry=0; DHT11_IO_IN();//SET INPUT while (DHT11_DQ_IN&&retry<100) //DHT11 会拉低 40~80us { retry++; delay_us(1); }; if(retry>=100)return 1; else retry=0; while (!DHT11_DQ_IN&&retry<100)//DHT11 拉低后会再次拉高 40~80us { retry++; delay_us(1); }; if(retry>=100)return 1; return 0; } //从 DHT11 读取一个位 //返回值:1/0 u8 DHT11_Read_Bit(void) { u8 retry=0; while(DHT11_DQ_IN&&retry<100)//等待变为低电平 { retry++; delay_us(1); } retry=0; while(!DHT11_DQ_IN&&retry<100)//等待变高电平 { retry++; delay_us(1); } delay_us(40);//等待 40us if(DHT11_DQ_IN)return 1; else return 0; } //从 DHT11 读取一个字节 //返回值:读到的数据 495 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 u8 DHT11_Read_Byte(void) { u8 i,dat; dat=0; for (i=0;i<8;i++) { dat<<=1; dat|=DHT11_Read_Bit(); } return dat; } //从 DHT11 读取一次数据 //temp:温度值(范围:0~50°) //humi:湿度值(范围:20%~90%) //返回值:0,正常;1,读取失败 u8 DHT11_Read_Data(u8 *temp,u8 *humi) { u8 buf[5]; u8 i; DHT11_Rst(); if(DHT11_Check()==0) { for(i=0;i<5;i++)//读取 40 位数据 { buf[i]=DHT11_Read_Byte(); } if((buf[0]+buf[1]+buf[2]+buf[3])==buf[4]) { *humi=buf[0]; *temp=buf[2]; } }else return 1; return 0; } //初始化 DHT11 的 IO 口 DQ 同时检测 DHT11 的存在 //返回 1:不存在 //返回 0:存在 u8 DHT11_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOG, ENABLE); //使能 PG 端口时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11; //PG11 端口配置 496 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOG, &GPIO_InitStructure); GPIO_SetBits(GPIOG,GPIO_Pin_11); //初始化 IO 口 //PG11 输出高 DHT11_Rst(); return DHT11_Check(); //复位 DHT11 //等待 DHT11 的回应 } 该部分代码首先是通过函数 DHT11_Init 初始化传感器,然后根据我们前面介绍的单总线 操作时序来读取 DHT11 的温湿度值的,DHT11 的温湿度值通过 DHT11_Read_Data 函数读取,如 果返回 0,则说明读取成功,返回 1,则说明读取失败。同样我们打开 dht11.h 可以看到,头文 件中主要是一些端口配置以及函数申明,代码比较简单。 接下来我们打开 main.c,该文件代 码如下: int main(void) { u8 t=0; u8 temperature; u8 humidity; delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组 2 uart_init(115200); //串口初始化为 115200 LED_Init(); LCD_Init(); POINT_COLOR=RED; //初始化与 LED 连接的硬件接口 //初始化 LCD //设置字体为红色 LCD_ShowString(30,50,200,16,16,"WarShip STM32"); LCD_ShowString(30,70,200,16,16,"DHT11 TEST"); LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(30,110,200,16,16,"2015/1/16"); while(DHT11_Init()) //DHT11 初始化 { LCD_ShowString(30,130,200,16,16,"DHT11 Error"); delay_ms(200); LCD_Fill(30,130,239,130+16,WHITE); delay_ms(200); } LCD_ShowString(30,130,200,16,16,"DHT11 OK"); POINT_COLOR=BLUE;//设置字体为蓝色 LCD_ShowString(30,150,200,16,16,"Temp: C"); LCD_ShowString(30,170,200,16,16,"Humi: %"); while(1) { if(t%10==0) //每 100ms 读取一次 497 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 { DHT11_Read_Data(&temperature,&humidity); //读取温湿度值 LCD_ShowNum(30+40,150,temperature,2,16); //显示温度 LCD_ShowNum(30+40,170,humidity,2,16); //显示湿度 } delay_ms(10); t++; if(t==20) { t=0; LED0=!LED0; } } } 主函数比较简单,进行一系列初始化后,如果 DHT11 初始化成功,那么每隔 100ms 读取 一次转换数据并显示在液晶上。至此,我们本章的软件设计就结束了。 36.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK 战舰 STM32 开发板上,可以看到 LCD 显示开始显示当前的温度值(假定 DHT11 已经接上去了),如图 36.4.1 所示: 图 36.4.1 DHT11 读取到的温湿度值 至此,本章实验结束。大家可以将本章通过 DHT11 读取到的温度值,和前一章的通过 DS18B20 读取到的温度值对比一下,看看哪个更准确? 498 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第三十七章 MPU6050 六轴传感器实验 本章,我们介绍当下最流行的一款六轴(三轴加速度+三轴角速度(陀螺仪))传感器: MPU6050,该传感器广泛用于四轴、平衡车和空中鼠标等设计,具有非常广泛的应用范围。 ALIENTEK 战舰 STM32F1 开发板本身并不带 MPU6050 传感器,但是可以通过 ATK MODULE 接口,外扩 ATK-MPU6050 模块来实现本例程。 本章我们将使用 STM32F1 来驱动 MPU6050,读取其原始数据,并利用其自带的 DMP 实 现姿态解算,结合匿名四轴上位机软件和 LCD 显示,教大家如何使用这款功能强大的六轴传感 器。本章分为如下几个部分: 37.1 MPU6050 简介 37.2 硬件设计 37.3 软件设计 37.4 下载验证 499 37.1 MPU6050 简介 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 本节,我们将分 2 个部分介绍:1,MPU6050 基础介绍。2,DMP 使用简介。 37.1.1 MPU6050 基础介绍 MPU6050 是 InvenSense 公司推出的全球首款整合性 6 轴运动处理组件,相较于多组件方 案,免除了组合陀螺仪与加速器时之轴间差的问题,减少了安装空间。 MPU6050 内部整合了 3 轴陀螺仪和 3 轴加速度传感器,并且含有一个第二 IIC 接口,可用 于连接外部磁力传感器,并利用自带的数字运动处理器(DMP: Digital Motion Processor)硬件 加速引擎,通过主 IIC 接口,向应用端输出完整的 9 轴融合演算数据。有了 DMP,我们可以使 用 InvenSense 公司提供的运动处理资料库,非常方便的实现姿态解算,降低了运动处理运算对 操作系统的负荷,同时大大降低了开发难度。 MPU6050 的特点包括: ① 以数字形式输出 6 轴或 9 轴(需外接磁传感器)的旋转矩阵、四元数(quaternion)、欧 拉角格式(Euler Angle forma)的融合演算数据(需 DMP 支持) ② 具有 131 LSBs/°/sec 敏感度与全格感测范围为±250、±500、±1000 与±2000°/sec 的 3 轴角速度感测器(陀螺仪) ③ 集成可程序控制,范围为±2g、±4g、±8g 和±16g 的 3 轴加速度传感器 ④ 移除加速器与陀螺仪轴间敏感度,降低设定给予的影响与感测器的飘移 ⑤ 自带数字运动处理(DMP: Digital Motion Processing)引擎可减少 MCU 复杂的融合演算 数据、感测器同步化、姿势感应等的负荷 ⑥ 内建运作时间偏差与磁力感测器校正演算技术,免除了客户须另外进行校正的需求 ⑦ 自带一个数字温度传感器 ⑧ 带数字输入同步引脚(Sync pin)支持视频电子影相稳定技术与 GPS ⑨ 可程序控制的中断(interrupt),支持姿势识别、摇摄、画面放大缩小、滚动、快速下降 中断、high-G 中断、零动作感应、触击感应、摇动感应功能 ⑩ VDD 供电电压为 2.5V±5%、3.0V±5%、3.3V±5%;VLOGIC 可低至 1.8V± 5% ⑪ 陀螺仪工作电流:5mA,陀螺仪待机电流:5uA;加速器工作电流:500uA,加速器省 电模式电流:40uA@10Hz ⑫ 自带 1024 字节 FIFO,有助于降低系统功耗 ⑬ 高达 400Khz 的 IIC 通信接口 ⑭ 超小封装尺寸:4x4x0.9mm(QFN) MPU6050 传感器的检测轴如图 37.1.1.1 所示: 图 37.1.1.1 MPU6050 检测轴及其方向 500 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 MPU6050 的内部框图如图 37.1.1.2 所示: 图 37.1.1.2 MPU6050 框图 其中,SCL 和 SDA 是连接 MCU 的 IIC 接口,MCU 通过这个 IIC 接口来控制 MPU6050, 另外还有一个 IIC 接口:AUX_CL 和 AUX_DA,这个接口可用来连接外部从设备,比如磁传感 器,这样就可以组成一个九轴传感器。VLOGIC 是 IO 口电压,该引脚最低可以到 1.8V,我们 一般直接接 VDD 即可。AD0 是从 IIC 接口(接 MCU)的地址控制引脚,该引脚控制 IIC 地址 的最低位。如果接 GND,则 MPU6050 的 IIC 地址是:0X68,如果接 VDD,则是 0X69,注意: 这里的地址是不包含数据传输的最低位的(最低位用来表示读写)!! 在战舰 STM32F1 开发板上,我们通过 PA15 控制 ATK-MPU6050 模块 AD0 接 GND,因而 选择 MPU6050 的 IIC 地址是 0X68(不含最低位),IIC 通信的时序我们在之前已经介绍过(第 二十八章,IIC 实验),这里就不再细说了。 接下来,我们介绍一下利用 STM32F1 读取 MPU6050 的加速度和角度传感器数据(非中 断方式),需要哪些初始化步骤: 1)初始化 IIC 接口 MPU6050 采用 IIC 与 STM32F1 通信,所以我们需要先初始化与 MPU6050 连接的 SDA 和 SCL 数据线。 2)复位 MPU6050 这一步让 MPU6050 内部所有寄存器恢复默认值,通过对电源管理寄存器 1(0X6B)的 bit7 写 1 实现。 复位后,电源管理寄存器 1 恢复默认值(0X40),然后必须设置该寄存器为 0X00, 以唤醒 MPU6050,进入正常工作状态。 3)设置角速度传感器(陀螺仪)和加速度传感器的满量程范围 这一步,我们设置两个传感器的满量程范围(FSR),分别通过陀螺仪配置寄存器(0X1B) 和加速度传感器配置寄存器(0X1C)设置。我们一般设置陀螺仪的满量程范围为±2000dps, 501 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 加速度传感器的满量程范围为±2g。 4)设置其他参数 这里,我们还需要配置的参数有:关闭中断、关闭 AUX IIC 接口、禁止 FIFO、设置陀螺 仪采样率和设置数字低通滤波器(DLPF)等。本章我们不用中断方式读取数据,所以关闭中断, 然后也没用到 AUX IIC 接口外接其他传感器,所以也关闭这个接口。分别通过中断使能寄存器 (0X38)和用户控制寄存器(0X6A)控制。MPU6050 可以使用 FIFO 存储传感器数据,不过 本章我们没有用到,所以关闭所有 FIFO 通道,这个通过 FIFO 使能寄存器(0X23)控制,默 认都是 0(即禁止 FIFO),所以用默认值就可以了。陀螺仪采样率通过采样率分频寄存器(0X19) 控制,这个采样率我们一般设置为 50 即可。数字低通滤波器(DLPF)则通过配置寄存器(0X1A) 设置,一般设置 DLPF 为带宽的 1/2 即可。 5)配置系统时钟源并使能角速度传感器和加速度传感器 系统时钟源同样是通过电源管理寄存器 1(0X1B)来设置,该寄存器的最低三位用于设置 系统时钟源选择,默认值是 0(内部 8M RC 震荡),不过我们一般设置为 1,选择 x 轴陀螺 PLL 作为时钟源,以获得更高精度的时钟。同时,使能角速度传感器和加速度传感器,这两个操作 通过电源管理寄存器 2(0X6C)来设置,设置对应位为 0 即可开启。 至此,MPU6050 的初始化就完成了,可以正常工作了(其他未设置的寄存器全部采用默认 值即可),接下来,我们就可以读取相关寄存器,得到加速度传感器、角速度传感器和温度传感 器的数据了。不过,我们先简单介绍几个重要的寄存器。 首先,我们介绍电源管理寄存器 1,该寄存器地址为 0X6B,各位描述如图 37.1.1.3 所示: 图 37.1.1.3 电源管理寄存器 1 各位描述 其中,DEVICE_RESET 位用来控制复位,设置为 1,复位 MPU6050,复位结束后,MPU 硬件自动清零该位。SLEEEP 位用于控制 MPU6050 的工作模式,复位后,该位为 1,即进入了 睡眠模式(低功耗),所以我们要清零该位,以进入正常工作模式。TEMP_DIS 用于设置是否 使能温度传感器,设置为 0,则使能。最后 CLKSEL[2:0]用于选择系统时钟源,选择关系如表 37.1.1.1 所示: CLKSEL[2:0] 时钟源 000 内部 8M RC 晶振 001 PLL,使用 X 轴陀螺作为参考 010 PLL,使用 Y 轴陀螺作为参考 011 PLL,使用 Z 轴陀螺作为参考 100 PLL,使用外部 32.768Khz 作为参考 101 PLL,使用外部 19.2Mhz 作为参考 110 保留 111 关闭时钟,保持时序产生电路复位状态 表 37.1.1.1 CLKSEL 选择列表 默认是使用内部 8M RC 晶振的,精度不高,所以我们一般选择 X/Y/Z 轴陀螺作为参考的 PLL 作为时钟源,一般设置 CLKSEL=001 即可。 接着,我们看陀螺仪配置寄存器,该寄存器地址为:0X1B,各位描述如图 37.1.4 所示: 502 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 37.1.1.4 陀螺仪配置寄存器各位描述 该寄存器我们只关心 FS_SEL[1:0]这两个位,用于设置陀螺仪的满量程范围:0,±250°/S; 1,±500°/S;2,±1000°/S;3,±2000°/S;我们一般设置为 3,即±2000°/S,因为陀螺 仪的 ADC 为 16 位分辨率,所以得到灵敏度为:65536/4000=16.4LSB/(°/S)。 接下来,我们看加速度传感器配置寄存器,寄存器地址为:0X1C,各位描述如图 37.1.1.5 所示: 图 37.1.1.5 加速度传感器配置寄存器各位描述 该寄存器我们只关心 AFS_SEL[1:0]这两个位,用于设置加速度传感器的满量程范围:0, ±2g;1,±4g;2,±8g;3,±16g;我们一般设置为 0,即±2g,因为加速度传感器的 ADC 也是 16 位,所以得到灵敏度为:65536/4=16384LSB/g。 接下来,我看看 FIFO 使能寄存器,寄存器地址为:0X1C,各位描述如图 37.1.1.6 所示: 图 37.1.1.6 FIFO 使能寄存器各位描述 该寄存器用于控制 FIFO 使能,在简单读取传感器数据的时候,可以不用 FIFO,设置对应 位为 0 即可禁止 FIFO,设置为 1,则使能 FIFO。注意加速度传感器的 3 个轴,全由 1 个位 (ACCEL_FIFO_EN)控制,只要该位置 1,则加速度传感器的三个通道都开启 FIFO 了。 接下来,我们看陀螺仪采样率分频寄存器,寄存器地址为:0X19,各位描述如图 37.1.1.7 所示: 图 37.1.1.7 陀螺仪采样率分频寄存器各位描述 该寄存器用于设置 MPU6050 的陀螺仪采样频率,计算公式为: 采样频率 = 陀螺仪输出频率 / (1+SMPLRT_DIV) 这里陀螺仪的输出频率,是 1Khz 或者 8Khz,与数字低通滤波器(DLPF)的设置有关, 当 DLPF_CFG=0/7 的时候,频率为 8Khz,其他情况是 1Khz。而且 DLPF 滤波频率一般设置为 采样率的一半。采样率,我们假定设置为 50Hz,那么 SMPLRT_DIV=1000/50-1=19。 接下来,我们看配置寄存器,寄存器地址为:0X1A,各位描述如图 37.1.1.8 所示: 图 37.1.1.8 配置寄存器各位描述 这里,我们主要关心数字低通滤波器(DLPF)的设置位,即:DLPF_CFG[2:0],加速度计 和陀螺仪,都是根据这三个位的配置进行过滤的。DLPF_CFG 不同配置对应的过滤情况如表 37.1. 1. 2 所示: 503 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 加速度传感器 角速度传感器 DLPF_CFG[2:0] Fs=1Khz (陀螺仪) 带宽(Hz) 延迟(ms) 带宽(Hz) 延迟(ms) Fs(Khz) 000 260 0 256 0.98 8 001 184 2.0 188 1.9 1 010 94 3.0 98 2.8 1 011 44 4.9 42 4.8 1 100 21 8.5 20 8.3 1 101 10 13.8 10 13.4 1 110 5 19.0 5 18.6 1 111 保留 保留 8 表 37.1.1.2 DLPF_CFG 配置表 这里的加速度传感器,输出速率(Fs)固定是 1Khz,而角速度传感器的输出速率(Fs), 则根据 DLPF_CFG 的配置有所不同。一般我们设置角速度传感器的带宽为其采样率的一半,如 前面所说的,如果设置采样率为 50Hz,那么带宽就应该设置为 25Hz,取近似值 20Hz,就应该 设置 DLPF_CFG=100。 接下来,我们看电源管理寄存器 2,寄存器地址为:0X6C,各位描述如图 37.1.1.9 所示: 图 37.1.1.9 电源管理寄存器 2 各位描述 该寄存器的 LP_WAKE_CTRL 用于控制低功耗时的唤醒频率,本章用不到。剩下的 6 位, 分别控制加速度和陀螺仪的 x/y/z 轴是否进入待机模式,这里我们全部都不进入待机模式,所以 全部设置为 0 即可。 接下来,我们看看陀螺仪数据输出寄存器,总共有 8 个寄存器组成,地址为:0X43~0X48, 通过读取这 8 个寄存器,就可以读到陀螺仪 x/y/z 轴的值,比如 x 轴的数据,可以通过读取 0X43 (高 8 位)和 0X44(低 8 位)寄存器得到,其他轴以此类推。 同样,加速度传感器数据输出寄存器,也有 8 个,地址为:0X3B~0X40,通过读取这 8 个 寄存器,就可以读到加速度传感器 x/y/z 轴的值,比如读 x 轴的数据,可以通过读取 0X3B(高 8 位)和 0X3C(低 8 位)寄存器得到,其他轴以此类推。 最后,温度传感器的值,可以通过读取 0X41(高 8 位)和 0X42(低 8 位)寄存器得到, 温度换算公式为: Temperature = 36.53 + regval/340 其中,Temperature 为计算得到的温度值,单位为℃,regval 为从 0X41 和 0X42 读到的温度 传感器值。 关于 MPU6050 的基础介绍,我们就介绍到这。MPU6050 的详细资料和相关寄存器介绍, 请参考光盘:7,硬件资料MPU6050 资料MPU-6000 and MPU-6050 Product Specification.pdf 和 MPU-6000 and MPU-6050 Register Map and Descriptions.pdf 这两个文档,另外该目录还提供 了部分 MPU6050 的中文资料,供大家参考学习。 37.1.2 DMP 使用简介 经过 37.1.1 节的介绍,我们可以读出 MPU6050 的加速度传感器和角速度传感器的原始数 504 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 据。不过这些原始数据,对想搞四轴之类的初学者来说,用处不大,我们期望得到的是姿态数 据,也就是欧拉角:航向角(yaw)、横滚角(roll)和俯仰角(pitch)。有了这三个角,我们就 可以得到当前四轴的姿态,这才是我们想要的结果。 要得到欧拉角数据,就得利用我们的原始数据,进行姿态融合解算,这个比较复杂,知识 点比较多,初学者 不易掌握。而 MPU6050 自带了数字运动处理器,即 DMP,并且,InvenSense 提供了一个 MPU6050 的嵌入式运动驱动库,结合 MPU6050 的 DMP,可以将我们的原始数据, 直接转换成四元数输出,而得到四元数之后,就可以很方便的计算出欧拉角,从而得到 yaw、 roll 和 pitch。 使用内置的 DMP,大大简化了四轴的代码设计,且 MCU 不用进行姿态解算过程,大大降 低了 MCU 的负担,从而有更多的时间去处理其他事件,提高系统实时性。 使用 MPU6050 的 DMP 输出的四元数是 q30 格式的,也就是浮点数放大了 2 的 30 次方倍。 在换算成欧拉角之前,必须先将其转换为浮点数,也就是除以 2 的 30 次方,然后再进行计算, 计算公式为: q0=quat[0] / q30; //q30 格式转换为浮点数 q1=quat[1] / q30; q2=quat[2] / q30; q3=quat[3] / q30; //计算得到俯仰角/横滚角/航向角 pitch=asin(-2 * q1 * q3 + 2 * q0* q2)* 57.3; //俯仰角 roll=atan2(2 * q2 * q3 + 2 * q0 * q1, -2 * q1 * q1 - 2 * q2* q2 + 1)* 57.3; //横滚角 yaw=atan2(2*(q1*q2 + q0*q3),q0*q0+q1*q1-q2*q2-q3*q3) * 57.3; //航向角 其中 quat[0]~ quat[3]是 MPU6050 的 DMP 解算后的四元数,q30 格式,所以要除以一个 2 的 30 次方,其中 q30 是一个常量:1073741824,即 2 的 30 次方,然后带入公式,计算出欧拉 角。上述计算公式的 57.3 是弧度转换为角度,即 180/π,这样得到的结果就是以度(°)为单 位的。关于四元数与欧拉角的公式推导,这里我们不进行讲解,感兴趣的朋友,可以自行查阅 相关资料学习。 InvenSense 提供的 MPU6050 运动驱动库是基于 MSP430 的,我们需要将其移植一下,才 可以用到 STM32F1 上面,官方原版驱动在光盘:7,硬件资料MPU6050 资料DMP 资料 Embedded_MotionDriver_5.1.rar,这就是官方原版的驱动,代码比较多,不过官方提供了两个 资料供大家学习:Embedded Motion Driver V5.1.1 API 说明.pdf 和 Embedded Motion Driver V5.1.1 教程.pdf,这两个文件都在 DMP 资料文件夹里面,大家可以阅读这两个文件,来熟悉官 方驱动库的使用。 官方 DMP 驱动库移植起来,还是比较简单的,主要是实现这 4 个函数:i2c_write,i2c_read, delay_ms 和 get_ms,具体细节,我们就不详细介绍了,移植后的驱动代码,我们放在本例程 HARDWAREMPU6050eMPL 文件夹内,总共 6 个文件,如图 37.1.2.1 所示: 505 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 37.1.2.1 移植后的驱动库代码 该驱动库,重点就是两个 c 文件:inv_mpu.c 和 inv_mpu_dmp_motion_driver.c。其中我们在 inv_mpu.c 添加了几个函数,方便我们使用,重点是两个函数:mpu_dmp_init 和 mpu_dmp_get_data 这两个函数,这里我们简单介绍下这两个函数。 mpu_dmp_init,是 MPU6050 DMP 初始化函数,该函数代码如下: //mpu6050,dmp 初始化 //返回值:0,正常 // 其他,失败 u8 mpu_dmp_init(void) { u8 res=0; IIC_Init(); //初始化 IIC 总线 if(mpu_init()==0) //初始化 MPU6050 { res=mpu_set_sensors(INV_XYZ_GYRO|INV_XYZ_ACCEL);//设置需要的传感器 if(res)return 1; res=mpu_configure_fifo(INV_XYZ_GYRO|INV_XYZ_ACCEL);//设置 FIFO if(res)return 2; res=mpu_set_sample_rate(DEFAULT_MPU_HZ); //设置采样率 if(res)return 3; res=dmp_load_motion_driver_firmware(); //加载 dmp 固件 if(res)return 4; res=dmp_set_orientation(inv_orientation_matrix_to_scalar(gyro_orientation)); //设置陀螺仪方向 if(res)return 5; res=dmp_enable_feature(DMP_FEATURE_6X_LP_QUAT|DMP_FEATURE_TAP| DMP_FEATURE_ANDROID_ORIENT|DMP_FEATURE_SEND_RAW_ACCEL| DMP_FEATURE_SEND_CAL_GYRO|DMP_FEATURE_GYRO_CAL); //设置 dmp 功能 506 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 if(res)return 6; res=dmp_set_fifo_rate(DEFAULT_MPU_HZ);//设置 DMP 输出速率(最大 200Hz) if(res)return 7; res=run_self_test(); //自检 if(res)return 8; res=mpu_set_dmp_state(1); //使能 DMP if(res)return 9; } return 0; } 此函数首先通过 IIC_Init(需外部提供)初始化与 MPU6050 连接的 IIC 接口,然后调用 mpu_init 函数,初始化 MPU6050,之后就是设置 DMP 所用传感器、FIFO、采样率和加载固件 等一系列操作,在所有操作都正常之后,最后通过 mpu_set_dmp_state(1)使能 DMP 功能,在使 能成功以后,我们便可以通过 mpu_dmp_get_data 来读取姿态解算后的数据了。 mpu_dmp_get_data 函数代码如下: //得到 dmp 处理后的数据(注意,本函数需要比较多堆栈,局部变量有点多) //pitch:俯仰角 精度:0.1° 范围:-90.0° <---> +90.0° //roll:横滚角 精度:0.1° 范围:-180.0°<---> +180.0° //yaw:航向角 精度:0.1° 范围:-180.0°<---> +180.0° //返回值:0,正常 // 其他,失败 u8 mpu_dmp_get_data(float *pitch,float *roll,float *yaw) { float q0=1.0f,q1=0.0f,q2=0.0f,q3=0.0f; unsigned long sensor_timestamp; short gyro[3], accel[3], sensors; unsigned char more; long quat[4]; if(dmp_read_fifo(gyro, accel, quat, &sensor_timestamp, &sensors,&more))return 1; if(sensors&INV_WXYZ_QUAT) { q0 = quat[0] / q30; //q30 格式转换为浮点数 q1 = quat[1] / q30; q2 = quat[2] / q30; q3 = quat[3] / q30; //计算得到俯仰角/横滚角/航向角 *pitch = asin(-2 * q1 * q3 + 2 * q0* q2)* 57.3; // pitch *roll = atan2(2 * q2 * q3 + 2 * q0 * q1, -2 * q1 * q1 - 2 * q2* q2 + 1)* 57.3; // roll *yaw= atan2(2*(q1*q2 + q0*q3),q0*q0+q1*q1-q2*q2-q3*q3) * 57.3; //yaw }else return 2; return 0; } 此函数用于得到 DMP 姿态解算后的俯仰角、横滚角和航向角。不过本函数局部变量有点 507 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 多,大家在使用的时候,如果死机,那么请设置堆栈大一点(在 startup_stm32f10x_hd.s 里面设置, 默认是 400)。这里就用到了我们前面介绍的四元数转欧拉角公式,将 dmp_read_fifo 函数读到 的 q30 格式四元数转换成欧拉角。 利用这两个函数,我们就可以读取到姿态解算后的欧拉角,使用非常方便。DMP 部分,我 们就介绍到这。 37.2 硬件设计 本实验采用战舰 STM32F1 开发板的 ATK MODULE 接口连接 ATK-MPU6050 模块,本章 实验功能简介:程序先初始化 MPU6050 等外设,然后利用 DMP 库,初始化 MPU6050 及使 能 DMP,最后,在死循环里面不停读取:温度传感器、加速度传感器、陀螺仪、DMP 姿态解 算后的欧拉角等数据,通过串口上报给上位机(温度不上报),利用上位机软件(ANO_Tech 匿名四轴上位机_V2.6.exe),可以实时显示 MPU6050 的传感器状态曲线,并显示 3D 姿态, 可以通过 KEY0 按键开启/关闭数据上传功能。同时,在 LCD 模块上面显示温度和欧拉角等信 息。DS0 来指示程序正在运行。 所要用到的硬件资源如下: 1) 指示灯 DS0 2) KEY0 按键 3) TFTLCD 模块 4) 串口 5) ATK-MPU6050 模块 6) ATK MODULE 接口 前 4 个,在之前的实例已经介绍过了,这里我们介绍下 ATK-MPU6050 模块与战舰 STM32F1 开发板的连接。ATK-MPU6050 模块原理图如图 37.2.1 所示: 图 37.2.1 ATK-MPU6050 模块原理图 508 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 从上图可知,ATK-MPU6050 模块,通过 P1 排针与外部连接,引出了 VCC、GND、IIC_SDA、 IIC_SCL、MPU_INT 和 MPU_AD0 等信号,其中,IIC_SDA 和 IIC_SCL 带了 4.7K 上拉电阻, 外部可以不用再加上拉电阻了,另外 MPU_AD0 自带了 10K 下拉电阻,当 AD0 悬空时,默认 IIC 地址为(0X68)。模块的 P1 接口可以直接插在战舰 STM32F1 开发板的 ATK MODULE 接口 上,如图 37.2.2 所示: 图 37.2.2 ATK-MPU6050 模块与开发板实物连接图 而 ATK MODULE 接口与 MCU 的连接原理图如图 37.2.3 所示: 图 37.2.3 ATK MODULE 接口与 STM32F103 的连接原理图 从上图可以看出,ATK MODULE 接口,必须将 P8 的 USART3_TX(PB10)和 GBC_RX 以及 USART3_RX(PB11)和 GBC_TX 连接,才能完成和 STM32 的连接,如图 37.2.4 所示: 图 37.2.3 P8 跳线帽连接示意图 509 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 这样连接好后,ATK-MPU6050 模块的 IIC_SCL、IIC_SDA、MPU_INT 和 MPU_AD0 分别 连接在 STM32 的 PB10、PB11、PA4 和 PA15 上面。不过,本例程我们并没有用到中断。另外, MPU_AD0 通过 PA15 输出低电平,从而选则 MPU6050 的器件地址是:0X68。 37.3 软件设计 本章实验工程所在目录可以看到,在 HARDWARE 文件夹下新建一个 MPU6050 的文件夹。 然后新建一个 mpu6050.c 和 mpu6050.h 的文件保存在 MPU6050 文件夹下,并在工程中将这个 文件夹加入头文件包含路径。 同时,将 DMP 驱动库代码:见光盘例程源码:实验 32 MPU6050 六轴传感器实验 \HARDWARE\MPU6050\ eMPL,里面的 eMPL 文件夹,拷贝到本例程 MPU6050 文件夹里面, 将 eMPL 文件夹也加入头文件包含路径,然后将 eMPL 文件夹里面的两个 c 文件:inv_mpu.c 和 inv_mpu_dmp_motion_driver.c 加入 HARDWARE 组。 由于 mpu6050.c 里面代码比较多,这里我们就不全部列出来了,仅介绍几个重要的函数。 首先是:MPU_Init,该函数代码如下: u8 MPU_Init(void) { u8 res; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);//使能 AFIO 时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//使能外设 PA 时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15; // 端口配置 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //IO 口速度为 50MHz GPIO_Init(GPIOA, &GPIO_InitStructure); //根据设定参数初始化 GPIOA GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE); //禁止 JTAG,从而 PA15 可以做普通 IO 使用,否则 PA15 不能做普通 IO!!! MPU_AD0_CTRL=0; //控制 MPU6050 的 AD0 脚为低电平,从机地址为:0X68 MPU_IIC_Init();//初始化 IIC 总线 MPU_Write_Byte(MPU_PWR_MGMT1_REG,0X80); //复位 MPU6050 delay_ms(100); MPU_Write_Byte(MPU_PWR_MGMT1_REG,0X00); //唤醒 MPU6050 MPU_Set_Gyro_Fsr(3); MPU_Set_Accel_Fsr(0); MPU_Set_Rate(50); //陀螺仪传感器,±2000dps //加速度传感器,±2g //设置采样率 50Hz MPU_Write_Byte(MPU_INT_EN_REG,0X00); //关闭所有中断 MPU_Write_Byte(MPU_USER_CTRL_REG,0X00); //I2C 主模式关闭 MPU_Write_Byte(MPU_FIFO_EN_REG,0X00); //关闭 FIFO MPU_Write_Byte(MPU_INTBP_CFG_REG,0X80); //INT 引脚低电平有效 res=MPU_Read_Byte(MPU_DEVICE_ID_REG); if(res==MPU_ADDR)//器件 ID 正确 510 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 { MPU_Write_Byte(MPU_PWR_MGMT1_REG,0X01);//设置 CLKSEL,PLL X 轴为参考 MPU_Write_Byte(MPU_PWR_MGMT2_REG,0X00); //加速度与陀螺仪都工作 MPU_Set_Rate(50); //设置采样率为 50Hz }else return 1; return 0; } 该函数就是按我们在 37.1.1 节介绍的方法,对 MPU6050 进行初始化,该函数执行成功后, 便可以读取传感器数据了。 然后再看 MPU_Get_Temperature、MPU_Get_Gyroscope 和 MPU_Get_Accelerometer 等三个 函数,源码如下: //得到温度值 //返回值:温度值(扩大了 100 倍) short MPU_Get_Temperature(void) { u8 buf[2]; short raw; float temp; MPU_Read_Len(MPU_ADDR,MPU_TEMP_OUTH_REG,2,buf); raw=((u16)buf[0]<<8)|buf[1]; temp=36.53+((double)raw)/340; return temp*100;; } //得到陀螺仪值(原始值) //gx,gy,gz:陀螺仪 x,y,z 轴的原始读数(带符号) //返回值:0,成功 // 其他,错误代码 u8 MPU_Get_Gyroscope(short *gx,short *gy,short *gz) { u8 buf[6],res; res=MPU_Read_Len(MPU_ADDR,MPU_GYRO_XOUTH_REG,6,buf); if(res==0) { *gx=((u16)buf[0]<<8)|buf[1]; *gy=((u16)buf[2]<<8)|buf[3]; *gz=((u16)buf[4]<<8)|buf[5]; } return res;; } //得到加速度值(原始值) //gx,gy,gz:陀螺仪 x,y,z 轴的原始读数(带符号) //返回值:0,成功 // 其他,错误代码 u8 MPU_Get_Accelerometer(short *ax,short *ay,short *az) 511 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 { u8 buf[6],res; res=MPU_Read_Len(MPU_ADDR,MPU_ACCEL_XOUTH_REG,6,buf); if(res==0) { *ax=((u16)buf[0]<<8)|buf[1]; *ay=((u16)buf[2]<<8)|buf[3]; *az=((u16)buf[4]<<8)|buf[5]; } return res;; } 其中 MPU_Get_Temperature 用于获取 MPU6050 自带温度传感器的温度值,然后 MPU_Get_Gyroscope 和 MPU_Get_Accelerometer 分别用于读取陀螺仪和加速度传感器的原 始数据。 最后看 MPU_Write_Len 和 MPU_Read_Len 这两个函数,代码如下: //IIC 连续写 //addr:器件地址 //reg:寄存器地址 //len:写入长度 //buf:数据区 //返回值:0,正常 // 其他,错误代码 u8 MPU_Write_Len(u8 addr,u8 reg,u8 len,u8 *buf) { u8 i; IIC_Start(); IIC_Send_Byte((addr<<1)|0);//发送器件地址+写命令 if(IIC_Wait_Ack()){IIC_Stop();return 1;}//等待应答 IIC_Send_Byte(reg); //写寄存器地址 IIC_Wait_Ack(); //等待应答 for(i=0;i28)return; //最多 28 字节数据 send_buf[len+3]=0; //校验数置零 send_buf[0]=0X88; //帧头 send_buf[1]=fun; //功能字 send_buf[2]=len; //数据长度 for(i=0;i>8)&0XFF; tbuf[1]=aacx&0XFF; tbuf[2]=(aacy>>8)&0XFF; tbuf[3]=aacy&0XFF; tbuf[4]=(aacz>>8)&0XFF; tbuf[5]=aacz&0XFF; tbuf[6]=(gyrox>>8)&0XFF; tbuf[7]=gyrox&0XFF; tbuf[8]=(gyroy>>8)&0XFF; tbuf[9]=gyroy&0XFF; tbuf[10]=(gyroz>>8)&0XFF; tbuf[11]=gyroz&0XFF; usart1_niming_report(0XA1,tbuf,12);//自定义帧,0XA1 } //通过串口 1 上报结算后的姿态数据给电脑 //aacx,aacy,aacz:x,y,z 三个方向上面的加速度值 //gyrox,gyroy,gyroz:x,y,z 三个方向上面的陀螺仪值 //roll:横滚角.单位 0.01 度。 -18000 -> 18000 对应 -180.00 -> 180.00 度 //pitch:俯仰角.单位 0.01 度。-9000 - 9000 对应 -90.00 -> 90.00 度 //yaw:航向角.单位为 0.1 度 0 -> 3600 对应 0 -> 360.0 度 void usart1_report_imu(short aacx,short aacy,short aacz,short gyrox,short gyroy,short gyroz, short roll,short pitch,short yaw) { u8 tbuf[28]; u8 i; for(i=0;i<28;i++)tbuf[i]=0;//清 0 tbuf[0]=(aacx>>8)&0XFF; tbuf[1]=aacx&0XFF; tbuf[2]=(aacy>>8)&0XFF; tbuf[3]=aacy&0XFF; tbuf[4]=(aacz>>8)&0XFF; tbuf[5]=aacz&0XFF; tbuf[6]=(gyrox>>8)&0XFF; tbuf[7]=gyrox&0XFF; tbuf[8]=(gyroy>>8)&0XFF; tbuf[9]=gyroy&0XFF; tbuf[10]=(gyroz>>8)&0XFF; tbuf[11]=gyroz&0XFF; tbuf[18]=(roll>>8)&0XFF; tbuf[19]=roll&0XFF; tbuf[20]=(pitch>>8)&0XFF; tbuf[21]=pitch&0XFF; tbuf[22]=(yaw>>8)&0XFF; tbuf[23]=yaw&0XFF; 514 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 usart1_niming_report(0XAF,tbuf,28);//飞控显示帧,0XAF } int main(void) { u8 t=0,report=1; //默认开启上报 u8 key; float pitch,roll,yaw; //欧拉角 short aacx,aacy,aacz; //加速度传感器原始数据 short gyrox,gyroy,gyroz; //陀螺仪原始数据 short temp; //温度 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置 NVIC 中断分组 2 uart_init(500000); //串口初始化为 500000 delay_init(); //延时初始化 usmart_dev.init(72); //初始化 USMART LED_Init(); //初始化与 LED 连接的硬件接口 KEY_Init(); //初始化按键 LCD_Init(); //初始化 LCD MPU_Init(); //初始化 MPU6050 POINT_COLOR=RED; //设置字体为红色 LCD_ShowString(30,50,200,16,16,"WarShip STM32"); LCD_ShowString(30,70,200,16,16,"MPU6050 TEST"); LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(30,110,200,16,16,"2015/1/17"); while(mpu_dmp_init()) { LCD_ShowString(30,130,200,16,16,"MPU6050 Error"); delay_ms(200); LCD_Fill(30,130,239,130+16,WHITE); delay_ms(200); } LCD_ShowString(30,130,200,16,16,"MPU6050 OK"); LCD_ShowString(30,150,200,16,16,"KEY0:UPLOAD ON/OFF"); POINT_COLOR=BLUE;//设置字体为蓝色 LCD_ShowString(30,170,200,16,16,"UPLOAD ON "); LCD_ShowString(30,200,200,16,16," Temp: . C"); LCD_ShowString(30,220,200,16,16,"Pitch: . C"); LCD_ShowString(30,240,200,16,16," Roll: . C"); LCD_ShowString(30,260,200,16,16," Yaw : . C"); while(1) { key=KEY_Scan(0); if(key==KEY0_PRES) { report=!report; if(report)LCD_ShowString(30,170,200,16,16,"UPLOAD ON "); 515 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 else LCD_ShowString(30,170,200,16,16,"UPLOAD OFF"); } if(mpu_dmp_get_data(&pitch,&roll,&yaw)==0) { temp=MPU_Get_Temperature(); //得到温度值 MPU_Get_Accelerometer(&aacx,&aacy,&aacz); //得到加速度传感器数据 MPU_Get_Gyroscope(&gyrox,&gyroy,&gyroz); //得到陀螺仪数据 if(report)mpu6050_send_data(aacx,aacy,aacz,gyrox,gyroy,gyroz); //用自定义帧发送加速度和陀螺仪原始数据 if(report)usart1_report_imu(aacx,aacy,aacz,gyrox,gyroy,gyroz,(int)(roll*100), (int)(pitch*100),(int)(yaw*10)); if((t%10)==0) { if(temp<0) { LCD_ShowChar(30+48,200,'-',16,0); //显示负号 temp=-temp; //转为正数 }else LCD_ShowChar(30+48,200,' ',16,0); //去掉负号 LCD_ShowNum(30+48+8,200,temp/100,3,16); //显示整数部分 LCD_ShowNum(30+48+40,200,temp%10,1,16); //显示小数部分 temp=pitch*10; if(temp<0) { LCD_ShowChar(30+48,220,'-',16,0); //显示负号 temp=-temp; //转为正数 }else LCD_ShowChar(30+48,220,' ',16,0); //去掉负号 LCD_ShowNum(30+48+8,220,temp/10,3,16); //显示整数部分 LCD_ShowNum(30+48+40,220,temp%10,1,16); //显示小数部分 temp=roll*10; if(temp<0) { LCD_ShowChar(30+48,240,'-',16,0); //显示负号 temp=-temp; //转为正数 }else LCD_ShowChar(30+48,240,' ',16,0); //去掉负号 LCD_ShowNum(30+48+8,240,temp/10,3,16); //显示整数部分 LCD_ShowNum(30+48+40,240,temp%10,1,16); //显示小数部分 temp=yaw*10; if(temp<0) { LCD_ShowChar(30+48,260,'-',16,0); temp=-temp; //转为正数 }else LCD_ShowChar(30+48,260,' ',16,0); LCD_ShowNum(30+48+8,260,temp/10,3,16); //显示负号 //去掉负号 //显示整数部分 516 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 LCD_ShowNum(30+48+40,260,temp%10,1,16); //显示小数部分 t=0; LED0=!LED0;//LED 闪烁 } } t++; } } 此部分代码除了 main 函数,还有几个函数,用于上报数据给上位机软件,利用上位机软件 显示传感器波形,以及 3D 姿态显示,有助于更好的调试 MPU6050。上位机软件使用:ANO_Tech 匿名四轴上位机_V2.6.exe,该软件在:开发板光盘 6,软件资料软件匿名四轴上位机 文 件夹里面可以找到,该软件的使用方法,见该文件夹下的 README.txt,这里我们不做介绍。其 中,usart1_niming_report 函数用于将数据打包、计算校验和,然后上报给匿名四轴上位机软件。 mpu6050_send_data 函数用于上报加速度和陀螺仪的原始数据,可用于波形显示传感器数据, 通过 A1 自定义帧发送。而 usart1_report_imu 函数,则用于上报飞控显示帧,可以实时 3D 显示 MPU6050 的姿态,传感器数据等。 这里,main 函数是比较简单的,大家看代码即可,不过需要注意的是,为了高速上传数据, 这里我们将串口 1 的波特率设置为 500Kbps 了,测试的时候要注意下。 最后,我们将 MPU_Write_Byte、MPU_Read_Byte 和 MPU_Get_Temperature 等三个函数加 入 USMART 控制,这样,我们就可以通过串口调试助手,改写和读取 MPU6050 的寄存器数据 了,并可以读取温度传感器的值,方便大家调试(注意在 USMART 调试的时候,最好通过按 KEY0,先关闭数据上传功能,否则会受到很多乱码,妨碍调试)。 至此,我们的软件设计部分就结束了。 37.4 下载验证 本例程测试需要自备 ATK-MPU6050 模块一个。在代码编译成功之后,我们通过下载代码 到 ALIENTEK 战舰 STM32F1 开发板上,可以看到 LCD 显示如图 37.4.1 所示的内容(假定 ATK-MPU6050 模块已经接到战舰 STM32 开发板的 ATK MODULE 接口): 517 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 37.4.1 程序运行时 LCD 显示内容 屏幕显示了 MPU6050 的温度、俯仰角(pitch)、横滚角(roll)和航向角(yaw)的数值。 然后,我们可以晃动开发板,看看各角度的变化。 另外,通过按 KEY0 可以开启或关闭数据上报,开启状态下,我们可以打开:ANO_Tech 匿名四轴上位机_V2.6.exe,这个软件,接收 STM32F1 上传的数据,从而图形化显示传感器数 据以及飞行姿态,如图 37.4.2 和图 37.4.3 所示: 图 37.4.2 传感器数据波形显示 图 37.4.3 飞控状态显示 图 37.4.2 就是波形化显示我们通过 mpu6050_send_data 函数发送的数据,采用 A1 功能帧发 送,总共 6 条线(Series1~6)显示波形,全部来自 A1 功能帧,int16 数据格式,Series1~6 分别 代表:加速度传感器 x/y/z 和角速度传感器(陀螺仪)x/y/z 方向的原始数据。 518 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 37.4.3 则 3D 显示了我们开发板的姿态,通过 usart1_report_imu 函数发送的数据显示, 采用飞控显示帧格(AF)式上传,同时还显示了加速度陀螺仪等传感器的原始数据。注意,首 先我们要在界面左侧的“基本功能”选项卡里面设置好 USB 虚拟串口号以及波特率(500000), 然后在“飞控状态”选项卡里面依次在右下方选上“高级收码”,“波形显示”和“打开串口” 三个按钮即可。 最后,我们还可以用 USMART 读写 MPU6050 的任何寄存器,来调试代码,这里我们就不 做演示了,大家自己测试即可。最后,建议大家用 USMART 调试的时候,先按 KEY0 关闭数 据上传功能,否则会收到很多乱码!!,注意波特率设置为:500Kbps(设置方法:XCOM 在关 闭串口状态下,选择自定义波特率,然后输入:500000,再打开串口就可以了)。 519 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第三十八章 无线通信实验 ALIENTKE 战舰 STM32F1 开发板带有一个无线模块(WIRELESS)接口,采用 8 脚插针 方式与开发板连接,可以用来连接 NRF24L01/RFID 等无线模块。本章我们将以 NRF24L01 模 块为例向大家介绍如何在 ALIENTEK 战舰 STM32 开发板上实现无线通信。在本章中,我们将 使用两块战舰 STM32 开发板,一块用于发送收据,另外一块用于接收,从而实现无线数据传 输。本章分为如下几个部分: 38.1 NRF24L01 无线模块简介 38.2 硬件设计 38.3 软件设计 38.4 下载验证 520 38.1 NRF24L01 无线模块简介 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 NRF24L01 无线模块,采用的芯片是 NRF24L01,该芯片的主要特点如下: 1)2.4G 全球开放的 ISM 频段,免许可证使用。 2)最高工作速率 2Mbps,高校的 GFSK 调制,抗干扰能力强。 3)125 个可选的频道,满足多点通信和调频通信的需要。 4)内置 CRC 检错和点对多点的通信地址控制。 5)低工作电压(1.9~3.6V)。 6)可设置自动应答,确保数据可靠传输。 该芯片通过 SPI 与外部 MCU 通信,最大的 SPI 速度可以达到 10Mhz。本章我们用到的模 块是深圳云佳科技生产的 NRF24L01,该模块已经被很多公司大量使用,成熟度和稳定性都是 相当不错的。该模块的外形和引脚图如图 38.1.1 所示: 图 38.1.1 NRF24L01 无线模块外观引脚图 模块 VCC 脚的电压范围为 1.9~3.6V,建议不要超过 3.6V,否则可能烧坏模块,一般用 3.3V 电压比较合适。除了 VCC 和 GND 脚,其他引脚都可以和 5V 单片机的 IO 口直连,正是因为其 兼容 5V 单片机的 IO,故使用上具有很大优势。 关于 NRF24L01 的详细介绍,请参考 NRF24L01 的技术手册。 38.2 硬件设计 本章实验功能简介:开机的时候先检测 NRF24L01 模块是否存在,在检测到 NRF24L01 模块之后,根据 KEY0 和 KEY1 的设置来决定模块的工作模式,在设定好工作模式之后,就会 不停的发送/接收数据,同样用 DS0 来指示程序正在运行。 所要用到的硬件资源如下: 1) 指示灯 DS0 2) KEY0 和 KEY1 按键 3) TFTLCD 模块 4) NRF24L01 模块 NRF24L01 模块属于外部模块,这里我们仅介绍 NRF24L01 接口和 STM32 的连接情况,他 们的连接关系如图 38.2.1 所示: 521 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 38.2.1 NRF24L01 模块接口与 STM32 连接原理图 这里NRF24L01也是使用的SPI2,和W25Q128共用一个SPI接口,所以在使用的时候,他 们分时复用SPI2。本章我们需要把W25Q128的片选信号置高,以防止这个器件对NRF24L01 的通信造成干扰。另外,NRF_IRQ和DM9000_INT共用了PG6,所以,他们不能同时使用,不 过我们一般用不到NRF_IRQ这个信号,因此,DM9000和NRF一般也可以同时使用。 NRF24L01无线模块和开发板的连接实物图,如图38.2.2所示: 图38.2.2 NRF24L01模块和开发板连接实物图 由于无线通信实验是双向的,所以至少要有两个模块同时能工作,这里我们使用2套 ALIENTEK战舰STM32F1开发板来向大家演示。 522 38.3 软件设计 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 打开我们的无线通信实验项目工程,可以看到我们加入了 24l01.c 文件和 24l01.h 头文件, 所有 24L01 相关的驱动代码和定义都在这两个文件中实现。同时,我们还加入了之前的 spi 驱 动文件 spi.c 和 spi.h 头文件,因为 24L01 是通过 SPI 接口通信的。 打开 24l01.c 文件,代码如下: #include "24l01.h" #include "lcd.h" #include "delay.h" #include "spi.h" #include "usart.h" const u8 TX_ADDRESS[TX_ADR_WIDTH]={0x34,0x43,0x10,0x10,0x01}; //发送地址 const u8 RX_ADDRESS[RX_ADR_WIDTH]={0x34,0x43,0x10,0x10,0x01}; //发送地址 //初始化 24L01 的 IO 口 void NRF24L01_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOG, ENABLE); //使能 PB,G 端口时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12; //PB12 上拉 防止 W25X 的干扰 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); //初始化指定 IO GPIO_SetBits(GPIOB,GPIO_Pin_12);//上拉 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7|GPIO_Pin_8; GPIO_Init(GPIOG, &GPIO_InitStructure);//初始化指定 IO //PG8 7 推挽 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //PG6 输入 GPIO_Init(GPIOG, &GPIO_InitStructure); GPIO_ResetBits(GPIOG,GPIO_Pin_6|GPIO_Pin_7|GPIO_Pin_8);//PG6,7,8 上拉 SPI2_Init(); //初始化 SPI SPI_Cmd(SPI2, DISABLE); // SPI 外设不使能 SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //设置 //SPI 单向或者双向的数据模式:SPI 设置为双线双向全双工 SPI_InitStructure.SPI_Mode = SPI_Mode_Master;//设置 SPI 工作模式:设置为主 SPI SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 8 位帧结构 SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; //时钟悬空低 SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; //数据捕获于第 1 个时钟沿 523 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS 信号由软件控制 SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16; //定义波特率预分频的值:波特率预分频值为 16 SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // MSB 位开始 SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC 值计算的多项式 SPI_Init(SPI2, &SPI_InitStructure); //根据指定的参数初始化外设 SPIx SPI_Cmd(SPI2, ENABLE); //使能 SPI 外设 NRF24L01_CE=0; //使能 24L01 NRF24L01_CSN=1; //SPI 片选取消 } //检测 24L01 是否存在 //返回值:0,成功;1,失败 u8 NRF24L01_Check(void) { u8 buf[5]={0XA5,0XA5,0XA5,0XA5,0XA5}; u8 i; SPI2_SetSpeed(SPI_BaudRatePrescaler_4); //spi 速度为 9Mhz NRF24L01_Write_Buf(WRITE_REG_NRF+TX_ADDR,buf,5);//写入 5 个字节的地址. NRF24L01_Read_Buf(TX_ADDR,buf,5); //读出写入的地址 for(i=0;i<5;i++)if(buf[i]!=0XA5)break; if(i!=5)return 1; //检测 24L01 错误 return 0; //检测到 24L01 } //SPI 写寄存器 //reg:指定寄存器地址 //value:写入的值 u8 NRF24L01_Write_Reg(u8 reg,u8 value) { u8 status; NRF24L01_CSN=0; status =SPI2_ReadWriteByte(reg); SPI2_ReadWriteByte(value); NRF24L01_CSN=1; return(status); //使能 SPI 传输 //发送寄存器号 //写入寄存器的值 //禁止 SPI 传输 //返回状态值 } //读取 SPI 寄存器值 //reg:要读的寄存器 u8 NRF24L01_Read_Reg(u8 reg) { u8 reg_val; NRF24L01_CSN = 0; SPI2_ReadWriteByte(reg); //使能 SPI 传输 //发送寄存器号 524 STM32F1 开发指南(库函数版) reg_val=SPI2_ReadWriteByte(0XFF); NRF24L01_CSN = 1; return(reg_val); ALIENTEK 战舰 STM32F103 V3 开发板教程 //读取寄存器内容 //禁止 SPI 传输 //返回状态值 } //在指定位置读出指定长度的数据 //reg:寄存器(位置) //*pBuf:数据指针 //len:数据长度 //返回值,此次读到的状态寄存器值 u8 NRF24L01_Read_Buf(u8 reg,u8 *pBuf,u8 len) { u8 status,u8_ctr; NRF24L01_CSN = 0; //使能 SPI 传输 status=SPI2_ReadWriteByte(reg); //发送寄存器值(位置),并读取状态值 for(u8_ctr=0;u8_ctr('~'))key=' '; tmp_buf[t]=key; } mode++; if(mode>'~')mode=' '; tmp_buf[32]=0;//加入结束符 }else { LCD_Fill(0,170,lcddev.width,170+16*3,WHITE);//清空显示 LCD_ShowString(30,170,lcddev.width-1,32,16,"Send Failed "); }; LED0=!LED0; delay_ms(1500); }; } } 以上代码,我们就实现了 38.2 节所介绍的功能,程序运行时先通过 NRF24L01_Check 函 数检测 NRF24L01 是否存在,如果存在,则让用户选择发送模式(KEY1)还是接收模式(KEY0), 在确定模式之后,设置 NRF24L01 的工作模式,然后执行相应的数据发送/接收处理。 至此,我们整个实验的软件设计就完成了。 38.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK 战舰 STM32F1 开发板上,可以看 530 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 到 LCD 显示如图 38.4.1 所示的内容(假定 NRF24L01 模块已经接上开发板): 图 38.4.1 选择工作模式界面 通过 KEY0 和 KEY1 来选择 NRF24L01 模块所要进入的工作模式,我们两个开发板一个选 择发送,一个选择接收就可以了。设置好后通信界面如图 38.4.2 和图 38.4.3 所示: 图 38.4.2 开发板 A 发送数据 图 38.4.3 开发板 B 接收数据 图 38.4.2 来自开发板 A,工作在发送模式。图 38.4.3 来自开发板 B,工作在接收模式,A 发送,B 接收。可以看到收发数据是一致的,说明实验成功。 531 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第三十九章 FLASH 模拟 EEPROM 实验 STM32 本身没有自带 EEPROM,但是 STM32 具有 IAP(在应用编程)功能,所以我们可 以把它的 FLASH 当成 EEPROM 来使用。本章,我们将利用 STM32 内部的 FLASH 来实现第二 十八章类似的效果,不过这次我们是将数据直接存放在 STM32 内部,而不是存放在 W25Q128。 本章分为如下几个部分: 39.1 STM32 FLASH 简介 39.2 硬件设计 39.3 软件设计 39.4 下载验证 532 39.1 STM32 FLASH 简介 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 不同型号的 STM32,其 FLASH 容量也有所不同,最小的只有 16K 字节,最大的则达到了 1024K 字节。战舰 STM32 开发板选择的 STM32F103ZET6 的 FLASH 容量为 512K 字节,属于 大容量产品(另外还有中容量和小容量产品),大容量产品的闪存模块组织如图 39.1.1 所示: 图 39.1.1 大容量产品闪存模块组织 STM32 的闪存模块由:主存储器、信息块和闪存存储器接口寄存器等 3 部分组成。 主存储器,该部分用来存放代码和数据常数(如 const 类型的数据)。对于大容量产品,其 被划分为 256 页,每页 2K 字节。注意,小容量和中容量产品则每页只有 1K 字节。从上图可以 看出主存储器的起始地址就是 0X08000000, B0、B1 都接 GND 的时候,就是从 0X08000000 开始运行代码的。 信息块,该部分分为 2 个小部分,其中启动程序代码,是用来存储 ST 自带的启动程序, 用于串口下载代码,当 B0 接 V3.3,B1 接 GND 的时候,运行的就是这部分代码。用户选择字 节,则一般用于配置写保护、读保护等功能,本章不作介绍。 闪存存储器接口寄存器,该部分用于控制闪存读写等,是整个闪存模块的控制机构。 对主存储器和信息块的写入由内嵌的闪存编程/擦除控制器(FPEC)管理;编程与擦除的高电 压由内部产生。 在执行闪存写操作时,任何对闪存的读操作都会锁住总线,在写操作完成后读操作才能正 确地进行;既在进行写或擦除操作时,不能进行代码或数据的读取操作。 闪存的读取 内置闪存模块可以在通用地址空间直接寻址,任何 32 位数据的读操作都能访问闪存模块的 内容并得到相应的数据。读接口在闪存端包含一个读控制器,还包含一个 AHB 接口与 CPU 衔 接。这个接口的主要工作是产生读闪存的控制信号并预取 CPU 要求的指令块,预取指令块仅用 于在 I-Code 总线上的取指操作,数据常量是通过 D-Code 总线访问的。这两条总线的访问目标 533 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 是相同的闪存模块,访问 D-Code 将比预取指令优先级高。 这里要特别留意一个闪存等待时间,因为 CPU 运行速度比 FLASH 快得多,STM32F103 的 FLASH 最快访问速度≤24Mhz,如果 CPU 频率超过这个速度,那么必须加入等待时间,比 如我们一般使用 72Mhz 的主频,那么 FLASH 等待周期就必须设置为 2,该设置通过 FLASH_ACR 寄存器设置。 例如,我们要从地址 addr,读取一个半字(半字为 16 为,字为 32 位),可以通过如下的 语句读取: data=*(vu16*)addr; 将 addr 强制转换为 vu16 指针,然后取该指针所指向的地址的值,即得到了 addr 地址的值。 类似的,将上面的 vu16 改为 vu8,即可读取指定地址的一个字节。相对 FLASH 读取来说,STM32 FLASH 的写就复杂一点了,下面我们介绍 STM32 闪存的编程和擦除。 闪存的编程和擦除 STM32 的闪存编程是由 FPEC(闪存编程和擦除控制器)模块处理的,这个模块包含 7 个 32 位寄存器,他们分别是:  FPEC 键寄存器(FLASH_KEYR)  选择字节键寄存器(FLASH_OPTKEYR)  闪存控制寄存器(FLASH_CR)  闪存状态寄存器(FLASH_SR)  闪存地址寄存器(FLASH_AR)  选择字节寄存器(FLASH_OBR)  写保护寄存器(FLASH_WRPR) 其中 FPEC 键寄存器总共有 3 个键值: RDPRT 键=0X000000A5 KEY1=0X45670123 KEY2=0XCDEF89AB STM32 复位后,FPEC 模块是被保护的,不能写入 FLASH_CR 寄存器;通过写入特定的序 列到 FLASH_KEYR 寄存器可以打开 FPEC 模块(即写入 KEY1 和 KEY2),只有在写保护被解 除后,我们才能操作相关寄存器。 STM32 闪存的编程每次必须写入 16 位(不能单纯的写入 8 位数据哦!),当 FLASH_CR 寄 存器的 PG 位为’1’时,在一个闪存地址写入一个半字将启动一次编程;写入任何非半字的数 据,FPEC 都会产生总线错误。在编程过程中(BSY 位为’1’),任何读写闪存的操作都会使 CPU 暂停,直到此次闪存编程结束。 同样,STM32 的 FLASH 在编程的时候,也必须要求其写入地址的 FLASH 是被擦除了的 (也就是其值必须是 0XFFFF),否则无法写入,在 FLASH_SR 寄存器的 PGERR 位将得到一个 警告。 STM23 的 FLASH 编程过程如图 39.1.2 所示: 534 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 39.1.2 STM32 闪存编程过程 从上图可以得到闪存的编程顺序如下:  检查 FLASH_CR 的 LOCK 是否解锁,如果没有则先解锁  检查 FLASH_SR 寄存器的 BSY 位,以确认没有其他正在进行的编程操作  设置 FLASH_CR 寄存器的 PG 位为’1’  在指定的地址写入要编程的半字  等待 BSY 位变为’0’  读出写入的地址并验证数据 前面提到,我们在 STM32 的 FLASH 编程的时候,要先判断缩写地址是否被擦除了,所以, 我们有必要再介绍一下 STM32 的闪存擦除,STM32 的闪存擦除分为两种:页擦除和整片擦除。 页擦除过程如图 39.1.3 所示 图 39.1.3 STM32 闪存页擦除过程 535 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 从上图可以看出,STM32 的页擦除顺序为:  检查 FLASH_CR 的 LOCK 是否解锁,如果没有则先解锁  检查 FLASH_SR 寄存器的 BSY 位,以确认没有其他正在进行的闪存操作  设置 FLASH_CR 寄存器的 PER 位为’1’  用 FLASH_AR 寄存器选择要擦除的页  设置 FLASH_CR 寄存器的 STRT 位为’1’  等待 BSY 位变为’0’  读出被擦除的页并做验证 本章,我们只用到了 STM32 的页擦除功能,整片擦除功能我们在这里就不介绍了。通过 以上了解,我们基本上知道了 STM32 闪存的读写所要执行的步骤了,接下来,我们看看与读 写相关的寄存器说明。 第一个介绍的是 FPEC 键寄存器:FLASH_KEYR。该寄存器各位描述如图 39.1.4 所示: 图 39.1.4 寄存器 FLASH_KEYR 各位描述 该寄存器主要用来解锁 FPEC,必须在该寄存器写入特定的序列(KEY1 和 KEY2)解锁后, 才能对 FLASH_CR 寄存器进行写操作。 第二个要介绍的是闪存控制寄存器:FLASH_CR。该寄存器的各位描述如图 39.1.5 所示: 图 39.1.5 寄存器 FLASH_CR 各位描述 该寄存器我们本章只用到了它的 LOCK、STRT、PER 和 PG 等 4 个位。 LOCK 位,该位用于指示 FLASH_CR 寄存器是否被锁住,该位在检测到正确的解锁序列后, 硬件将其清零。在一次不成功的解锁操作后,在下次系统复位之前,该位将不再改变。 STRT 位,该位用于开始一次擦除操作。在该位写入 1 ,将执行一次擦除操作。 PER 位,该位用于选择页擦除操作,在页擦除的时候,需要将该位置 1。 PG 位,该位用于选择编程操作,在往 FLASH 写数据的时候,该位需要置 1。 FLASH_CR 的其他位,我们就不在这里介绍了,请大家参考《STM32F10xxx 闪存编程参 考手册》第 18 页。 第三个要介绍的是闪存状态寄存器:FLASH_SR。该寄存器各位描述如图 39.1.6 所示: 536 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 39.1.6 寄存器 FLASH_SR 各位描述 该寄存器主要用来指示当前 FPEC 的操作编程状态。 最后,我们再来看看闪存地址寄存器:FLASH_AR。该寄存器各位描述如图 39.1.7 所示: 图 39.1.7 寄存器 FLASH_AR 各位描述 该寄存器在本章,我们主要用来设置要擦除的页。 关 于 STM32 FLASH 的 基 础 知 识 介 绍 , 我 们 就 介 绍 到 这 。 更 详 细 的 介 绍 , 请 参 考 《STM32F10xxx 闪存编程参考手册》。下面我们讲解使用 STM32 的官方固件库操作 FLASH 的 几个常用函数。这些函数和定义分布在文件 stm32f10x_flash.c 以及 stm32f10x_flash.h 文件中。 1. 锁定解锁函数 上面讲解到在对 FLASH 进行写操作前必须先解锁,解锁操作也就是必须在 FLASH_KEYR 寄 537 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 存器写入特定的序列(KEY1 和 KEY2),固件库函数实现很简单: void FLASH_Unlock(void); 同样的道理,在对 FLASH 写操作完成之后,我们要锁定 FLASH,使用的库函数是: void FLASH_Lock(void); 2. 写操作函数 固件库提供了三个 FLASH 写函数: FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data); FLASH_Status FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data); FLASH_Status FLASH_ProgramOptionByteData(uint32_t Address, uint8_t Data); 顾名思义分别为:FLASH_ProgramWord 为 32 位字写入函数,其他分别为 16 位半字写入和用 户选择字节写入函数。这里需要说明,32 位字节写入实际上是写入的两次 16 位数据,写完第 一次后地址+2,这与我们前面讲解的 STM32 闪存的编程每次必须写入 16 位并不矛盾。写入 8 位实际也是占用的两个地址了,跟写入 16 位基本上没啥区别。 3. 擦除函数 固件库提供三个 FLASH 擦除函数: FLASH_Status FLASH_ErasePage(uint32_t Page_Address); FLASH_Status FLASH_EraseAllPages(void); FLASH_Status FLASH_EraseOptionBytes(void); 这三个函数顾名思义,第一个函数是页擦除函数,根据页地址擦除特定的页数据,第二个函数 是擦除所有的页数据,第三个函数是擦除用户选择字节数据。这三个函数使用非常简单。 4. 获取 FLASH 状态 主要是用的函数是: FLASH_Status FLASH_GetStatus(void); 返回值是通过枚举类型定义的: typedef enum { FLASH_BUSY = 1,//忙 FLASH_ERROR_PG,//编程错误 FLASH_ERROR_WRP,//写保护错误 FLASH_COMPLETE,//操作完成 FLASH_TIMEOUT//操作超时 }FLASH_Status; 从这里面我们可以看到 FLASH 操作的 5 个状态,每个代表的意思我们在后面注释了。 5. 等待操作完成函数 在执行闪存写操作时,任何对闪存的读操作都会锁住总线,在写操作完成后读操作才能正 确地进行;既在进行写或擦除操作时,不能进行代码或数据的读取操作。 所以在每次操作之前,我们都要等待上一次操作完成这次操作才能开始。使用的函数是: FLASH_Status FLASH_WaitForLastOperation(uint32_t Timeout) 入口参数为等待时间,返回值是 FLASH 的状态,这个很容易理解,这个函数本身我们在固件 库中使用得不多,但是在固件库函数体中间可以多次看到。 6. 读 FLASH 特定地址数据函数 有写就必定有读,而读取 FLASH 指定地址的半字的函数固件库并没有给出来,这里我们 自己写的一个函数: 538 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 u16 STMFLASH_ReadHalfWord(u32 faddr) { return *(vu16*)faddr; } 39.2 硬件设计 本章实验功能简介:开机的时候先显示一些提示信息,然后在主循环里面检测两个按键, 其中 1 个按键(WK_UP)用来执行写入 FLASH 的操作,另外一个按键(KEY1)用来执行读 出操作,在 TFTLCD 模块上显示相关信息。同时用 DS0 提示程序正在运行。 所要用到的硬件资源如下: 1) 指示灯 DS0 2) WK_UP 和 KEY1 按键 3) TFTLCD 模块 4) STM32 内部 FLASH 本章需要用到的资源和电路连接,在之前已经全部有介绍过了,接下来我们直接开始软件 设计。 39.3 软件设计 打开我们的 FLASH 模拟 EEPROM 实验工程,可以看到我们添加了两个文件 stmflash.c 和 stm32flash.h 。 同 时 我 们 还 引 入 了 固 件 库 flash 操 作 文 件 stm32f10x_flash.c 和 头 文 件 stm32f10x_flash.h。 打开 stmflash.c 文件,代码如下: #include "stmflash.h" #include "delay.h" #include "usart.h" //读取指定地址的半字(16 位数据) //faddr:读地址(此地址必须为 2 的倍数!!) //返回值:对应数据. u16 STMFLASH_ReadHalfWord(u32 faddr) { return *(vu16*)faddr; } #if STM32_FLASH_WREN //如果使能了写 //不检查的写入 //WriteAddr:起始地址 //pBuffer:数据指针 //NumToWrite:半字(16 位)数 void STMFLASH_Write_NoCheck(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite) { u16 i; for(i=0;i= (STM32_FLASH_BASE+1024*STM32_FLASH_SIZE)))return;//非法地址 FLASH_Unlock(); //解锁 offaddr=WriteAddr-STM32_FLASH_BASE; //实际偏移地址. secpos=offaddr/STM_SECTOR_SIZE; secoff=(offaddr%STM_SECTOR_SIZE)/2; //在扇区内的偏移(2 个字节为基本单位.) secremain=STM_SECTOR_SIZE/2-secoff; //扇区剩余空间大小 if(NumToWrite<=secremain)secremain=NumToWrite;//不大于该扇区范围 while(1) { STMFLASH_Read(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE, STMFLASH_BUF,STM_SECTOR_SIZE/2); //读出整个扇区的内容 for(i=0;i(STM_SECTOR_SIZE/2))secremain=STM_SECTOR_SIZE/2; else secremain=NumToWrite;//下一个扇区可以写完了 } }; FLASH_Lock();//上锁 } #endif //从指定地址开始读出指定长度的数据 //ReadAddr:起始地址 //pBuffer:数据指针 //NumToWrite:半字(16 位)数 void STMFLASH_Read(u32 ReadAddr,u16 *pBuffer,u16 NumToRead) { u16 i; for(i=0;iIDR&0XFF; //读数据 OV7670_RCK_H; color<<=8; OV7670_RCK_L; color|=GPIOC->IDR&0XFF; //读数据 OV7670_RCK_H; LCD->LCD_RAM=color; } ov_sta=0; //清零帧中断标记 ov_frame++; LCD_Scan_Dir(DFT_SCAN_DIR); //恢复默认扫描方向 } } int main(void) { u8 key; u8 lightmode=0,saturation=2,brightness=2,contrast=2; u8 effect=0; u8 i=0; u8 msgbuf[15]; //消息缓存区 u8 tm=0; delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组 2 uart_init(115200); //串口初始化为 115200 usmart_dev.init(72); LED_Init(); KEY_Init(); //初始化 USMART //初始化与 LED 连接的硬件接口 //初始化按键 LCD_Init(); //初始化 LCD TPAD_Init(); POINT_COLOR=RED; //触摸按键初始化 //设置字体为红色 LCD_ShowString(30,50,200,16,16,"WarShip STM32"); 556 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 LCD_ShowString(30,70,200,16,16,"OV7670 TEST"); LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(30,110,200,16,16,"2015/1/18"); LCD_ShowString(30,130,200,16,16,"KEY0:Light Mode"); LCD_ShowString(30,150,200,16,16,"KEY1:Saturation"); LCD_ShowString(30,170,200,16,16,"KEY2:Brightness"); LCD_ShowString(30,190,200,16,16,"KEY_UP:Contrast"); LCD_ShowString(30,210,200,16,16,"TPAD:Effects"); LCD_ShowString(30,230,200,16,16,"OV7670 Init..."); while(OV7670_Init())//初始化 OV7670 { LCD_ShowString(30,230,200,16,16,"OV7670 Error!!"); delay_ms(200); LCD_Fill(30,230,239,246,WHITE); delay_ms(200); } LCD_ShowString(30,230,200,16,16,"OV7670 Init OK"); delay_ms(1500); OV7670_Light_Mode(lightmode); OV7670_Color_Saturation(saturation); OV7670_Brightness(brightness); OV7670_Contrast(contrast); OV7670_Special_Effects(effect); TIM6_Int_Init(10000,7199); //10Khz 计数频率,1 秒钟中断 EXTI8_Init(); //使能定时器捕获 OV7670_Window_Set(12,176,240,320); //设置窗口 OV7670_CS=0; LCD_Clear(BLACK); while(1) { key=KEY_Scan(0);//不支持连按 if(key) { tm=20; switch(key) { case KEY0_PRES: //灯光模式 Light Mode lightmode++; if(lightmode>4)lightmode=0; OV7670_Light_Mode(lightmode); sprintf((char*)msgbuf,"%s",LMODE_TBL[lightmode]); break; 557 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 case KEY1_PRES: //饱和度 Saturation saturation++; if(saturation>4)saturation=0; OV7670_Color_Saturation(saturation); sprintf((char*)msgbuf,"Saturation:%d",(signed char)saturation-2); break; case KEY2_PRES: //亮度 Brightness brightness++; if(brightness>4)brightness=0; OV7670_Brightness(brightness); sprintf((char*)msgbuf,"Brightness:%d",(signed char)brightness-2); break; case WKUP_PRES: //对比度 Contrast contrast++; if(contrast>4)contrast=0; OV7670_Contrast(contrast); sprintf((char*)msgbuf,"Contrast:%d",(signed char)contrast-2); break; } } if(TPAD_Scan(0))//检测到触摸按键 { effect++; if(effect>6)effect=0; OV7670_Special_Effects(effect);//设置特效 sprintf((char*)msgbuf,"%s",EFFECTS_TBL[effect]); tm=20; } camera_refresh();//更新显示 if(tm) { LCD_ShowString((lcddev.width-240)/2+30,(lcddev.height-320)/2+60,200,16,16,msgbuf); tm--; } i++; if(i==15)//DS0 闪烁. { i=0; LED0=!LED0; } } } 558 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 此部分代码除了 mian 函数,还有一个 camera_refresh 函数,该函数用于读取摄像头模块自 带 FIFO 里面的数据,并显示在 LCD 上面,对分辨率大于 320*240 的屏幕,则通过开窗函数 (LCD_Set_Window)将显示区域开窗在屏幕的正中央。注意,为了提高 FIFO 读取速度,我们 将 FIFO_RCK 的控制,采用快速 IO 控制,关键代码如下(在 ov7670.h 里面): #define OV7670_RCK_H GPIOB->BSRR=1<<4 //设置读数据时钟高电平 #define OV7670_RCK_L GPIOB->BRR=1<<4 //设置读数据时钟低电平 OV7670_RCK_H 和 OV7670_RCK_L 就用到了 BSRR 和 BRR 这两个寄存器,以实现快速 IO 设置,从而提高读取速度。 main 函数的处理则比较简单,我们就不细说了。 前面提到,我们要用 USMART 来设置摄像头的参数,我们只需要在 usmart_nametab 里面 添加 SCCB_WR_Reg 和 SCCB_RD_Reg 这两个函数,就可以轻松调试摄像头了。 最后,为了得到最快的显示速度,我们将 MDK 的代码优化等级设置为-O2 级别(在 C/C++ 选项卡里面设置),这样 LCD 的显示帧率可以达到 18 帧。注意:这里是因为 TPAD_Scan 扫描 占用了很多时间(>15ms/次),帧率才是 18 帧,如果屏蔽掉 TPAD_Scan,则可以达到 30 帧。 40.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK 战舰 STM32 开发板上,得到如图 40.4.1 所示界面: 图 40.4.1 程序运行效果图 随后,进入监控界面。此时,我们可以按不同的按键(KEY0~KEY2、KEY_UP、TPAD 等), 来设置摄像头的相关参数和模式,得到不同的成像效果。同时,你还可以在串口,通过 USMART 调用 SCCB_WR_Reg 等函数,来设置 OV7670 的各寄存器,达到调试测试 OV7670 的目的,如 图 40.4.2 所示: 559 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 40.4.2 USMART 调试 OV7670 从上图还可以看出,LCD 显示帧率为 19 帧左右,而实际上 OV7670 的输出速度是 30 帧(即 OV_VSYNC 的频率)。图中,我们通过 USMART 发送 SCCB_WR_Reg(0X42,0X08),即可设置 OV7670 输出彩条,方便大家测试。 560 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第四十一章 外部 SRAM 实验 STM32F103ZET6 自带了 64K 字节的 SRAM,对一般应用来说,已经足够了,不过在一些 对内存要求高的场合,STM32 自带的这些内存就不够用了。比如跑算法或者跑 GUI 等,就可 能不太够用,所以战舰 STM32 开发板板载了一颗 1M 字节容量的 SRAM 芯片:IS62WV51216, 满足大内存使用的需求。 本章,我们将使用 STM32 来驱动 IS62WV51216,实现对 IS62WV51216 的访问控制,并测 试其容量。本章分为如下几个部分: 41.1 IS62WV51216 简介 41.2 硬件设计 41.3 软件设计 41.4 下载验证 561 41.1 IS62WV51216 简介 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 IS62WV51216 是 ISSI(Integrated Silicon Solution, Inc)公司生产的一颗 16 位宽 512K (512*16,即 1M 字节)容量的 CMOS 静态内存芯片。该芯片具有如下几个特点:  高速。具有 45ns/55ns 访问速度。  低功耗。  TTL 电平兼容。  全静态操作。不需要刷新和时钟电路。  三态输出。  字节控制功能。支持高/低字节控制。 IS62WV51216 的功能框图如图 41.1.1 所示: 图 41.1.1 IS62WV51216 功能框图 图中 A0~18 为地址线,总共 19 根地址线(即 2^19=512K,1K=1024);IO0~15 为数据线, 总共 16 根数据线。CS2 和 CS1 都是片选信号,不过 CS2 是高电平有效 CS1 是低电平有效;OE 是输出使能信号(读信号);WE 为写使能信号;UB 和 LB 分别是高字节控制和低字节控制信 号; 战舰 STM32 开发板使用的是 TSOP44 封装的 IS62WV51216 芯片,该芯片直接接在 STM32 的 FSMC 上,IS62WV51216 原理图如图 41.1.2 所示: 562 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 41.1.2 IS62WV51216 原理图 从原理图可以看出,IS62WV51216 同 STM32 的连接关系: A[0:18]接 FMSC_A[0:18] D[0:15]接 FSMC_D[0:15] UB 接 FSMC_NBL1 LB 接 FSMC_NBL0 OE 接 FSMC_OE WE 接 FSMC_WE CS 接 FSMC_NE3 本章,我们使用 FSMC 的 BANK1 区域 3 来控制 IS62WV51216,关于 FSMC 的详细介绍, 我们在第十八章已经介绍过,在第十八章,我们采用的是读写不同的时序来操作 TFTLCD 模块 (因为 TFTLCD 模块读的速度比写的速度慢很多),但是在本章,因为 IS62WV51216 的读写时 间基本一致,所以,我们设置读写相同的时序来访问 FSMC。关于 FSMC 的详细介绍,请大家 看第十八章和《STM32 参考手册》。 IS62WV51216 就介绍到这,最后,我们来看看实现 IS62WV51216 的访问,需要对 FSMC 进行哪些配置。FSMC 的详细配置介绍在之前的 LCD 实验章节已经有详细讲解,这里就做一个 概括性的讲解。步骤如下: 1)使能 FSMC 时钟,并配置 FSMC 相关的 IO 及其时钟使能。 要使用 FSMC,当然首先得开启其时钟。然后需要把 FSMC_D0~15,FSMCA0~18 等相关 IO 口,全部配置为复用输出,并使能各 IO 组的时钟。 使能 FSMC 时钟的方法: RCC_AHBPeriphClockCmd(RCC_AHBPeriph_FSMC,ENABLE); 对于其他 IO 口设置的方法前面讲解很详细,这里不做过多的讲解。 2)设置 FSMC BANK1 区域 3。 此部分包括设置区域 3 的存储器的工作模式、位宽和读写时序等。本章我们使用模式 A、 16 位宽,读写共用一个时序寄存器。使用的函数是: 563 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 void FSMC_NORSRAMInit(FSMC_NORSRAMInitTypeDef* FSMC_NORSRAMInitStruct) 这个函数的讲解在前面 LCD 实验的时候已经讲解很详细,所以大家可以回过头看看相关的讲 解。具体的设置方法请参考我们的 sarm.c 文件中的 FSMC_SRAM_Init()函数。 3)使能 BANK1 区域 3。 使能 BANK 的方法跟前面 LCD 实验也是一样的,这里也不做详细讲解,函数是: void FSMC_NORSRAMCmd(uint32_t FSMC_Bank, FunctionalState NewState); 通过以上几个步骤,我们就完成了 FSMC 的配置,可以访问 IS62WV51216 了,这里还需 要注意,因为我们使用的是 BANK1 的区域 3,所以 HADDR[27:26]=10,故外部内存的首地址 为 0X68000000。 41.2 硬件设计 本章实验功能简介:开机后,显示提示信息,然后按下 KEY1 按键,即测试外部 SRAM 容 量大小并显示在 LCD 上。按下 WK_UP 按键,即显示预存在外部 SRAM 的数据。DS0 指示程 序运行状态。 本实验用到的硬件资源有: 1) 指示灯 DS0 2) KEY1 和 WK_UP 按键 3) 串口 4) TFTLCD 模块 5) IS62WV51216 这些我们都已经介绍过(IS62WV51216 与 STM32F1 的各 IO 对应关系,请参考光盘原理图), 接下来我们开始软件设计。 41.3 软件设计 打开外部 SRAM 实验工程,可以看到,我们增加了 sram.c 文件以及头文件 sram.h,FSMC 初始化相关配置和定义都在这两个文件中。同时还引入了 FSMC 固件库文件 stm32f10x_fsmc.c 和 stm32f10x_fsmc.h 文件。 打开 sram.c 文件,代码如下: #include "sram.h" #include "usart.h" //使用 NOR/SRAM 的 Bank1.sector3,地址位 HADDR[27,26]=10 //对 IS61LV25616/IS62WV25616,地址线范围为 A0~A17 //对 IS61LV51216/IS62WV51216,地址线范围为 A0~A18 #define Bank1_SRAM3_ADDR //初始化外部 SRAM ((u32)(0x68000000)) void FSMC_SRAM_Init(void) { FSMC_NORSRAMInitTypeDef FSMC_NSInitStructure; FSMC_NORSRAMTimingInitTypeDef readWriteTiming; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD|RCC_APB2Periph_GPIOE| RCC_APB2Periph_GPIOF|RCC_APB2Periph_GPIOG,ENABLE); 564 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_FSMC,ENABLE); GPIO_InitStructure.GPIO_Pin = 0xFF33; //PORTD 复用推挽输出 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOD, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = 0xFF83; //PORTE 复用推挽输出 GPIO_Init(GPIOE, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = 0xF03F; //PORTD 复用推挽输出 GPIO_Init(GPIOF, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = 0x043F; //PORTD 复用推挽输出 GPIO_Init(GPIOG, &GPIO_InitStructure); readWriteTiming.FSMC_AddressSetupTime = 0x00; //地址建立时间为 1 个 HCLK readWriteTiming.FSMC_AddressHoldTime = 0x00; //地址保持时间模式 A 未用到 readWriteTiming.FSMC_DataSetupTime = 0x03; //数据保持时间为 3 个 HCLK readWriteTiming.FSMC_BusTurnAroundDuration = 0x00; readWriteTiming.FSMC_CLKDivision = 0x00; readWriteTiming.FSMC_DataLatency = 0x00; readWriteTiming.FSMC_AccessMode = FSMC_AccessMode_A; //模式 A FSMC_NSInitStructure.FSMC_Bank = FSMC_Bank1_NORSRAM3;// BTCR[4],[5]。 FSMC_NSInitStructure.FSMC_DataAddressMux= FSMC_DataAddressMux_Disable; FSMC_NSInitStructure.FSMC_MemoryType =FSMC_MemoryType_SRAM //SRAM FSMC_NSInitStructure.FSMC_MemoryDataWidth= FSMC_MemoryDataWidth_16b; //存储器数据宽度为 16bit FSMC_NSInitStructure.FSMC_BurstAccessMode=FSMC_BurstAccessMode_Disable; FSMC_NSInitStructure.FSMC_WaitSignalPolarity=FSMC_WaitSignalPolarity_Low; FSMC_NSInitStructure.FSMC_AsynchronousWait=FSMC_AsynchronousWait_Disable; FSMC_NSInitStructure.FSMC_WrapMode = FSMC_WrapMode_Disable; FSMC_NSInitStructure.FSMC_WaitSignalActive= FSMC_WaitSignalActive_BeforeWaitState; FSMC_NSInitStructure.FSMC_WriteOperation = FSMC_WriteOperation_Enable; //存储器写使能 FSMC_NSInitStructure.FSMC_WaitSignal = FSMC_WaitSignal_Disable; FSMC_NSInitStructure.FSMC_ExtendedMode = FSMC_ExtendedMode_Disable; // 读写使用相同的时序 FSMC_NSInitStructure.FSMC_WriteBurst = FSMC_WriteBurst_Disable; FSMC_NSInitStructure.FSMC_ReadWriteTimingStruct = &readWriteTiming; FSMC_NSInitStructure.FSMC_WriteTimingStruct = &readWriteTiming; FSMC_NORSRAMInit(&FSMC_NSInitStructure); //初始化 FSMC 配置 FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM3, ENABLE); // 使能 BANK3 } 565 STM32F1 开发指南(库函数版) //在指定地址开始,连续写入 n 个字节. //pBuffer:字节指针 //WriteAddr:要写入的地址 //n:要写入的字节数 ALIENTEK 战舰 STM32F103 V3 开发板教程 void FSMC_SRAM_WriteBuffer(u8* pBuffer,u32 WriteAddr,u32 n) { for(;n!=0;n--) { *(vu8*)(Bank1_SRAM3_ADDR+WriteAddr)=*pBuffer; WriteAddr++; pBuffer++; } } //在指定地址开始,连续读出 n 个字节. //pBuffer:字节指针 //ReadAddr:要读出的起始地址 //n:要写入的字节数 void FSMC_SRAM_ReadBuffer(u8* pBuffer,u32 ReadAddr,u32 n) { for(;n!=0;n--) { *pBuffer++=*(vu8*)(Bank1_SRAM3_ADDR+ReadAddr); ReadAddr+=1; //址右移一位对齐.加 2 相当于加 1. } } 此部分代码包含 3 个函数,FSMC_SRAM_Init 函数用于初始化,包括 FSMC 相关 IO 口的 初始化以及 FSMC 配置。另外,FSMC_SRAM_WriteBuffer 和 FSMC_SRAM_ReadBuffer 这两个 函数分别用于在外部 SRAM 的指定地址写入和读取指定长度的数据(以字节为单位)。 这里需要注意的是:FSMC 当位宽为 16 位的时候,HADDR 右移一位同地址对齐,但是 ReadAddr 我们这里却没有加 2,而是加 1,是因为我们这里用的数据为宽是 8 位,通过 UB 和 LB 来控制高低字节位,所以地址在这里是可以只加 1 的。另外,因为我们使用的是 BANK1, 区域 3,所以外部 SRAM 的基址为:0x68000000。 sram.h 文件的内容就很简单,主要是一些函数申明,这里我们就不一一列出。 下面我们打开 main.c 文件,内容如下: u32 testsram[250000] __attribute__((at(0X68000000)));//测试用数组 //外部内存测试(最大支持 1M 字节内存测试) void fsmc_sram_test(u16 x,u16 y) { u32 i=0; u8 temp=0; u8 sval=0; //在地址 0 读到的数据 LCD_ShowString(x,y,239,y+16,16,"Ex Memory Test: 0KB"); 566 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 //每隔 4K 字节,写入一个数据,总共写入 256 个数据,刚好是 1M 字节 for(i=0;i<1024*1024;i+=4096) { FSMC_SRAM_WriteBuffer(&temp,i,1); temp++; } //依次读出之前写入的数据,进行校验 for(i=0;i<1024*1024;i+=4096) { FSMC_SRAM_ReadBuffer(&temp,i,1); if(i==0)sval=temp; else if(temp<=sval)break;//后面读出的数据一定要比第一次读到的数据大. LCD_ShowxNum(x+15*8,y,(u16)(temp-sval+1)*4,4,16,0);//显示内存容量 } } int main(void) { u8 key; u8 i=0; u32 ts=0; delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组 2 uart_init(115200); //串口初始化为 115200 LED_Init(); //初始化与 LED 连接的硬件接口 KEY_Init(); //初始化按键 LCD_Init(); FSMC_SRAM_Init(); POINT_COLOR=RED; //初始化 LCD //初始化外部 SRAM //设置字体为红色 LCD_ShowString(30,50,200,16,16,"WarShip STM32"); LCD_ShowString(30,70,200,16,16,"SRAM TEST"); LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(30,110,200,16,16,"2015/1/19"); LCD_ShowString(30,130,200,16,16,"KEY0:Test Sram"); LCD_ShowString(30,150,200,16,16,"KEY1:TEST Data"); POINT_COLOR=BLUE;//设置字体为蓝色 for(ts=0;ts<250000;ts++)testsram[ts]=ts;//预存测试数据 while(1) { key=KEY_Scan(0);//不支持连按 if(key==KEY0_PRES)fsmc_sram_test(30,170);//测试 SRAM 容量 else if(key==KEY1_PRES)//打印预存测试数据 { 567 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 for(ts=0;ts<250000;ts++)LCD_ShowxNum(30,190,testsram[ts],6,16,0);//显示数据 }else delay_ms(10); i++; if(i==20)//DS0 闪烁. { i=0; LED0=!LED0; } } } 此部分代码除了 mian 函数,还有一个 fsmc_sram_test 函数,该函数用于测试外部 SRAM 的容量大小,并显示其容量。main 函数则比较简单,我们就不细说了。 此段代码,我们定义了一个超大数组 testsram,我们指定该数组定义在外部 sram 起始地址 (__attribute__((at(0X68000000)))),该数组用来测试外部 SRAM 数据的读写。注意该数组的定 义方法,是我们推荐的使用外部 SRAM 的方法。如果想用 MDK 自动分配,那么需要用到分散 加载还需要添加汇编的 FSMC 初始化代码,相对来说比较麻烦。而且外部 SRAM 访问速度又远 不如内部 SRAM,如果将一些需要快速访问的 SRAM 定义到了外部 SRAM,将会严重拖慢程序 运行速度。而如果以我们推荐的方式来分配外部 SRAM,那么就可以控制 SRAM 的分配,可以 针对性的选择放外部还是放内部,有利于提高程序运行速度,使用起来也比较方便。 最后,我们将 fsmc_sram_test_write 和 fsmc_sram_test_read 函数加入 USMART 控制,这样, 我们就可以通过串口调试助手,测试外部 SRAM 任意地址的读写了。 41.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK 战舰 STM32 开发板上,得到如图 41.4.1 所示界面: 图 41.4.1 程序运行效果图 此时,我们按下 KEY1,就可以在 LCD 上看到内存测试的画面,同样,按下 WK_UP,就 可以看到 LCD 显示存放在数组 testsram 里面的测试数据,如图 41.4.2 所示: 568 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 图 41.4.2 外部 SRAM 测试界面 该实验我们还可以借助 USMART 来测试,有兴趣的同学可以测试一下。 569 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 第四十二章 内存管理实验 上一节,我们学会了使用 STM32 驱动外部 SRAM,以扩展 STM32 的内存,加上 STM32 本身自带的 64K 字节内存,我们可供使用的内存还是比较多的。如果我们所用的内存都像上一 节的 testsram 那样,定义一个数组来使用,显然不是一个好办法。 本章,我们将学习内存管理,实现对内存的动态管理。本章分为如下几个部分: 42.1 内存管理简介 42.2 硬件设计 42.3 软件设计 42.4 下载验证 570 42.1 内存管理简介 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 内存管理,是指软件运行时对计算机内存资源的分配和使用的技术。其最主要的目的是如 何高效,快速的分配,并且在适当的时候释放和回收内存资源。内存管理的实现方法有很多种, 他们其实最终都是要实现 2 个函数:malloc 和 free;malloc 函数用于内存申请,free 函数用于 内存释放。 本章,我们介绍一种比较简单的办法来实现:分块式内存管理。下面我们介绍一下该方法 的实现原理,如图 42.1.1 所示: 内存管理 内存块 内存块 内存块 1 2 3 …… 内存块 内存池 n 第1项 第2项 第3项 …… 第n项 内存管理表 分配方向 malloc,free等函数 图 42.1.1 分块式内存管理原理 从上图可以看出,分块式内存管理由内存池和内存管理表两部分组成。内存池被等分为 n 块,对应的内存管理表,大小也为 n,内存管理表的每一个项对应内存池的一块内存。 内存管理表的项值代表的意义为:当该项值为 0 的时候,代表对应的内存块未被占用,当 该项值非零的时候,代表该项对应的内存块已经被占用,其数值则代表被连续占用的内存块数。 比如某项值为 10,那么说明包括本项对应的内存块在内,总共分配了 10 个内存块给外部的某 个指针。 内寸分配方向如图所示,是从顶底的分配方向。即首先从最末端开始找空内存。当内存 管理刚初始化的时候,内存表全部清零,表示没有任何内存块被占用。 分配原理 当指针 p 调用 malloc 申请内存的时候,先判断 p 要分配的内存块数(m),然后从第 n 项开 始,向下查找,直到找到 m 块连续的空内存块(即对应内存管理表项为 0),然后将这 m 个内 存管理表项的值都设置为 m(标记被占用),最后,把最后的这个空内存块的地址返回指针 p, 完成一次分配。注意,如果当内存不够的时候(找到最后也没找到连续的 m 块空闲内存),则 返回 NULL 给 p,表示分配失败。 释放原理 当 p 申请的内存用完,需要释放的时候,调用 free 函数实现。free 函数先判断 p 指向的内 存地址所对应的内存块,然后找到对应的内存管理表项目,得到 p 所占用的内存块数目 m(内 存管理表项目的值就是所分配内存块的数目),将这 m 个内存管理表项目的值都清零,标记释 放,完成一次内存释放。 关于分块式内存管理的原理,我们就介绍到这里。 571 42.2 硬件设计 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 本章实验功能简介:开机后,显示提示信息,等待外部输入。KEY0 用于申请内存,每次 申请 2K 字节内存。KEY1 用于写数据到申请到的内存里面。KEY2 用于释放内存。WK_UP 用 于切换操作内存区(内部内存/外部内存)。DS0 用于指示程序运行状态。本章我们还可以通过 USMART 调试,测试内存管理函数。 本实验用到的硬件资源有: 1) 指示灯 DS0 2) 四个按键 3) 串口 4) TFTLCD 模块 5) IS62WV51216 这些我们都已经介绍过,接下来我们开始软件设计。 42.3 软件设计 本章,我们将内存管理部分单独做一个分组,在工程目录下新建一个 MALLOC 的文件夹, 然后新建 malloc.c 和 malloc.h 两个文件,将他们保存在 MALLOC 文件夹下。 在 MDK 新建一个 MALLOC 的组,然后将 malloc.c 文件加入到该组,并将 MALLOC 文件 夹添加到头文件包含路径。 打开 malloc.c 文件,代码如下: #include "malloc.h" //内存池(4 字节对齐) __align(4) u8 mem1base[MEM1_MAX_SIZE]; //内部 SRAM 内存池 __align(4) u8 mem2base[MEM2_MAX_SIZE] __attribute__((at(0X68000000))); //外部 SRAM 内存池 //内存管理表 u16 mem1mapbase[MEM1_ALLOC_TABLE_SIZE]; //内部 SRAM 内存池 MAP u16 mem2mapbase[MEM2_ALLOC_TABLE_SIZE] __attribute__((at(0X68000000+ MEM2_MAX_SIZE))); //外部 SRAM 内存池 MAP //内存管理参数 const u32 memtblsize[2]={MEM1_ALLOC_TABLE_SIZE,MEM2_ALLOC_TABLE_SIZE}; //内存表大小 const u32 memblksize[2]={MEM1_BLOCK_SIZE,MEM2_BLOCK_SIZE}; //内存分块大小 const u32 memsize[2]={MEM1_MAX_SIZE,MEM2_MAX_SIZE}; //内存总大小 //内存管理控制器 struct _m_mallco_dev mallco_dev= { mem_init, //内存初始化 mem_perused, mem1base,mem2base, mem1mapbase,mem2mapbase, //内存使用率 //内存池 //内存管理状态表 572 STM32F1 开发指南(库函数版) ALIENTEK 战舰 STM32F103 V3 开发板教程 0,0, //内存管理未就绪 }; //复制内存 //*des:目的地址 //*src:源地址 //n:需要复制的内存长度(字节为单位) void mymemcpy(void *des,void *src,u32 n) { u8 *xdes=des; u8 *xsrc=src; while(n--)*xdes++=*xsrc++; } //设置内存 //*s:内存首地址 //c :要设置的值 //count:需要设置的内存大小(字节为单位) void mymemset(void *s,u8 c,u32 count) { u8 *xs = s; while(count--)*xs++=c; } //内存管理初始化 //memx:所属内存块 void mem_init(u8 memx) { mymemset(mallco_dev.memmap[memx], 0,memtblsize[memx]*2);//内存状态表数据清零 mymemset(mallco_dev.membase[memx], 0,memsize[memx]); //内存池所有数据清零 mallco_dev.memrdy[memx]=1; //内存管理初始化 OK } //获取内存使用率 //memx:所属内存块 //返回值:使用率(0~100) u8 mem_perused(u8 memx) { u32 used=0; u32 i; for(i=0;i=0;offset--)//搜索整个内存控制区 { if(!mallco_dev.memmap[memx][offset])cmemb++;//连续空内存块数增加 else cmemb=0; //连续内存块清零 if(cmemb==nmemb) //找到了连续 nmemb 个空内存块 { for(i=0;i