首页资源分类嵌入式处理器ARM MCU > stm32库函数开发教程

stm32库函数开发教程

已有 445110个资源

下载专区

上传者其他资源

    文档信息举报收藏

    标    签:stm32库函数STM32

    分    享:

    文档简介

    stm32库函数stm32库函数stm32库函数stm32库函数

    文档预览

    ALIENTEK 战舰STM32开发板 STM32 开发指南 V1.2 −ALIENTEK 战舰 STM32 开发板库函数教程 官方店铺 1:http://shop62057469.taobao.com 官方店铺 2:http://shop62103354.taobao.com 技术论坛:www.openedv.com www.openedv.com I ALIENTEK 战舰STM32开发板 内容简介 ........................................................................................................................ I 前言 ................................................................................................................................2 第一篇 硬件篇 ..............................................................................................................4 第一章 实验平台简介 ..................................................................................................5 1.1 ALIENTEK 战舰 STM32 开发板资源初探 ........................................................ 5 1.2 ALIENTEK 战舰 STM32 开发板资源说明 ........................................................ 7 1.2.1 硬件资源说明 .................................................................................................... 7 1.2.2 软件资源说明 .................................................................................................. 12 第二章 实验平台硬件资源详解 ................................................................................14 2.1 开发板原理图详解 ............................................................................................ 14 2.1.1 MCU ................................................................................................................. 14 2.1.2 引出 IO 口 ....................................................................................................... 16 2.1.3 USB 串口/串口 1 选择接口 ............................................................................ 16 2.1.4 JTAG/SWD....................................................................................................... 17 2.1.5 SRAM............................................................................................................... 17 2.1.6 LCD/OLED 模块接口 ..................................................................................... 18 2.1.7 复位电路 ......................................................................................................... 19 2.1.8 启动模式设置接口 ......................................................................................... 19 2.1.9 RS232 串口 ...................................................................................................... 20 2.1.10 RS485 接口 .................................................................................................... 20 2.1.11 CAN/USB 接口 .............................................................................................. 21 2.1.12 EEPROM ........................................................................................................ 21 2.1.13 游戏手柄接口 ............................................................................................... 22 2.1.14 SPI FLASH..................................................................................................... 22 2.1.15 3D 加速度传感器 .......................................................................................... 23 2.1.16 温湿度传感器接口 ....................................................................................... 23 2.1.17 红外接收头 ................................................................................................... 23 2.1.18 无线模块接口 ............................................................................................... 24 2.1.19 LED ................................................................................................................ 24 2.1.20 按键 ............................................................................................................... 25 2.1.21 TPAD 电容触摸按键 ..................................................................................... 25 2.1.22 PS/2 接口........................................................................................................ 26 www.openedv.com II ALIENTEK 战舰STM32开发板 2.1.23 OLED/摄像头模块接口 ................................................................................ 26 2.1.24 有源蜂鸣器 ................................................................................................... 27 2.1.25 SD 卡/以太网模块接口 ................................................................................. 28 2.1.26 多功能端口 ................................................................................................... 29 2.1.27 音频选择 ....................................................................................................... 30 2.1.28 FM 收发 ......................................................................................................... 31 2.1.29 音频输出 ....................................................................................................... 31 2.1.30 音频编解码 ................................................................................................... 32 2.1.31 电源 ............................................................................................................... 32 2.1.33 USB 串口 ....................................................................................................... 34 2.2 开发板使用注意事项 ......................................................................................... 34 第二篇 软件篇 ............................................................................................................36 第三章 RVMDK 软件入门..........................................................................................37 3.1 STM32 官方固件库简介 .................................................................................... 37 3.1.1 库开发与寄存器开发的关系 ........................................................................ 37 3.1.2 STM32 固件库与 CMSIS 标准讲解 ............................................................. 38 3.1.3 STM32 官方库包介绍 ................................................................................... 39 3.1.3.1 文件夹介绍: .............................................................................................. 40 3.1.3.2 关键文件介绍: .......................................................................................... 41 3.2 RVMDK3.80A 简介............................................................................................ 42 3.3 新建基于固件库的 RVMDK 工程模板............................................................. 43 3.3.1 MDK3.8a 安装步骤 ........................................................................................ 43 3.3.2 添加 License Key ......................................................................................... 44 3.3.3 新建工程模板 ................................................................................................ 46 3.4 MDK 下的程序下载与调试............................................................................... 65 3.4.1 STM32 软件仿真 ............................................................................................. 65 3.4.2 STM32 程序下载 ............................................................................................. 70 3.4.3 STM32 硬件调试 ............................................................................................. 75 3.5 RVMDK 使用技巧.............................................................................................. 79 3.5.1 文本美化 ......................................................................................................... 79 3.5.2 代码编辑技巧 ................................................................................................. 83 3.5.3 其他小技巧 ..................................................................................................... 88 3.5.4 调试技巧 ......................................................................................................... 89 www.openedv.com III ALIENTEK 战舰STM32开发板 第四章 STM32 开发基础知识入门 ............................................................................93 4.1 MDK 下 C 语言基础复习 .................................................................................. 93 4.1.1 位操作 ............................................................................................................. 93 4.1.2 define 宏定义 ................................................................................................... 94 4.1.3 ifdef 条件编译.................................................................................................. 94 4.1.4 extern 变量申明 ............................................................................................... 95 4.1.5 typedef 类型别名 ............................................................................................. 96 4.1.6 结构体 ............................................................................................................. 96 4.2 STM32 系统架构 ................................................................................................ 98 4.3 STM32 时钟系统 ................................................................................................ 99 4.4 端口复用和重映射 .......................................................................................... 103 4.4.1 端口复用功能 ................................................................................................ 103 4.4.2 端口重映射 .................................................................................................... 104 4.5 STM32 NVIC 中断优先级管理 ....................................................................... 105 4.6 MDK 中寄存器地址名称映射分析................................................................. 108 4.7 MDK 固件库快速组织代码技巧..................................................................... 110 第五章 SYSTEM 文件夹介绍 .................................................................................116 5.1 delay 文件夹代码介绍 ..................................................................................... 116 5.1.1 delay_init 函数 ............................................................................................... 117 5.1.2 delay_us 函数 ................................................................................................. 118 5.1.3 delay_ms 函数................................................................................................ 120 5.2 sys 文件夹代码介绍 ......................................................................................... 121 5.2.1 IO 口的位操作实现 .................................................................................... 121 5.2.2 中断分组设置函数 ....................................................................................... 123 5.3 usart 文件夹介绍 .............................................................................................. 123 5.3.1 printf 函数支持 .............................................................................................. 124 5.3.2 uart_init 函数.................................................................................................. 124 5.3.3 USART1_IRQHandler 函数........................................................................... 127 第三篇 实战篇 ..........................................................................................................130 第六章 跑马灯实验 ..................................................................................................131 6.1 STM32 IO 简介................................................................................................. 132 6.2 硬件设计 .......................................................................................................... 139 www.openedv.com IV ALIENTEK 战舰STM32开发板 6.3 软件设计 .......................................................................................................... 139 6.4 仿真与下载 ...................................................................................................... 144 第七章 蜂鸣器实验 ..................................................................................................147 7.1 蜂鸣器简介 ...................................................................................................... 148 7.2 硬件设计 .......................................................................................................... 148 7.3 软件设计 .......................................................................................................... 149 7.4 仿真与下载 ...................................................................................................... 152 第八章 按键输入实验 ..............................................................................................154 8.1 STM32 IO 口简介............................................................................................. 155 8.2 硬件设计 .......................................................................................................... 155 8.3 软件设计 .......................................................................................................... 155 8.4 仿真与下载 ...................................................................................................... 158 第九章 串口实验 ......................................................................................................163 9.1 STM32 串口简介 .............................................................................................. 164 9.2 硬件设计 .......................................................................................................... 166 9.3 软件设计 .......................................................................................................... 167 9.4 下载验证 .......................................................................................................... 170 第十章 外部中断实验 ..............................................................................................173 10.1 STM32 外部中断简介 .................................................................................... 174 10.2 硬件设计 ........................................................................................................ 177 10.3 软件设计 ........................................................................................................ 177 10.4 下载验证 ........................................................................................................ 179 第十一章 独立看门狗(IWDG)实验 ...................................................................180 11.1 STM32 独立看门狗简介 ................................................................................ 181 11.2 硬件设计 ........................................................................................................ 182 11.3 软件设计 ........................................................................................................ 183 11.4 下载验证 ........................................................................................................ 184 第十二章 窗口门狗(WWDG)实验.....................................................................185 12.1 STM32 窗口看门狗简介 ................................................................................ 186 12.2 硬件设计 ........................................................................................................ 188 12.3 软件设计 ........................................................................................................ 189 12.4 下载验证 ........................................................................................................ 190 www.openedv.com V ALIENTEK 战舰STM32开发板 第十三章 定时器中断实验 ......................................................................................191 13.1 STM32 通用定时器简介 ................................................................................ 192 13.2 硬件设计 ........................................................................................................ 197 13.3 软件设计 ........................................................................................................ 197 13.4 下载验证 ........................................................................................................ 199 第十四章 PWM 输出实验........................................................................................200 14.1 PWM 简介....................................................................................................... 201 14.2 硬件设计 ........................................................................................................ 204 14.3 软件设计 ........................................................................................................ 204 14.4 下载验证 ........................................................................................................ 206 第十五章 输入捕获实验 ..........................................................................................208 15.1 输入捕获简介 ................................................................................................ 209 15.2 硬件设计 ........................................................................................................ 213 15.3 软件设计 ........................................................................................................ 213 15.4 下载验证 ........................................................................................................ 217 第十六章 电容触摸按键实验 ..................................................................................219 16.1 电容触摸按键简介 ........................................................................................ 220 16.2 硬件设计 ........................................................................................................ 221 16.3 软件设计 ........................................................................................................ 221 16.4 下载验证 ........................................................................................................ 226 第十七章 OLED 显示实验 ......................................................................................227 17.1 OLED 简介 ..................................................................................................... 228 17.2 硬件设计 ........................................................................................................ 234 17.3 软件设计 ........................................................................................................ 235 17.4 下载验证 ........................................................................................................ 242 第十八章 TFTLCD 显示实验 ..................................................................................244 18.1 TFTLCD&FSMC 简介 ................................................................................... 245 18.1.1 TFTLCD 简介 .............................................................................................. 245 18.1.2 FSMC 简介 .................................................................................................. 249 18.2 硬件设计 ........................................................................................................ 257 18.3 软件设计 ........................................................................................................ 258 18.4 下载验证 ........................................................................................................ 268 www.openedv.com VI ALIENTEK 战舰STM32开发板 第十九章 USMART 调试组件实验.........................................................................270 19.1 USMART 调试组件简介................................................................................ 271 19.2 硬件设计 ........................................................................................................ 274 19.3 软件设计 ........................................................................................................ 274 19.4 下载验证 ........................................................................................................ 278 第二十章 RTC 实时时钟实验..................................................................................282 20.1 STM32 RTC 时钟简介 ................................................................................... 283 20.2 硬件设计 ........................................................................................................ 289 20.3 软件设计 ........................................................................................................ 289 20.4 下载验证 ........................................................................................................ 296 第二十一章 待机唤醒实验 ......................................................................................297 21.1 STM32 待机模式简介 .................................................................................... 298 21.2 硬件设计 ........................................................................................................ 301 21.3 软件设计 ........................................................................................................ 301 21.4 下载与测试 .................................................................................................... 304 第二十二章 ADC 实验.............................................................................................305 22.1 STM32 ADC 简介 .......................................................................................... 306 22.2 硬件设计 ........................................................................................................ 314 22.3 软件设计 ........................................................................................................ 314 22.4 下载验证 ........................................................................................................ 317 第二十三章 内部温度传感器实验 ..........................................................................318 23.1 STM32 内部温度传感器简介 ....................................................................... 319 23.2 硬件设计 ........................................................................................................ 319 23.3 软件设计 ........................................................................................................ 320 23.4 下载验证 ........................................................................................................ 321 第二十四章 DAC 实验.............................................................................................323 24.1 STM32 DAC 简介 .......................................................................................... 324 24.2 硬件设计 ........................................................................................................ 329 24.3 软件设计 ........................................................................................................ 330 24.4 下载验证 ........................................................................................................ 332 第二十五章 PWM DAC 实验 ..................................................................................334 25.1 PWM DAC 简介 ............................................................................................. 335 www.openedv.com VII ALIENTEK 战舰STM32开发板 25.2 硬件设计 ........................................................................................................ 336 25.3 软件设计 ........................................................................................................ 337 25.4 下载验证 ........................................................................................................ 340 第二十六章 DMA 实验............................................................................................343 26.1 STM32 DMA 简介.......................................................................................... 344 26.2 硬件设计 ........................................................................................................ 349 26.3 软件设计 ........................................................................................................ 349 26.4 下载验证 ........................................................................................................ 352 第二十七章 IIC 实验................................................................................................355 27.1 IIC 简介........................................................................................................... 356 27.2 硬件设计 ........................................................................................................ 356 27.3 软件设计 ........................................................................................................ 357 27.4 下载验证 ........................................................................................................ 364 第二十八章 SPI 实验 ..............................................................................................366 28.1 SPI 简介 ......................................................................................................... 367 28.2 硬件设计 ........................................................................................................ 370 28.3 软件设计 ........................................................................................................ 371 28.4 下载验证 ........................................................................................................ 376 第二十九章 485 实验 ..............................................................................................378 29.1 485 简介 ......................................................................................................... 379 29.2 硬件设计 ........................................................................................................ 380 29.3 软件设计 ........................................................................................................ 381 29.4 下载验证 ........................................................................................................ 385 第三十章 CAN 通讯实验.........................................................................................387 30.1 CAN 简介........................................................................................................ 388 30.1.1 CAN 发送流程............................................................................................. 397 30.1.2 CAN 接收流程............................................................................................. 397 30.2 硬件设计 ........................................................................................................ 407 30.3 软件设计 ........................................................................................................ 408 30.4 下载验证 ........................................................................................................ 413 第三十一章 触摸屏实验 ..........................................................................................416 31.1 触摸屏简介 .................................................................................................... 417 www.openedv.com VIII ALIENTEK 战舰STM32开发板 31.2 硬件设计 ........................................................................................................ 418 31.3 软件设计 ........................................................................................................ 418 31.4 下载验证 ........................................................................................................ 426 第三十二章 红外遥控实验 ....................................................................................429 32.1 红外遥控简介 ................................................................................................. 430 32.2 硬件设计 ........................................................................................................ 431 32.3 软件设计 ........................................................................................................ 432 32.4 下载验证 ........................................................................................................ 437 第三十三章 游戏手柄实验 ....................................................................................439 33.1 游戏手柄简介 ................................................................................................. 440 33.2 硬件设计 ........................................................................................................ 441 33.3 软件设计 ........................................................................................................ 443 33.4 下载验证 ........................................................................................................ 445 第三十四章 三轴加速度传感器实验 ......................................................................447 34.1 ADXL345 简介 ............................................................................................... 448 34.2 硬件设计 ........................................................................................................ 450 34.3 软件设计 ........................................................................................................ 451 34.4 下载验证 ........................................................................................................ 457 第三十五章 DS18B20 数字温度传感器实验 .......................................................460 35.1 DS18B20 简介 ................................................................................................ 461 35.2 硬件设计 ........................................................................................................ 462 35.3 软件设计 ........................................................................................................ 463 35.4 下载验证 ........................................................................................................ 468 第三十六章 DHT11 数字温湿度传感器实验 .......................................................469 36.1 DHT11 简介 .................................................................................................... 470 36.2 硬件设计 ........................................................................................................ 472 36.3 软件设计 ........................................................................................................ 472 36.4 下载验证 ........................................................................................................ 476 第三十七章 无线通信实验 ....................................................................................478 37.1 NRF24L01 无线模块简介 .............................................................................. 479 37.2 硬件设计 ........................................................................................................ 479 37.3 软件设计 ........................................................................................................ 480 www.openedv.com IX ALIENTEK 战舰STM32开发板 37.4 下载验证 ........................................................................................................ 488 第三十八章 PS2 鼠标实验.....................................................................................490 38.1 PS/2 简介......................................................................................................... 491 38.2 硬件设计 ........................................................................................................ 493 38.3 软件设计 ........................................................................................................ 494 38.4 下载验证 ........................................................................................................ 504 第三十九章 FLASH 模拟 EEPROM 实验 ..............................................................505 39.1 STM32 FLASH 简介 ...................................................................................... 506 39.2 硬件设计 ........................................................................................................ 512 39.3 软件设计 ........................................................................................................ 512 39.4 下载验证 ........................................................................................................ 516 第四十章 FM 收发实验 ...........................................................................................518 40.1 RDA5820 简介................................................................................................ 519 40.2 硬件设计 ........................................................................................................ 520 40.3 软件设计 ........................................................................................................ 522 40.4 下载验证 ........................................................................................................ 531 第四十一章 摄像头实验 ..........................................................................................532 41.1 OV7670 简介 .................................................................................................. 533 41.2 硬件设计 ........................................................................................................ 537 41.3 软件设计 ........................................................................................................ 539 41.4 下载验证 ........................................................................................................ 547 第四十二章 外部 SRAM 实验.................................................................................549 42.1 IS62WV51216 简介 ........................................................................................ 550 42.2 硬件设计 ........................................................................................................ 552 42.3 软件设计 ........................................................................................................ 552 42.4 下载验证 ........................................................................................................ 556 第四十三章 内存管理实验 ......................................................................................558 43.1 内存管理简介 ................................................................................................ 559 43.2 硬件设计 ........................................................................................................ 560 43.3 软件设计 ........................................................................................................ 560 43.4 下载验证 ........................................................................................................ 567 第四十四章 SD 卡实验 ..........................................................................................569 www.openedv.com X ALIENTEK 战舰STM32开发板 44.1 SD 卡简介 ....................................................................................................... 570 44.2 硬件设计 ........................................................................................................ 573 44.3 软件设计 ........................................................................................................ 574 44.4 下载验证 ........................................................................................................ 578 第四十五章 FATFS 实验........................................................................................580 45.1 FATFS 简介 ..................................................................................................... 581 45.2 硬件设计 ........................................................................................................ 586 45.3 软件设计 ........................................................................................................ 586 45.4 下载验证 ........................................................................................................ 593 第四十六章 汉字显示实验 ......................................................................................595 46.1 汉字显示原理简介 ........................................................................................ 596 46.2 硬件设计 ........................................................................................................ 600 46.3 软件设计 ........................................................................................................ 600 46.4 下载验证 ........................................................................................................ 609 第四十七章 图片显示实验 ......................................................................................611 47.1 图片格式简介 ................................................................................................ 612 47.2 硬件设计 ........................................................................................................ 613 47.3 软件设计 ........................................................................................................ 614 47.4 下载验证 ........................................................................................................ 622 第四十八章 照相机实验 ..........................................................................................623 48.1 BMP 编码简介................................................................................................ 624 48.2 硬件设计 ........................................................................................................ 626 48.3 软件设计 ........................................................................................................ 627 48.4 下载验证 ........................................................................................................ 633 第四十九章 音乐播放器实验 ..................................................................................634 49.1 VS1053 简介 ................................................................................................... 635 49.2 硬件设计 ........................................................................................................ 640 49.3 软件设计 ........................................................................................................ 640 49.4 下载验证 ........................................................................................................ 645 第五十章 录音机实验 ..............................................................................................647 50.1 WAV 简介........................................................................................................ 648 50.2 硬件设计 ........................................................................................................ 651 www.openedv.com XI ALIENTEK 战舰STM32开发板 50.3 软件设计 ........................................................................................................ 651 50.4 下载验证 ........................................................................................................ 657 第五十一章 手写识别实验 ......................................................................................660 51.1 手写识别简介 ................................................................................................ 661 51.2 硬件设计 ........................................................................................................ 665 51.3 软件设计 ........................................................................................................ 665 51.4 下载验证 ........................................................................................................ 668 第五十二章 T9 拼音输入法实验.............................................................................670 52.1 拼音输入法简介 ............................................................................................ 671 52.2 硬件设计 ........................................................................................................ 673 52.3 软件设计 ........................................................................................................ 673 52.4 下载验证 ........................................................................................................ 680 第五十三章 串口 IAP 实验......................................................................................683 53.1 IAP 简介.......................................................................................................... 684 53.2 硬件设计 ........................................................................................................ 689 53.3 软件设计 ........................................................................................................ 690 53.4 下载验证 ........................................................................................................ 696 第五十四章 触控 USB 鼠标实验 ............................................................................699 54.1 USB 简介 ........................................................................................................ 700 54.2 硬件设计 ........................................................................................................ 701 54.3 软件设计 ........................................................................................................ 702 54.4 下载验证 ........................................................................................................ 707 第五十五章 USB 读卡器实验 .................................................................................710 55.1 USB 读卡器简介 ............................................................................................ 711 55.2 硬件设计 ........................................................................................................ 711 55.3 软件设计 ........................................................................................................ 712 55.4 下载验证 ........................................................................................................ 715 第五十六章 USB 声卡实验 .....................................................................................717 56.1 USB 声卡简介 ................................................................................................ 718 56.2 硬件设计 ........................................................................................................ 718 56.3 软件设计 ........................................................................................................ 719 56.4 下载验证 ........................................................................................................ 720 www.openedv.com XII ALIENTEK 战舰STM32开发板 第五十七章 ENC28J60 网络实验............................................................................723 57.1 ENC28J60 以及 uIP 简介 ............................................................................... 724 57.1.1 ENC28J60 简介............................................................................................ 724 57.1.2 uIP 简介........................................................................................................ 726 57.2 硬件设计 ........................................................................................................ 729 57.3 软件设计 ........................................................................................................ 730 57.4 下载验证 ........................................................................................................ 743 第五十八章 UCOSII 实验 1-任务调度....................................................................748 58.1 UCOSII 简介................................................................................................... 749 58.2 硬件设计 ........................................................................................................ 754 58.3 软件设计 ........................................................................................................ 754 58.4 下载验证 ........................................................................................................ 758 58.5 任务删除,挂起和恢复测试 ........................................................................ 758 第五十九章 UCOSII 实验 2-信号量和邮箱............................................................763 59.1 UCOSII 信号量和邮箱简介........................................................................... 764 59.2 硬件设计 ........................................................................................................ 766 59.3 软件设计 ........................................................................................................ 766 59.4 下载验证 ........................................................................................................ 772 第六十章 UCOSII 实验 3-消息队列、信号量集和软件定时器............................773 60.1 UCOSII 消息队列、信号量集和软件定时器简介....................................... 774 60.2 硬件设计 ........................................................................................................ 781 60.3 软件设计 ........................................................................................................ 781 60.4 下载验证 ........................................................................................................ 789 第六十一章 战舰 STM32 开发板综合实验............................................................791 61.1 战舰 STM32 开发板综合实验简介.............................................................. 792 61.2 战舰 STM32 开发板综合实验详解............................................................... 793 61.2.1 电子图书 ..................................................................................................... 795 61.2.2 数码相框 ..................................................................................................... 797 61.2.3 音乐播放 ..................................................................................................... 798 61.2.4 应用中心 ..................................................................................................... 800 61.2.5 时钟 ............................................................................................................. 801 61.2.6 系统设置 ..................................................................................................... 802 www.openedv.com XIII ALIENTEK 战舰STM32开发板 61.2.7 FC 游戏机 .................................................................................................... 809 61.2.8 收音机 ......................................................................................................... 811 61.2.9 记事本 ......................................................................................................... 812 61.2.10 运行器 ....................................................................................................... 814 61.2.11 3D ............................................................................................................... 815 61.2.12 手写画笔 ................................................................................................... 815 61.2.13 照相机 ....................................................................................................... 818 61.2.14 录音机 ....................................................................................................... 820 61.2.15 USB 连接 ................................................................................................... 822 61.2.16 TOM 猫 ...................................................................................................... 823 61.2.17 无线传书 ................................................................................................... 823 61.2.18 计算器 ....................................................................................................... 825 www.openedv.com XIV ALIENTEK 战舰STM32开发板 内容简介 本开发指南将由浅入深,带领大家进入 STM32 的世界。本指南总共分为三篇:1,硬件篇, 主要介绍本指南的实验平台;2,软件篇,主要介绍 STM32 开发软件的使用以及一些下载调试 的技巧,并详细介绍了几个常用的系统文件(程序);3,实战篇,主要通过基于 STM32 固件 库版本 V3.5 编写的 56 个实例带领大家一步步深入 STM32 的学习。 本指南为 ALIENTEK 战舰 STM32 开发板的固件库版本配套教程,在开发板配套的光盘里 面,有详细原理图以及所有实例的完整代码,这些代码都有详细的注释,所有源码都经过我们 严格测试,不会有任何警告和错误,另外,源码有我们生成好的 hex 文件,大家只需要通过串 口下载到开发板即可看到实验现象,亲自体验实验过程。 本指南不仅非常适合广大学生和电子爱好者学习 STM32,其大量的实验以及详细的解说,, 也可供从事智能仪器仪表、自动控制、电力电子、机电一体化等专业的技术人员参考。 www.openedv.com I ALIENTEK 战舰STM32开发板 前言 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 权威指南》两者的优点,并从固件库级 www.openedv.com 2 ALIENTEK 战舰STM32开发板 别出发,深入浅出,向读者展示 STM32 的各种功能。总共配有 56 个实例,基本上每个实例在 均配有软硬件设计,在介绍完软硬件之后,马上附上实例代码,并带有详细注释及说明,让读 者快速理解代码。 这些实例涵盖了 STM32 的绝大部分内部资源,并且提供很多实用级别的程序,如:内存 管理、拼音输入法、手写识别、图片解码、IAP 等。所有实例在 MDK3.80A 编译器下编译通过, 大家只需下载程序到 ALIENTEK 战舰 STM32 开发板,即可验证实验。 不管你是一个 STM32 初学者,还是一个老手,本指南都非常适合。尤其对于初学者,本 指南将手把手的教你如何使用 MDK,包括新建工程、编译、仿真、下载调试等一系列步骤, 让你轻松上手。本指南适用于想通过库函数学习 STM32 的读者,大家可以结合官方提供的库 函数实例对照学习。 本指南的实验平台是 ALIENTEK 战舰 STM32 开发板,有这款开发板的朋友则直接可以拿 本指南配套的光盘上的例程在开发板上运行、验证。而没有这款开发板而又想要的朋友,可以 上淘宝购买。当然你如果有了一款自己的开发板,而又不想再买,也是可以的,只要你的板子 上有 ALIENTEK 战舰 STM32 开发板上的相同资源(需要实验用到的),代码一般都是可以通 用的,你需要做的就只是把底层的驱动函数(一般是 IO 操作)稍做修改,使之适合你的开发 板即可。 俗话说:人无完人。本指南也不例外,在编写过程中虽然得到了不少网友的指正,但难免 不会再有出错的地方,如果大家发现指南中有什么错误的地方,还请告诉我们,我们的官方技 术支持论坛为 www.openedv.com(开源电子网),大家可以在论坛上发帖提问。在此先向各位朋 友表示衷心的感谢。 ALIENTEK//广州市星翼电子科技有限公司 www.openedv.com 3 ALIENTEK 战舰STM32开发板 第一篇 硬件篇 实践出真知,要想学好 STM32,实验平台必不可少!本篇将详细介绍我们用来学习 STM32 的硬件平台:ALIENTEK 战舰 STM32 开发板,通过该篇的介绍,你将了解到我们的学习平台 ALIENTEK 战舰 STM32 开发板的功能及特点。 为了让读者更好的使用 ALIENTEK 战舰 STM32 开发板,本篇还介绍了开发板的一些使用 注意事项,请读者在使用开发板的时候一定要注意。 本篇将分为如下两章: 1,实验平台简介; 2,实验平台硬件资源详解; www.openedv.com 4 ALIENTEK 战舰STM32开发板 第一章 实验平台简介 本章,主要向大家简要介绍我们的实验平台:ALIENTEK 战舰 STM32 开发板。通过本章 的学习,你将对我们后面使用的实验平台有个大概了解,为后面的学习做铺垫。 本章将分为如下两节: 1.1,ALIENTEK 战舰 STM32 开发板资源初探; 1.2,ALIENTEK 战舰 STM32 开发板资源说明; 1.1 ALIENTEK 战舰 STM32 开发板资源初探 在 ALIENTEK 战舰 STM32 开发板之前,ALIENTEK 推出过 MiniSTM32 开发板,在两年 的时间里面,售出 10000 多套,连续一年多稳居淘宝 STM32 开发板销量之首(目前仍是销量 第一)。而这款战舰 STM32 开发板,则是 MiniSTM32 开发板的超级加强版。下面我们开始介 绍战舰 STM32 开发板。 ALIENTEK 战舰 STM32 开发板的资源图如图 1.1.1 所示: NRF24L01 CAN LCD/OLED 模块接口 总线接口 模块接口 游戏手柄 接口 PS/2 鼠标/ 24C02 键盘接口 EEPROM RS232 接口 RS232/485 RS485 选择接口 总线接口 W25Q64 64M FLASH IS62WV51216 8M SRAM SD 卡/网络模块 接口选择接口 引出 IO 口 CAN/USB 选择接 口 JTAG/SWD 接口 USB 串口/串口 1 STM32F103ZET6 STM32 USB 口 后备电池接口 USB 转串口 OLED/摄像头 模块接口 有源 蜂鸣器 红外 接收头 FM 收发天线接口 DC6~16V 电源输入 电源开关 ADXL345 加速度 传感器 3.3V 电源输入/输出 5V 电源输入/输出 VS1053 IIS 输出口 耳机输出接口 MIC 选择口 录音输入接口 MIC(咪头) 多功能端口 电源指示灯 触摸按钮 DS18B20/D HT11 接口 引出 IO 口 2个 LED 灯 启动选 复位 择端口 按钮 参考电压 选择端口 WK_UP 及 3 个普通按钮 图 1.1.1 战舰 STM32 开发板资源图 从图 1.1.1 可以看出,ALIENTEK 战舰 STM32 开发板,资源十分丰富,并把 STM32F103 的内部资源发挥到了极致,基本所有 STM32F103 的内部资源,都可以在此开发板上验证,同 时扩充丰富的接口和功能模块,整个开发板显得十分大气。 www.openedv.com 5 ALIENTEK 战舰STM32开发板 开发板的外形尺寸为 11.2cm*15.6cm 大小,板子的设计充分考虑了人性化设计,并结合广 大客户对 Mini 板提出的改进意见,经过反复修改(在面市之前,硬件就改版了 8 次之多,目前 版本为 V2.0),最终确了定这样的设计。 ALIENTEK 战舰 STM32 开发板板载资源如下: ◆ CPU:STM32F103ZET6,LQFP144,FLASH:512K,SRAM:64K; ◆ 外扩 SRAM:IS62WV51216,1M 字节 ◆ 外扩 SPI FLASH:W25Q64,8M 字节 ◆ 1 个电源指示灯(蓝色) ◆ 2 个状态指示灯(DS0:红色,DS1:绿色) ◆ 1 个红外接收头,并配备一款小巧的红外遥控器 ◆ 1 个 EEPROM 芯片,24C02,容量 256 字节 ◆ 1 个重力加速度传感器芯片,ADXL345 ◆ 1 个高性能音频编解码芯片,VS1053 ◆ 1 个 FM 立体声收发芯片,RDA5820 ◆ 1 个 2.4G 无线模块接口(NRF24L01) ◆ 1 路 CAN 接口,采用 TJA1050 芯片 ◆ 1 路 485 接口,采用 SP3485 芯片 ◆ 1 路 RS232 接口,采用 SP3232 芯片 ◆ 1 个 PS/2 接口,可外接鼠标、键盘 ◆ 1 个游戏手柄接口,可以直接插 FC(红白机)游戏手柄 ◆ 1 路数字温湿度传感器接口,支持 DS18B20 /DHT11 等 ◆ 1 个标准的 2.4/2.8/3.5 寸 LCD 接口,支持触摸屏 ◆ 1 个摄像头模块接口 ◆ 2 个 OLED 模块接口 ◆ 1 个 USB 串口,可用于程序下载和代码调试(USMART 调试) ◆ 1 个 USB SLAVE 接口,用于 USB 通信 ◆ 1 个有源蜂鸣器 ◆ 1 个 FM 收发天线接口,并配天线 ◆ 1 个 RS232/RS485 选择接口 ◆ 1 个 CAN/USB 选择接口 ◆ 1 个串口选择接口 ◆ 1 个 SD 卡接口(在板子背面,支持 SPI/SDIO) ◆ 1 个 SD 卡/网络模块选择接口 ◆ 1 个标准的 JTAG/SWD 调试下载口 ◆ 1 个 VS1053 的 IIS 输出接口 ◆ 1 个 MIC/LINE IN 选择接口 ◆ 1 个录音头(MIC/咪头) ◆ 1 路立体声音频输出接口 ◆ 1 路立体声录音输入接口 ◆ 1 组多功能端口(DAC/ADC/PWM DAC/AUDIO IN/TPAD) ◆ 1 组 5V 电源供应/接入口 ◆ 1 组 3.3V 电源供应/接入口 ◆ 1 个参考电压设置接口 www.openedv.com 6 ALIENTEK 战舰STM32开发板 ◆ 1 个直流电源输入接口(输入电压范围:6~16V) ◆ 1 个启动模式选择配置接口 ◆ 1 个 RTC 后备电池座,并带电池 ◆ 1 个复位按钮,可用于复位 MCU 和 LCD ◆ 4 个功能按钮,其中 WK_UP 兼具唤醒功能 ◆ 1 个电容触摸按键 ◆ 1 个电源开关,控制整个板的电源 ◆ 独创的一键下载功能 ◆ 除晶振占用的 IO 口外,其余所有 IO 口全部引出 ALIENTEK 战舰 STM32 开发板的特点包括: 1) 接口丰富。板子提供十来种标准接口,可以方便的进行各种外设的实验和开发。 2) 设计灵活。板上很多资源都可以灵活配置,以满足不同条件下的使用。我们引出了除晶 振占用的 IO 口外的所有 IO 口,可以极大的方便大家扩展及使用。另外板载一键下载 功能,可避免频繁设置 B0、B1 的麻烦,仅通过 1 根 USB 线即可实现 STM32 的开发。 3) 资源充足。外扩 1M 字节 SRAM 和 8M 字节 FLASH,满足大内存需求和大数据存储。 板载 MP3 和 FM 收发芯片,娱乐学习两不误。板载 3D 加速度传感器和各种接口芯片, 满足各种应用需求。 4) 人性化设计。各个接口都有丝印标注,使用起来一目了然;接口位置设计安排合理,方 便顺手。资源搭配合理,物尽其用。 1.2 ALIENTEK 战舰 STM32 开发板资源说明 资源说明部分,我们将分为两个部分说明:硬件资源说明和软件资源说明。 1.2.1 硬件资源说明 这里我们首先详细介绍战舰 STM32 开发板的各个部分(图 1.1.1 中的标注部分)的硬件资 源,我们将按逆时针的顺序依次介绍。 1. W25Q64 64M FALSH 这是开发板外扩的 SPI FLASH 芯片,容量为 64Mbit,也就是 8M 字节,可用于存储字库和 其他用户数据,满足大容量数据存储要求。当然如果觉得 8M 字节还不够用,你可以把数据存 放在外部 SD 卡。 2. IS62WV51216 8M SRAM 这是开发板外扩的 SRAM 芯片,容量为 8M 位,也就是 1M 字节,这样,对大内存需求的 应用(比如 GUI),就可以很好的实现了。 3. SD 卡/网络模块接口选择接口 这里是一个由 3 拍排针(在板上标号[下同]为:P10、P11 和 P12)组成的复合接口,当不 用网络模块的时候,这个组合就变成了 SD 卡的接口选择接口,可以通过跳线帽选择 SDIO/SPI (我们默认是设置在 SPI 接口的)。但是,如果需要网络模块(网络模块接 P12),那么 SD 卡 就只能用 SDIO 模式了。 4. 引出 IO 口 这里是一组 54 个 IO 口的引出(P5),在它的右侧不远,是另外一组 54 个 IO 口的引出(P4), 这两组排针引出 108 个 IO,而 STM32F103ZET6 总共只有 112 个 IO,除去 RTC 晶振占用的 2 个 IO,还剩下 PA9 和 PA10 没有在这里引出(由 P6 引出)。 www.openedv.com 7 ALIENTEK 战舰STM32开发板 5. CAN/USB 选择接口 这是一个 USB/CAN 的选择接口(P13),因为 STM32 的 USB 和 CAN 是共用一组 IO(PA11 和 PA12),所以我们通过跳线帽来选择不同的功能,以实现 USB/CAN 的实验。 6. JTAG/SWD 接口 这是 ALIENTEK 战舰 STM32 开发板板载的 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 进行连接的接口(P6),标号 RXD 和 TXD 是 USB 转串口的 2 个数据口(对 CH340G 来说),而 PA9(TXD)和 PA10(RXD)则是 STM32 的串口 1 的两个数据口(复用功能下)。他们通过跳线帽对接,就可以和连接在一起了,从而实现 STM32 的程序下载以及串口通信。 设计成 USB 串口,是出于现在电脑上串口正在消失,尤其是笔记本,几乎清一色的没有串 口。所以板载了 USB 串口可以方便大家下载代码和调试。而在板子上并没有直接连接在一起, 则是出于使用方便的考虑。这样设计,你可以把 ALIENTEK 战舰 STM32 开发板当成一个 USB 串口,来和其他板子通信,而其他板子的串口,也可以方便地接到 ALIENTEK 战舰 STM32 开 发板上。 8. STM32F103ZET6 这是开发板的核心芯片(U5),型号为:STM32F103ZET6。该芯片具有 64KB SRAM、512KB FLASH、2 个基本定时器、4 个通用定时器、2 个高级定时器、3 个 SPI、2 个 IIC、5 个串口、1 个 USB、1 个 CAN、3 个 12 位 ADC、1 个 12 位 DAC、1 个 SDIO 接口、1 个 FSMC 接口以及 112 个通用 IO 口。 9. STM32 USB 口 这是开发板板载的一个 MiniUSB 头(USB),用于 STM32 与电脑的 USB 通讯,通过此 MiniUSB 头,开发板就可以和电脑进行 USB 通信了。开发板总共板载了 2 个 MiniUSB 头,一 个用于 USB 转串口,连接 CH340G 芯片;另外一个用于 STM32 内带的 USB。 同时开发板可以通过此 MiniUSB 头供电,板载两个 MiniUSB 头(不共用),主要是考虑了 使用的方便性,以及可以给板子提供更大的电流(两个 USB 都接上)这两个因素。 10. 后备电池接口 这是 STM32 后备区域的供电接口,可以用来给 STM32 的后备区域提供能量,在外部电源 断电的时候,维持后备区域数据的存储,以及 RTC 的运行。 11. USB 转串口 这是开发板板载的另外一个 MiniUSB 头(USB_232),用于 USB 连接 CH340G 芯片,从而 实现 USB 转串口。同时,此 MiniUSB 接头也是开发板电源的主要提供口。 12. OLED/摄像头模块接口 这是开发板板载的一个 OLED/摄像头模块接口(P8),如果是 OLED 模块,靠左插即可(右 边两个孔位悬空)。如果是摄像头模块(ALIENTEK 提供),则刚好插满。通过这个接口,可以 分别连接 2 个外部模块,从而实现相关实验。 13. 有源蜂鸣器 这是开发板的板载蜂鸣器(BEEP),可以实现简单的报警/闹铃。让开发板可以听得见。 www.openedv.com 8 ALIENTEK 战舰STM32开发板 14. 红外接收头 这是开发板的红外接收头(U14),可以实现红外遥控功能,通过这个接收头,可以接受市 面常见的各种遥控器的红外信号,大家甚至可以自己实现万能红外解码。当然,如果应用得当, 该接收头也可以用来传输数据。 战舰 STM32 开发板给大家配备了一个小巧的红外遥控器,该遥控器外观如图 1.2.1 所示: 图 1.2.1 红外遥控器 15. DS18B20/DHT11 接口 这 是 开 发 板 的 一 个 复 用 接 口 ( U13 ), 该 接 口 由 4 个 镀 金 排 孔 组 成 , 可 以 用 来 接 DS18B20/DS1820 等数字温度传感器。也可以用来接 DHT11 这样的数字温湿度传感器。实现一 个接口,2 个功能。不用的时候,大家可以拆下上面的传感器,放到其他地方去用,使用上是 十分方便灵活的。 16. 2 个 LED 灯 这是开发板板载的两个 LED 灯(DS0 和 DS1),DS0 是红色的,DS1 是绿色的,主要是方 便大家识别。这里提醒大家不要停留在 51 跑马灯的思维,搞这么多灯,除了浪费 IO 口,实在 是想不出其他什么优点。 我们一般的应用 2 个 LED 足够了,在调试代码的时候,使用 LED 来指示程序状态,是非 常不错的一个辅助调试方法。战舰 STM32 开发板几乎每个实例都使用了 LED 来指示程序的运 行状态。 17. 启动选择端口 这是开发板板载的启动模式选择端口(BOOT),STM32 有 BOOT0(B0)和 BOOT1(B1) 两个启动选择引脚,用于选择复位后 STM32 的启动模式,作为开发板,这两个是必须的。在 开发板上,我们通过跳线帽选择 STM32 的启动模式。关于启动模式的说明,请看 2.1.8 小节。 18. 复位按钮 这是开发板板载的复位按键(RESET),用于复位 STM32,还具有复位液晶的功能,因为 液晶模块的复位引脚和 STM32 的复位引脚是连接在一起的,当按下该键的时候,STM32 和液 晶一并被复位。 www.openedv.com 9 ALIENTEK 战舰STM32开发板 19. 参考电压选择端口 这是 STM32 的参考电压选择端口(P7),我们默认是接开发板的 3.3V 和 GND。如果大家 想设置其他参考电压,只需要把你的参考电压源接到 REF-和 REF+上即可。 20. WK_UP 及 3 个普通按钮 这是开发板板载的 4 个机械式输入按键(KEY0、KEY1、KEY2 和 WK_UP),其中 WK_UP 具有唤醒功能,该按键连接到 STM32 的 WAKE_UP(PA0)引脚,可用于待机模式下的唤醒, 在不使用唤醒功能的时候,也可以做为普通按键输入使用。 其他 3 个是普通按键,可以用于人机交互的输入,这 3 个按键是直接连接在 STM32 的 IO 口上的。这里注意 WK_UP 是高电平有效,而 KEY0、KEY1 和 KEY2 是低电平有效,大家在 使用的时候留意一下。 21. 触摸按钮 这是开发板板载的一个电容触摸输入按键(TPAD),用于实现触摸按键。现在触摸按键非 常流行,所以我们在开发板上也设计了一个,咱得跟上时代的步伐。 22. 电源指示灯 这是开发板板载的一颗蓝色的 LED 灯(PWR),用于指示电源状态。在电源开启的时候(通 过板上的电源开关控制),该灯会亮,否则不亮。通过这个 LED,可以判断开发板的上电情况。 23. 多功能端口 这里大家可别小看这 6 个排针,这可是本开发板设计的很巧妙的一个端口(由 P3 和 P14 组成),这组端口通过组合可以实现的功能有:ADC 采集、DAC 输出、PWM DAC 输出、外部 音频输入、电容触摸按键、DAC 音频、PWM DAC 音频、DAC ADC 自测等,所有这些,你只 需要 1 个跳线帽的设置,就可以逐一实现。 24. MIC(咪头) 这是开发板的板载录音输入口(MIC),该咪头直接接到 VS1053 的输入上,可以用来实现 录音功能。 25. 录音输入接口 这是开发板板载的外部录音输入接口(LINE_IN),通过咪头我们只能实现单声道的录音, 而通过这个 LINE_IN,我们可以实现立体声录音。 26. MIC 选择口 这是开发板板载录音的接入选择口(P2),如果使用 LINE_IN 录音的时候,我们把 P2 断开, 以排除来自咪头的干扰信号,从而可以更好的立体声录音。而使用咪头录音的时候,我们短接 P2 即可。 27. 耳机输出接口 这是开发板板载的音频输出接口(PHONE),战舰 STM32 开发板有多个音频输出(VS1053/ 收音机/PWM DAC 等),通过 74HC4052 实现音频选择,输入到 TDA1308,再输出到该音频输 出口,实现开发板的音频输出。 28. VS1053 IIS 输出口 这是 VS1053 的 IIS 输出接口(P1),该接口可以用来连接外部 DAC,实现更好的音质输出。 其实我觉得 VS1053 本身的音频 DAC 已经很好了。这个接口适合发烧友使用。 29. 5V 电源输入/输出 这是开发板板载的一组 5V 电源输入输出排针(2*3)(VOUT2),用于给外部提供 5V 的电 源,也可以用于从外部取 5V 的电源给板子供电。 大家在实验的时候可能经常会为没有 5V 电源而苦恼不已,有了 ALIENTEK 战舰 STM32 开发板,你就可以很方便的拥有一个简单的 5V 电源(最大电流不能超过 500ma)。 www.openedv.com 10 ALIENTEK 战舰STM32开发板 30. 3.3V 电源输入/输出 这是开发板板载的一组 3.3V 电源输入输出排针(2*3)(VOU1),该排针用于给外部提供 3.3V 的电源,也可以用于从外部取 3.3V 的电源给板子供电。 同样大家在实验的时候可能经常会为没有 3.3V 电源而苦恼不已,ALIENTEK 充分考虑到 了大家需求,有了这组 3.3V 排针,你就可以很方便的拥有一个简单的 3.3V 电源(最大电流不 能超过 500ma)。 31. ADXL345 加速度传感器 这是开发板板载的一个 3 轴加速度传感器(U11),ADXL345 分辨率高(13 位),测量范围 大(±16g),可以通过 SPI/IIC 访问,战舰开发板采用 IIC 访问它。有了这个,大家就可以实现 一些比较有意思的应用(比如测量倾角等) 32. 电源开关 这是开发板板载的电源开关(K1)。该开关用于控制整个开发板的供电,如果切断,则整 个开发板都将断电,电源指示灯(PWR)会随着此开关的状态而亮灭。 33. DC9~12V 电源输入 这是开发板板载的一个外部电源输入口(DC_IN),采用标准的直流电源插座。开发板板载 了 DC-DC 芯片(MP2359),用于给开发板提供高效、稳定的 5V 电源。由于采用了 DC-DC 芯 片,所以开发板的供电范围十分宽,大家可以很方便的找到合适的的电源(只要输出范围在 DC6~16V 的基本都可以)来给开发板供电。特别注意:如果你使用的是战舰 V2.0 以前的版本, 输入电压建议不要超过 9V!切记不能超过 12V!战舰 V2.0 及以后的版本才支持 DC6~16V 的 宽输入范围。 34. FM 收发天线接口 这个是开发板板载 FM 收发芯片的天线接口(ANT),同时我们安装有天线在这个上面。通 过这个天线,可以很好的实现 FM 收音和 FM 发射。 35. RS485 总线接口 这是开发板板载的 RS485 总线接口(RS485),通过 3 个端口和外部 485 设备连接。一般情 况下,只需要连接 2 个端口即可,即 A 和 B,并不需要连接 GND。这里提醒大家,RS485 通 信的时候,必须 A 接 A,B 接 B。否则可能通信不正常! 36. RS232/485 选择接口 这是开发板板载的 RS232/485 选择接口(P9),因为 RS485 基本上就是一个半双工的串口, 为了节约 IO,我们把 RS232 和 RS485 共用一个串口,通过 P9 来设置当前是使用 RS232 还是 RS485。当然,这样的设计还有一个好处。就是我们的开发板既可以充当 RS232 到 TTL 串口的 转换,又可以充当 RS485 到 TTL485 的转换。(注意,这里的 TTL 高电平是 3.3V) 37. RS232 接口 这是开发板板载的 RS232 接口(COM),通过一个标准的 DB9 母头和外部的串口连接。通 过这个接口,我们可以连接带有串口的电脑或者其他设备,实现串口通信。 38. 24C02 EEPROM 这是开发板板载的 EEPROM 芯片(U15),容量为 2Kb,也就是 256 字节。用于存储一些 掉电不能丢失的重要数据,比如系统设置的一些参数/触摸屏校准数据等。有了这个就可以方便 的实现掉电数据保存。 39. PS/2 鼠标/键盘接口 这是开发板板载的一个标准 PS/2 母头(PS/2),用于连接电脑鼠标和键盘等 PS/2 设备。 通过 PS/2 口,我们仅仅需要 2 个 IO 口,就可以扩展一个键盘,所以大家不必要对板上只 有 4 个按键而感到担忧。ALIENTEK 提供了标准的鼠标驱动例程,方便大家学习 PS/2 协议。 www.openedv.com 11 ALIENTEK 战舰STM32开发板 40. 游戏手柄接口 这是开发板板载的一个 9 针游戏手柄接口(JOY_PAD),可以用来连接 FC 手柄(红白机/ 小霸王游戏机手柄),这样大家可以在开发板上编写游戏程序,直接通过手柄玩游戏了。我们的 综合实验提供有一个简单的 NES 模拟器,大家可以直接从网上下载 nes 游戏,放到开发板上玩。 41. LCD/OLED 模块接口 这是战舰 STM32 开发板的又一个特色设计,一个接口,兼容多种模块。如果是 OLED 模 块,请靠左侧插。如果是 LCD 模块,则靠右侧插。OLED 模块支持 ALIENTEK 的单色/双色 OLED 模块。LCD 模块则支持 ALIENTEK 的 2.4/2.8/3.5 寸 LCD 模块,并且支持触摸屏功能。 42. CAN 总线接口 这是开发板板载的 CAN 总线接口(CAN),通过 3 个端口和外部 CAN 总线连接。一般情 况下,只需要连接 2 个端口即可,即 CANH 和 CANL,并不需要连接 GND。这里提醒大家, CAN 通信的时候,必须 CANH 接 CANH,CANL 接 CANL。否则可能通信不正常! 43. NRF24L01 模块接口 这是开发板板载的 NRF24L01 模块接口(U7),只要插入模块,我们便可以实现无线通信, 从而使得我们板子具备了无线功能,但是这里需要 2 个模块和 2 个开发板同时工作才可以。如 果只有 1 个开发板或 1 个模块,是没法实现无线通信的。 1.2.2 软件资源说明 上面我们详细介绍了 ALIENTEK 战舰 STM32 开发板的硬件资源。接下来,我们将向大家 简要介绍一下战舰 STM32 开发板的软件资源。 战舰 STM32 开发板提供的标准例程多达 57 个,一般的 STM32 开发板仅提供库函数 代码,而我们则提供寄存器和库函数两个版本的代码(本指南以寄存器版本作为介绍)。我们提 供的这些例程,基本都是原创,拥有非常详细的注释,代码风格统一、循序渐进,非常适合初 学者入门。而其他开发板的例程,大都是来自 ST 库函数的直接修改,注释也比较少,对初学 者来说不那么容易入门。 战舰 STM32 开发板的例程列表如表 1.2.2.1 所示: 编号 实验名字 编号 实验名字 1 跑马灯实验 30 DS18B20 数字温度传感器实验 2 蜂鸣器实验 31 DHT11 数字温湿度传感器实验 3 按键输入实验 32 无线通信实验 4 串口实验 33 PS2 鼠标实验 5 外部中断实验 34 FLASH 模拟 EEPROM 实验 6 独立看门狗实验 35 FM 收发实验 7 窗口看门狗实验 36 摄像头实验 8 定时器中断实验 37 外部 SRAM 实验 9 PWM 输出实验 38 内存管理实验 10 输入捕获实验 39 SD 卡实验 11 电容触摸按键实验 40 FATFS 实验 12 OLED 实验 41 汉字显示实验 13 TFTLCD 实验 42 图片显示实验 14 USMART 调试实验 43 照相机实验 15 RTC 实验 44 音乐播放器实验 16 待机唤醒实验 45 录音机实验 www.openedv.com 12 ALIENTEK 战舰STM32开发板 17 ADC 实验 46 手写识别实验 18 内部温度传感器实验 47 T9 拼音输入法实验 19 DAC 实验 48 串口 IAP 实验 20 PWM DAC 实验 49 触控 USB 鼠标实验 21 DMA 实验 50 USB 读卡器实验 22 IIC 实验 51 USB 声卡实验 23 SPI 实验 52 ENC28J60 网络模块实验 24 485 实验 53 UCOSII 实验 1-任务调度 25 CAN 收发实验 54 UCOSII 实验 2-信号量和邮箱 UCOSII 实验 3-消息队列、信号量 26 触摸屏实验 55 集和软件定时器 27 红外遥控实验 56 战舰系统综合实验 28 游戏手柄实验 57 ucGUI 实验 29 三轴加速度传感器实验 表 1.2.2.1 ALIENTEK 战舰 STM32 开发板例程表 从上表可以看出,ALIENTEK 战舰 STM32 开发板的例程基本上涵盖了 STM32F103ZET6 的所有内部资源,并且外扩展了很多有价值的例程,比如:FLASH 模拟 EEPROM 实验、IAP 实验、拼音输入法实验、手写识别实验、综合实验等。 而且从上表可以看出,例程安排是循序渐进的,首先从最基础的跑马灯开始,然后一步步 深入,从简单到复杂,有利于大家的学习和掌握。所以,ALIENTEK 战舰 STM32 开发板是非 常适合初学者的。当然,对于想深入了解 STM32 内部资源的朋友,ALIENTEK 战舰 STM32 开 发板也绝对是一个不错的选择。 这里特别说明一下战舰系统综合实验,这个实验使得 ALIENTEK 战舰 STM32 开发板更像 一个产品,而不单单是一个开发板了,它拥有目前市面上所有开发板中最复杂,最强大的功能, 可玩性极高,它的实现,充分向大家展示了 ALIENTEK 战舰开发板的优势,同时也证明了 STM32 的强悍性能。解决了一部分人,STM32 能干啥的顾虑。 www.openedv.com 13 ALIENTEK 战舰STM32开发板 第二章 实验平台硬件资源详解 本章,我们将节将向大家详细介绍 ALIENTEK 战舰 STM32 开发板各部分的硬件原理图, 让大家对该开发板的各部分硬件原理有个深入理解,并向大家介绍开发板的使用注意事项,为 后面的学习做好准备。 本章将分为如下两节: 1.1,开发板原理图详解; 1.2,开发板使用注意事项; 2.1 开发板原理图详解 2.1.1 MCU ALIENTEK 战舰 STM32 开发板选择的是 STM32F103ZETT6 作为 MCU,该芯片是 STM32F103 里面配置非常强大的了,它拥有的资源包括:64KB SRAM、512KB FLASH、2 个 基本定时器、4 个通用定时器、2 个高级定时器、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 的刷屏速度,更重要的是其价格,23 元左右的 零售价,足以秒杀很多其他芯片了。所以我们选择了它作为我们的主芯片。MCU 部分的原理 图如图 2.1.1.1(请大家打开开发板光盘的原理图查看清晰版本)所示: www.openedv.com 14 ALIENTEK 战舰STM32开发板 图 2.1.1.1 MCU 部分原理图 上图中 U5 为我们的主芯片:STM32F103ZET6。 这里主要讲解一下 3 个地方: 1,后备区域供电脚 VBAT 脚的供电采用 CR1220 纽扣电池和 VCC3.3 混合供电的方式,在 有外部电源(VCC3.3)的时候,CR1220 不给 VBAT 供电,而在外部电源断开的时候,则由 CR1220 给其供电。这样,VBAT 总是有电的,以保证 RTC 的走时以及后备寄存器的内容不丢失。 2,图中的 R37 和 R38 用隔离 MCU 部分和外部的电源,这样的设计主要是考虑了后期维 护,如果 3.3V 电源短路,可以断开这两个电阻,来确定是 MCU 部分短路,还是外部短路,有 助于生产和维修。当然大家在自己的设计上,这两个电阻是完全可以去掉的。 3,图中 P7 是参考电压选择端口。我们开发板默认是接板载的 3.3V 作为参考电压,如果 大家想用自己的参考电压,则把你的参考电压接入 VREF-和 VREF+即可。 www.openedv.com 15 ALIENTEK 战舰STM32开发板 2.1.2 引出 IO 口 ALIENTEK 战舰 STM32 开发板引出了 STM32F103ZET6 的所有 IO 口,如图 2.1.2.1 所示: 图 2.1.2.1 引出 IO 口 图中 P4 和 P5 为 MCU 主 IO 引出口,这两组排针每组引出 IO 数位 54 个,共 108 个 IO 从 这里引出。STM32F103ZET6 总共有 112 个 IO,除去 RTC 晶振占用的 2 个,还剩 110 个,这两 组 IO 引出除 PA9 和 PA10 以外的所有 IO 口。大家可以通过这两组 IO 引出口,方便的扩展自 己的外设。(PA9 和 PA10 通过 P6 引出) 2.1.3 USB 串口/串口 1 选择接口 ALIENTEK 战舰 STM32 开发板板载的 USB 串口和 STM32F103ZET6 的串口是通过 P6 连 接起来的,如图 2.1.3.1 所示: 、 图 2.3.1.1 USB 串口/串口 1 选择接口 图中 TXD/RXD 是相对 CH340G 来说的,也就是 USB 串口的发送和接受脚。而 USART1_RX www.openedv.com 16 ALIENTEK 战舰STM32开发板 和 USART1_TX 则是相对于 STM32F103ZET6 来说的。这样,通过对接,就可以实现 USB 串口 和 STM32F103ZET6 的串口通信了。同时,P6 是 PA9 和 PA10 的引出口。 这样设计的好处就是使用上非常灵活。比如需要用到外部 TTL 串口和 STM32 通信的时候, 只需要拔了跳线帽,通过杜邦线连接外部 TTL 串口,就可以实现和外部设备的串口通信了;又 比如我有个板子需要和电脑通信,但是电脑没有串口,那么你就可以使用开发板的 RXD 和 TXD 来连接你的设备,把我们的开发板当成 USB 串口用了。 2.1.4 JTAG/SWD ALIENTEK 战舰 STM32 开发板板载的标准 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 调试。 2.1.5 SRAM ALIENTEK 战舰 STM32 开发板外扩了 1M 字节的 SRAM 芯片,如图 2.1.5.1 所示: www.openedv.com 17 ALIENTEK 战舰STM32开发板 图 2.1.5.1 外扩 SRAM 图中 U6 为外扩的 SRAM 芯片,型号为:IS62WV51216,容量为 1M 字节,该芯片挂在 STM32 的 FSMC 上。这样大大扩展了 STM32 的内存(芯片本身只有 64K 字节),从而在需要大内存的 场合,战舰 STM32 开发板也可以胜任。 2.1.6 LCD/OLED 模块接口 ALIENTEK 战舰 STM32 开发板板载的 LCD/OLED 模块接口电路如图 2.1.6.1 所示: 图 2.1.6.1 LCD/OLED 模块接口 图中 TFT_LCD 是一个通用的液晶模块接口,而 OLED 是一个给 OLED 显示模块供电的接 口,它和 TFT_LCD 拼接在一起,组合成一个组合接口。当使用 2.4 寸/2.8 寸/3.5 寸的 LCD 时, 我们接到 TFT_LCD 上就可以了(靠右插),而当我们使用 ALIENTEK 的 OLED 模块时,则接 www.openedv.com 18 ALIENTEK 战舰STM32开发板 OLED 排针做电源,同时会连接到 TFT_LCD 上的部分管脚(靠左插),从而实现 OLED 与 MCU 的连接。TFTLCD 模块也是接在 STM32F103ZET6 的 FSMC 上的,相比战舰 STM32 开发板, 这样可以显著提高 LCD 刷屏速度。 图中的 T_MISO/T_MOSI/T_PEN/T_CS/T_CS 用来实现对液晶触摸屏的控制。LCD_BL 则 控制 LCD 的背光。液晶复位信号 RESET 则是直接连接在开发板的复位按钮上,和 MCU 共用 一个复位电路。 2.1.7 复位电路 ALIENTEK 战舰 STM32 开发板的复位电路如图 2.1.7.1 所示: 图 2.1.7.1 复位电路 因为 STM32 是低电平复位的,所以我们设计的电路也是低电平复位的,这里的 R32 和 C51 构成了上电复位电路。同时,开发板把 TFT_LCD 的复位引脚也接在 RESET 上,这样这个复位 按钮不仅可以用来复位 MCU,还可以复位 LCD。 2.1.8 启动模式设置接口 ALIENTEK 战舰 STM32 开发板的启动模式设置端口电路如图 2.8.1.1 所示: 图 2.8.1.1 启动模式设置接口 上图的 BOOT0 和 BOOT1 用于设置 STM32 的启动方式,其对应启动模式如表 2.1.8.1 所示: 表 2.8.1.1 BOOT0、BOOT1 启动模式表 按照表 2.8.1.1,一般情况下如果我们想用用串口下载代码,则必须配置 BOOT0 为 1,BOOT1 为 0,而如果想让 STM32 一按复位键就开始跑代码,则需要配置 BOOT0 为 0,BOOT1 随便设 www.openedv.com 19 ALIENTEK 战舰STM32开发板 置都可以。这里 ALIENTEK 战舰 STM32 开发板专门设计了一键下载电路,通过串口的 DTR 和 RTS 信号,来自动配置 BOOT0 和 RST 信号,因此不需要用户来手动切换他们的状态,直接 串口下载软件自动控制,可以非常方便的下载代码。 2.1.9 RS232 串口 ALIENTEK 战舰 STM32 开发板板载的 RS232 串口电路,如图 2.1.9.1 所示: 图 2.1.9.1 RS232 串口 因为 RS232 电平不能直接连接到 STM32,所以需要一个电平转换芯片。这里我们选择的 是 SP3232(也可以用 MAX3232)来做电平转接,同时图中的 P9 用来实现 RS232/RS485 的选 择,以满足不同实验的需要。 图中 USART2_TX/USART2_RX 连接在 MCU 的串口 2 上(PA2/PA3),所以这里的 RS232/RS485 都是通过串口 2 来实现的。图中 RS485_TX 和 RS485_RX 信号接在 SP3485 的 DI 和 RO 信号上。 因为 P9 的存在,其实还带来另外一个好处,就是我们可以把开发板变成一个 RS232 电平 转换器,或者 RS485 电平转换器,比如你买的核心板,可能没有板载 RS485/RS232 接口,通过 连接战舰 STM32 开发板的 P9 端口,就可以让你的核心板拥有 RS232/RS485 的功能。 2.1.10 RS485 接口 ALIENTEK 战舰 STM32 开发板板载的 RS485 接口电路如图 2.1.10.1 所示: 图 2.1.10.1 RS485 接口 RS485 电平也不能直接连接到 STM32,同样需要电平转换芯片。这里我们使用 SP3485 来 www.openedv.com 20 ALIENTEK 战舰STM32开发板 做 485 电平转换,其中 R45 为匹配电阻。 RS485_RX/RS485_TX 连接在 P9 上面,通过 P9 跳线来选择是否连接在 MCU 上面, RS485_RE 则是直接连接在 MCU 的 IO 口(PG9)上的,该信号用来控制 SP3485 的工作模式(高 电平为发送模式,低电平为接收模式)。 2.1.11 CAN/USB 接口 ALIENTEK 战舰 STM32 开发板板载的 CAN 接口电路以及 STM32 USB 接口电路如图 2.1.11.1 所示: 图 2.1.11.1 CAN/USB 接口 CAN 总线电平也不能直接连接到 STM32,同样需要电平转换芯片。这里我们使用 TJA1050 来做 CAN 电平转换,其中 R48 为匹配电阻。 USB_D+/USB_D-连接在 MCU 的 USB 口(PA12/PA11)上,同时,因为 STM32 的 USB 和 CAN 共用这组信号,所以我们通过 P13 来选择使用 USB 还是 CAN。 图中的 USB 端子还具有供电功能,VUSB 为开发板的 USB 供电口,通过这个 USB 口,就 可以给整个开发板供电了。 2.1.12 EEPROM ALIENTEK 战舰 STM32 开发板板载的 EEPROM 电路如图 2.1.12.1 所示: www.openedv.com 图 2.1.12.1 EEPROM 21 ALIENTEK 战舰STM32开发板 EEPROM 芯片我们使用的是 24C02,该芯片的容量为 2Kb,也就是 256 个字节,对于我们 普通应用来说是足够了的。当然,你也可以选择换大的芯片,因为我们的电路在原理上是兼容 24C02~24C512 全系列 EEPROM 芯片的。 这里我们把 A0~A2 均接地,对 24C02 来说也就是把地址位设置成了 0 了,写程序的时候 要注意这点。IIC_SCL 接在 MCU 的 PB10 上,IIC_SDA 接在 MCU 的 PB11 上,这里我们虽然 接到 STM32 的硬件 IIC 上,但是我们并不提倡使用硬件 IIC,因为 STM32 的 IIC 是鸡肋!请谨 慎使用。IIC_SCL/IIC_SDA 总线上总共挂了 3 个器件:24C02、ADXL345 和 RDA5820,后续 我们将向大家介绍另外两个器件。 2.1.13 游戏手柄接口 ALIENTEK 战舰 STM32 开发板板载的游戏手柄接口电路如图 2.1.13.1 所示: 图 2.1.13.1 游戏手柄接口 因为很多 FC 游戏机(俗称红白机/小霸王游戏机)的手柄都是 9 针接口,刚好可以插到 9 针 的串口公头里面。这里我们使用一个 DB9 公头来做 FC 游戏手柄接口。 JOY_CLK/JOY_LAT/JOY_DAT 分别连接在 MCU 的 PC12/PC8/PC9 上,这 3 个信号和 SDIO 的 SCK/D0/D1 共用,所以他们不能同时使用!这里特别提醒:因为这个 DB9 的 2,3 脚直接接 在 STM32 的 IO 口,所以,这个口一定不要接 RS232 串口!!否则可能直接把 STM32F103ZET6 给烧了。 2.1.14 SPI FLASH ALIENTEK 战舰 STM32 开发板板载的 SPI FLASH 电路如图 2.1.14.1 所示: www.openedv.com 图 2.1.14.1 SPI FLASH 芯片 22 ALIENTEK 战舰STM32开发板 SPI FLASH 芯片型号为 W25Q64,该芯片的容量为 64Mb,也就是 8M 字节。该芯片和 SD 卡、NRF24L01 共用一个 SPI(SPI2),通过片选来选择使用某个器件,在使用其中一个器件的 时候,请务必禁止另外两个器件的片选信号。 图中 F_CS 连接在 MCU 的 PB12 上,SPI2_SCK/SPI2_MOSI/SPI2_MISO 则分别连接在 MCU 的 PB13/PB15/PB14 上。 2.1.15 3D 加速度传感器 ALIENTEK 战舰 STM32 开发板板载的 3D 加速度传感器电路如图 2.1.15.1 所示: 图 2.1.15.1 3D 加速度传感器 3D 加速度传感器芯片型号为 ADXL345,该芯片具有分辨率高(13 位),测量范围大(± 16g)的特点,支持多种接口,这里我们使用 IIC 接口来访问。 同 24C02 一样,该芯片的 IIC_SCL 和 IIC_SDA 同样是挂在 PB10 和 PB11 上,他们共享一 个 IIC 总线。 2.1.16 温湿度传感器接口 ALIENTEK 战舰 STM32 开发板板载的温湿度传感器接口电路如图 2.1.16.1 所示: 图 2.1.16.1 温湿度传感器接口 该接口支持 DS18B20/DS1820/DHT11 等单总线数字温湿度传感器。1WIRE_DQ 是传感器 的数据线,该信号连接在 MCU 的 PG11 上。 2.1.17 红外接收头 ALIENTEK 战舰 STM32 开发板板载的红外接收头电路如图 2.1.17.1 所示: www.openedv.com 23 ALIENTEK 战舰STM32开发板 图 2.1.17.1 红外接收头 HS0038 是一个通用的红外接收头,几乎可以接收市面上所有红外遥控器的信号,有了它, 就可以用红外遥控器来控制开发板了。REMOTE_IN 为红外接收头的输出信号,该信号连接在 MCU 的 PB9 上。 2.1.18 无线模块接口 ALIENTEK 战舰 STM32 开发板板载的无线模块接口电路如图 2.1.18.1 所示: 图 2.1.18.1 无线模块接口 该接口用来连接 NRF24L01 等 2.4G 无线模块,从而实现开发板与其他设备的无线数据传 输(注意:NRF24L01 不能和蓝牙/WIFI 连接)。NRF24L01 无线模块的最大传输速度可以达到 2Mbps,传输距离最大可以到 30 米左右(空旷地,无干扰)。 NRF_CE/NRF_CS/NRF_IRQ 连接在 MCU 的 PG6/PG7/PG8 上,而另外 3 个 SPI 信号则和 SPI FLASH 共用。 2.1.19 LED ALIENTEK 战舰 STM32 开发板板载总共有 3 个 LED,其原理图如图 2.1.19.1 所示: www.openedv.com 24 ALIENTEK 战舰STM32开发板 图 2.1.19.1 LED 其中 PWR 是系统电源指示灯,为蓝色。LED0 和 LED1 分别接在 PB5 和 PE5 上。为了方 便大家判断,我们选择了 DS0 为红色的 LED,DS1 为绿色的 LED。 2.1.20 按键 ALIENTEK 战舰 STM32 开发板板载总共有 4 个输入按键,其原理图如图 2.1.20.1 所示: 图 2.1.20.1 输入按键 KEY0、KEY1 和 KEY2 用作普通按键输入,分别连接在 PE4、PE3 和 PE2 上,这里并没有 使用外部上拉电阻,但是 STM32 的 IO 作为输入的时候,可以设置上下拉电阻,所以我们使用 STM32 的内部上拉电阻来为按键提供上拉。 WK_UP 按键连接到 PA0(STM32 的 WKUP 引脚),它除了可以用作普通输入按键外,还可 以用作 STM32 的唤醒输入。这个按键是高电平触发的。 2.1.21 TPAD 电容触摸按键 ALIENTEK 战舰 STM32 开发板板载了一个电容触摸按键,其原理图如图 2.1.21.1 所示: www.openedv.com 25 ALIENTEK 战舰STM32开发板 图 2.1.21.1 电容触摸按键 图中 5.1M 是电容充电电阻,TPAD 并没有直接连接在 MCU 上,而是连接在多功能端口 (P14)上面,通过跳线帽来选择是否连接到 STM32。多功能端口,我们将在 2.1.25 节介绍。 电容触摸按键的原理我们将在后续的实战篇里面介绍。 2.1.22 PS/2 接口 ALIENTEK 战舰 STM32 开发板板载了一个 PS/2 接口,其原理图如图 2.1.22.1 所示: 图 2.1.22.1 PS/2 接口 有了该接口,我们就可以用来连接外部标准的 PS/2 鼠标或键盘等设备了,也就大大的扩展 了 ALIENTEK 战舰 STM32 开发板的输入。PS_CLK 和 PS_DAT 分别接 PC11 和 PC10,PS/2 的 信号线的上拉电阻我们还是选择 STM32 内部的上拉电阻来实现。注意 PS/2 接口和 SDIO_D2 和 SDIO_D3 公用了 IO 口,所以他们不能同时工作。 2.1.23 OLED/摄像头模块接口 ALIENTEK 战舰 STM32 开发板板载了一个 OLED/摄像头模块接口,其原理图如图 2.1.23.1 所示: www.openedv.com 26 ALIENTEK 战舰STM32开发板 图 2.1.23.1 OLED/摄像头模块接口 图中 P8 是接口可以用来连接 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 模式 来连接我们的开发板,这样所有的实验都可以仿真!)。 2.1.24 有源蜂鸣器 ALIENTEK 战舰 STM32 开发板板载了一个有源蜂鸣器,其原理图如图 2.1.24.1 所示: 图 2.1.24.1 有源蜂鸣器 有源蜂鸣器是指自带了震荡电路的蜂鸣器,这种蜂鸣器一接上电就会自己震荡发声。而如 果是无源蜂鸣器,则需要外加一定频率(2~5Khz)的驱动信号,才会发声。这里我们选择使用 有源蜂鸣器,方便大家使用。 www.openedv.com 27 ALIENTEK 战舰STM32开发板 图中 Q1 是用来扩流,R60 则是一个下拉电阻,避免 MCU 复位的时候,蜂鸣器可能发声的 现象。BEEP 信号直接连接在 MCU 的 PB8 上面,PB8 可以做 PWM 输出,所以大家如果想玩 高级点(如:控制蜂鸣器“唱歌”),就可以使用 PWM 来控制蜂鸣器。 2.1.25 SD 卡/以太网模块接口 ALIENTEK 战舰 STM32 开发板板载了一个 SD 卡(大卡/相机卡)接口,但是并没有板载 以太网,不过我们板载了以太网模块接口,通过外部模块扩展以太网,其原理图如图 2.1.25.1 所示: 图 2.1.25.1 SD 卡/以太网接口 图中 SD_CARD 为 SD 卡接口,该接口在开发板的底面,这也是战舰 STM32 开发板底面唯 一的元器件。 在开发板的 PCB 上 P10/P11/P12 组合在一起,构成一个 SD 卡接口方式选择接口,可以用 来设置 SD 卡是工作在 SDIO 模式,还是工作在 SPI 模式。同时 P12 兼具以太网模块接口功能 (因为 ALIENTEK 网络模块接口和 P12 接口一模一样,我们只需要拿一组排线把他们对接即可 实现以太网与开发板的连接),这里需要注意以太网模块除 SD_CS 信号外,其余信号都是使用 无线模块的。使用以太网模块的时候,SD 卡就只能工作在 SDIO 模式了,同时无线模块也将无 法使用。 www.openedv.com 28 ALIENTEK 战舰STM32开发板 SD_DT0~SD_DT3 分别连接在 PC8~PC11 上面,他们和游戏手柄和 PS/2 接口信号共用 IO, 所以在使用 SDIO 模式的时候,手柄和 PS/2 设备将不能使用。SD_SCK 和 SD_CMD 则分别连 接在 PC12 和 PD2 上,而 SD_CS 和 SD_CMD 一样,也是连接在 PD2 上的,而 SD_CS 则是网 络模块的 INT 信号,所以当不适用 INT 信号的时候,网络模块和 SD 卡可以同时工作,而当要 用 INT 的时候,SD 卡将不能和网络模块一起使用,这点大家在使用上要稍加注意。P12 的其余 信号都是和无线模块共用,前面已经有介绍了,这里我们就不再介绍了。 2.1.26 多功能端口 ALIENTEK 战舰 STM32 开发板板载的多功能端口,是由 P14 和 P3 构成的一个 6PIN 端口,, 其原理图如图 2.1.26.1 所示: 图 2.1.26.1 多功能端口 从上图,大家可能还看不出这个多功能端口的全部功能,别担心,下面我们会详细介绍。 首先介绍左侧的 P14,其中 TPAD 为电容触摸按键信号,连接在电容触摸按键上。STM_ADC 和 STM_DAC 则分别连接在 PA1 和 PA4 上,用于 ADC 采集或 DAC 输出。当需要电容触摸按 键的时候,我们通过跳线帽短接 TPAD 和 STM_ADC,就可以实现电容触摸按键(利用定时器 的输入捕获)。STM_DAC 信号则既可以用作 DAC 输出,也可以用作 ADC 输入,因为 STM32 的该管脚同时具有这两个复用功能。 我们再来看看 P3,PWM_DAC 连接在 MCU 的 PB6,是定时器 4 的通道 1 输出,后面跟一 个二阶 RC 滤波电路,其截止频率为 33.8Khz。经过这个滤波电路,MCU 输出的方波就变为直 流信号了。PWM_AUDIO 是一个音频输入通道,它连接到 74HC4052,再进入到 TDA1308 进 行输出缓冲,最终输出到耳机。 单独介绍完了 P3 和 P14,我们再来看看他们组合在一起的多功能端口,如图 2.1.26.2 所示: 图 2.1.26.2 组合后的多功能端口 www.openedv.com 29 ALIENTEK 战舰STM32开发板 图中 AIN 是 PWM_AUDIO,PDC 是滤波后的 PWM_DAC 信号。下面我们来看看通过 1 个 跳线帽,这个多功能接口可以实现哪些功能。 当不用跳线帽的时候: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.27 音频选择 ALIENTEK 战舰 STM32 开发板板载了多个音频输出设备,所以需要一个音频选择电路, 来实现不同音频的切换,这里我们使用 74HC4052 模拟开关来实现音频切换,其原理图如图 2.1.27.1 所示: 图 2.1.27.1 音频选择电路 74HC4052 是一个双 4 路模拟开关,工作电压可以低至 2V,这里我们选择该模拟开关来做 音频切换。 图中 MP3_LEFT/MP3_RIGHT 连接在 VS1053 的音频输出端。RADIO_L 和 RADIO_R 是 RDA5820 的音频输出端。A_OUTR 和 A_OUTL 连接到 TDA1308 的输入端,最终输出到耳机。 而 OUTL 和 OUTR 则还连接到了 RDA5820 的音频输入端,所以开发板的所有声音,其实都可 以通过 FM 发射出去,大家可以在收音机里面听到来自开发板的声音。PWM_AUDIO 则是来自 外部音源输入(我们提供的 USB 声卡实验,就需要用到这个通道)。ASEL_A 和 ASEL_B 直接 连接在 MCU 的 PD7 和 PB7 上面,用来控制 74HC4052 的通道选择。 www.openedv.com 30 ALIENTEK 战舰STM32开发板 2.1.28 FM 收发 ALIENTEK 战舰 STM32 开发板板载了一颗 FM 收发芯片 RDA5820,其原理图如图 2.1.28.1 所示: 图 2.1.28.1 FM 收发电路 RDA5820 是一颗立体声 FM 收发芯片,该芯片通过 IIC 接口控制,可以实现 65~108MHz 的全球 FM 频段接收,同时可以作为 FM 发射。RDA5820 接收与发送天线共用,仅需要极少的 外围器件即可正常工作。 图中 OUTL 和 OUTR 为 FM 发射的音频输入信号,RADIO_L 和 RADIO_R 是 FM 接收的 音频输出信号,连接到 74HC4052。同 24C02 一样,该芯片的 IIC_SCL 和 IIC_SDA 同样是挂在 PB10 和 PB11 上,他们共享一个 IIC 总线。 2.1.29 音频输出 ALIENTEK 战舰 STM32 开发板板载的音频输出电路,其原理图如图 2.1.29.1 所示: 图 2.1.29.1 音频输出电路 图中 PHONE 为立体声音频输出插座,可以直接插 3.5mm 的耳机。A_OUTR 和 A_OUTL 是来自 74HC4052 的音频输出信号,直接输入到 TDA1308。 图中的 TDA1308 是 AB 类的数字音频(CD)专用耳机功放 IC。其具有低电压、低失真、高 速率、强输出等优异的性能是以往的 TDA2822、TDA7050、LM386 等“经典”功放望尘莫及 的。同时战舰 STM32 开发板搭载了效果一流的 VS1053 编解码芯片,所以,战舰 STM32 开发 www.openedv.com 31 ALIENTEK 战舰STM32开发板 板播放 MP3 的音质是非常不错的,胜过市面上很多中低端 MP3 的音质。 2.1.30 音频编解码 ALIENTEK 战舰 STM32 开发板板载 VS1053 音频编解码芯片,其原理图如图 2.1.30.1 所示: 2.1.30.1 音频编解码芯片 VS1053 是一颗单片 OGG/MP3/AAC/WMA/MIDI 音频解码器,通过 plugin 可以实现 FLAC 的解码,同时该芯片可以支持 IMA ADPCM 编码,通过 plugin 可以实现 OGG 编码。相比它的 前辈:VS1003,VS1053 性能提升了不少,比如支持 OGG 编解码,支持 FLAC 解码,同时音 质上也有比较大的提升,还支持空间效果设置。VS1053 还支持 IIS 输出,我们在开发板上引出 了 IIS 接口(P1),通过这个接口,大家可以在外部接自己的 DAC,以达到更好的音质。 图中 P2 是 MIC 录音选择接口,这个接口主要在大家选择使用 LINE_IN 录音的时候,需要 用到,断开 P2,就可以排除 MIC 对 LINE_IN 录音的干扰,从而达到更好的效果。默认我们是 用跳线帽短接 P2 的。 图中 MP3_LEFT/MP3_RIGHT 这两个信号是连接到 74HC4052 的,通过模拟开关选择是否 输出 MP3 音源。TP1/TP2/TP3 是 3 个测试点,用于测试。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 访问。 2.1.31 电源 ALIENTEK 战舰 STM32 开发板板载的电源供电部分,其原理图如图 2.1.31.1 所示: www.openedv.com 32 ALIENTEK 战舰STM32开发板 图 2.1.31.1 电源 图中,总共有 3 个稳压芯片:U16/U17/U19,DC_IN 用于外部直流电源输入,经过 U17 DC-DC 芯片转换为 5V 电源输出,其中 D4 是防反接二极管,避免外部直流电源极性搞错的时候,烧坏 开发板。K1 为开发板的总电源开关,F1 为 500ma 自恢复保险丝,用于保护电源。U16 为 3.3V 稳压芯片,给开发板提供 3.3V 电源,而 U19 则是 1.8V 稳压芯片,供 VS1053 的 CVDD 使用。 这里还有 USB 供电部分没有列出来,其中 VUSB 就是来自 USB 供电部分,我们将在相应 章节进行介绍。 2.1.32 电源输入输出接口 ALIENTEK 战舰 STM32 开发板板载了两组简单电源输入输出接口,其原理图如图 2.1.32.1 所示: 图 2.1.32.1 电源 图中,VOUT1 和 VOUT2 分别是 3.3V 和 5V 的电源输入输出接口,有了这 2 组接口,我们 可以通过开发板给外部提供 3.3V 和 5V 电源了,虽然功率不大(最大 500ma),但是一般情况 都够用了,大家在调试自己的小电路板的时候,有这两组电源还是比较方便的。同时这两组端 www.openedv.com 33 ALIENTEK 战舰STM32开发板 口,也可以用来由外部给开发板供电。 2.1.33 USB 串口 ALIENTEK 战舰 STM32 开发板板载了一个 USB 串口,其原理图如图 2.1.33.1 所示: 图 2.1.33.1 USB 串口 USB 转串口,我们选择的是 CH340G,我们的 Mini 板之前用的是 PL2303HX(后面会改为 CH340G),但是问题比较多,这次我们直接使用南京沁恒的 CH340G,稳定性经测试还不错, 所以还是多支持下国产。 图中 Q2 和 Q3 的组合构成了我们开发板的一键下载电路,只需要在 mcuisp 软件设置:DTR 的低电平复位,RTS 高电平进 BootLoader。就可以一键下载代码了,而不需要手动设置 B0 和 按复位了。其中,RESET 是开发板的复位信号,BOOT0 则是启动模式的 B0 信号。USB_232 是一个 MiniUSB 座,提供 CH340G 和电脑通信的接口,同时可以给开发板供电,VUSB 就是来 自电脑 USB 的电源,USB_232 是本开发板的主要供电口。 2.2 开发板使用注意事项 为了让大家更好的使用 ALIENTEK 战舰 STM32 开发板,我们在这里总结该开发板使用的 时候尤其要注意的一些问题,希望大家在使用的时候多多注意,以减少不必要的问题。 1, 开发板一般情况是由 USB_232 口供电,在第一次上电的时候由于 CH340G 在和电脑建 立连接的过程中,导致 DTR/RTS 信号不稳定,会引起 STM32 复位 2~3 次左右,这个 现象是正常的,后续按复位键就不会出现这种问题了。 2, 虽说开发板有 500mA 自恢复保险丝,但是由于自恢复保险丝是慢动作器件,所以在给 外部供电的时候,还是请大家小心一点,不要超过这个限额,以免引起不必要的问题 3, SPI2 被多个 SPI 器件共用(SD 卡/无线模块/网络模块/W25Q64),在使用的时候,必须 保证同一时刻只有 1 个 SPI 器件是被选中的(CS 为低),其他器件必须设置为非选中 (CS 为高),以免互相干扰。 4, JTAG 接口有几个信号(JTDO/JTRST)被摄像头模块/OLED 模块占用了,所以在调试 这两个模块的时候,请大家选择 SWD 模式,其实最好就是一直用 SWD 模式。 5, 当你想使用某个 IO 口用作其他用处的时候,请先看看开发板的原理图,该 IO 口是否 www.openedv.com 34 ALIENTEK 战舰STM32开发板 有连接在开发板的某个外设上,如果有,该外设的这个信号是否会对你的使用造成干 扰,先确定无干扰,再使用这个 IO。比如 PB8 就不怎么适合再用做其他输出,因为他 接了蜂鸣器,如果你输出高电平就会听到蜂鸣器的叫声了。 6, 开发板上的跳线帽比较多,大家在使用某个功能的时候,要先查查这个是否需要设置 跳线帽,以免浪费时间。 7, 当液晶显示白屏的时候,请先检查液晶模块是否插好(拔下来重新插试试),如果还不 行,可以通过串口看看 LCD ID 是否正常,再做进一步的分析。 8, 开发板有 2 个 DB9 接口,但是请特别注意,只有 COM 这个接口(PS/2 右侧的)可以 用来连接外部串口,而 JOY_PAD 这个接口(PS/2 左侧)是用来接手柄的,不能接串 口,否则可能把 STM32 给烧了!请大家一定要注意这个问题!! 至此,本指南的实验平台(ALIENTEK 战舰 STM32 开发板)的硬件部分就介绍完了, 了解了整个硬件对我们后面的学习会有很大帮助,有助于理解后面的代码,在编写软件的 时候,可以事半功倍,希望大家细读!另外 ALIENTEK 开发板的其他资料及教程更新,都 可以在技术论坛 www.openedv.com 下载到,大家可以经常去这个论坛获取更新的信息。 www.openedv.com 35 ALIENTEK 战舰STM32开发板 第二篇 软件篇 上一篇,我们介绍了本指南的实验平台,本篇我们将详细介绍 STM32 的开发软件:RVMDK。 通过该篇的学习,你将了解到:1、如何在 RVMDK 下新建 STM32 工程;2、工程的编译;3、 RVMDK 的一些使用技巧;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. MDK3.8a 软件:这是编程编译软件。 光盘目录:软件资料\软件\MDK3.80A 本篇将分为如下 3 个章节: 2.1,RVMDK 软件入门; 2.2,STM32 开发基础知识入门; 2.3,SYSTEM 文件介绍; www.openedv.com 36 ALIENTEK 战舰STM32开发板 第三章 RVMDK 软件入门 本章将向大家介绍 RVMDK 软件的使用,ST 官方固件库介绍,同时还介绍怎样建一个基 于 STM32 官方固件库 V3.5 的工程模板。通过本章的学习,我们最终将建立一个自己的 RVMDK 工程,最后本章还将向大家介绍 RVMDK 软件的一些使用技巧,希望大家在本章之后,能够对 RVMDK 这个软件有个比较全面的了解。 本章分为如下个小结: 3.1,STM32 官方固件库简介; 3.2,RVMDK3.80A 简介; 3.3,新建基于固件库的 RVMDK 工程模板; 3.4,MDK 下的程序下载与调试; 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) { www.openedv.com 37 ALIENTEK 战舰STM32开发板 GPIOx->BRR = GPIO_Pin; } 这个时候你不需要再直接去操作 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 公司交一定的专利费。 www.openedv.com 38 ALIENTEK 战舰STM32开发板 既然大家都使用的是 Cortex-M3 核,也就是说,本质上大家都是一样的,这样 ARM 公司 为了能让不同的芯片公司生产的 Cortex-M3 芯片能在软件上基本兼容,和芯片生产商共同提出 了一套标准 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 官方提供的固件库完整包 www.openedv.com 39 ALIENTEK 战舰STM32开发板 可以在官方下载,我们光盘也会提供。固件库是不断完善升级的,所以有不同的版本,我们使 用的是 V3.5 版本的固件库 大家可以到光盘目录: 软件资料\STM32 固件库使用参考资料\ 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 文件下就是官方评估板的一些对应源码,这个可以忽略不看。 www.openedv.com 40 ALIENTEK 战舰STM32开发板 根目录中还有一个 stm32f10x_stdperiph_lib_um.chm 文件,直接打开可以知道,这是一个固 件库的帮助文档,这个文档非常有用,只可惜是英文的,在开发过程中,这个文档会经常被使 用到。 3.1.3.2 关键文件介绍: 下面我们要着重介绍 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 文件。 www.openedv.com 41 ALIENTEK 战舰STM32开发板 图 3.1.2.6 startup 文件 这里之所以有 8 个启动文件,是因为对于不同容量的芯片启动文件不一样。对于 103 系列,主 要是用其中 3 个启动文件: startup_stm32f10x_ld.s: 适用于小容量 产品 startup_stm32f10x_md.s : 适用于中等容量产品 startup_stm32f10x_hd.s: 适用于大容量产品 这里的容量是指 FLASH 的大小.判断方法如下: 小容量:FLASH≤32K 中容量:64K≤FLASH≤128K 大容量:256K≤FLASH 我们 ALIENTEK STM32 战舰版采用的 103ZET6 是属于大容量产品,所以我们的启动文件选择 startup_stm32f10x_hd.s, 而我 们的 mini 板 子采 用的 103RBT6 是 中等 容量芯 片, 所以选 择 startup_stm32f10x_md.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 RVMDK3.80A 简介 RVMDK 源自德国的 KEIL 公司,是 RealView MDK 的简称。在全球 RVMDK 被超过 10 万 的嵌入式开发工程师使用,RealView MDK 集成了业内最领先的技术,包括 μVision3 集成开发 环境与 RealView 编译器。支持 ARM7、ARM9 和最新的 Cortex-M3 核处理器,自动配置启动 代码,集成 Flash 烧写模块,强大的 Simulation 设备模拟,性能分析等功能。与 ARM 之前的工 www.openedv.com 42 ALIENTEK 战舰STM32开发板 具包 ADS1.2 相比,RealView 编译器具有代更小、性能更高的优点,RealView 编译器与 ADS.2 的比较: 代码密度:比 ADS1.2 编译的代码尺寸小 10%; 代码性能:比 ADS1.2 编译的代码性能提高 20%; 目前 RVMDK 的最新版本是 RVMDK4.6,4.0 以上的版本的 RVMDK 对 IDE 界面进行了很 大改变,并且支持 Cortex-M0 内核的处理器。作者曾用过 RVMDK3.24/3.80A/4.10 等几个版本, 并对他们进行了一些简单的对比,从对比情况来看:3.24 和 3.80a 在各方面的比较都差不多, 当然有人说稳定性 3.80A 要好一些,这个我不做评论。但是 4.10 确实界面是好了,支持的器件 也多了,但编译效率没有 3.24/3.80A 高,尤其在编译后的代码执行速度(FFT 运算),4.10 要对 速度进行-O2 优化才能和 3.24/3.80A 的普通级别相比。另外,国内大都数单片机工程师都接触 和使用过 KEIL,相信大家都知道 KEIL 的使用是非常简单的,而且很容易上手。RVMDK3.80A 的编译器界面和 KEIL 十分相似,对于使用过 KEIL 的朋友来说,更容易上手。基于以上几点, 本书将选择 RVMDK3.80A 版本的编译器作为学习 STM32 的软件。当然大家也可以根据自己的 喜好换用 4.10 或以上版本的软件(注意最新版本的 MDK 可能将你的山寨 JLINK 刷成砖头,请 慎重考虑)。 3.3 新建基于固件库的 RVMDK 工程模板 在前面的章节我们介绍了 STM32 官方库包的一些知识,这些我们将着重讲解建立基于固 件库的工程模板的详细步骤。在此之前,首先我们要准备如下资料: 1) V3.5 固件库包:STM32F10x_StdPeriph_Lib_V3.5.0 这是 ST 官网下载的固件库完 整版,我们光盘目录: 软件资料\STM32 固件库使用参考资料\STM32F10x_StdPeriph_Lib_V3.5.0 我们官方论坛下载地址:http://openedv.com/posts/list/6054.htm 2) MDK3.8a 开发环境(我们的板子的开发环境目前是使用这个版本)。这在我们光盘 的软件目录下面有安装包:软件资料\软件\MDK3.80A 3) MDK 注册机,这在我们光盘的 MDK 同一目录下面有。 光盘目录:软件资料\软件\MDK3.80A\注册.exe 3.3.1 MDK3.8a 安装步骤 1) 找到 MDK 的安装文件并点击图标 ,这是 MDK 的安装文件,和安装其他软件一 样,相信大家都会明白,一直点击 Next 直到出现下面界面后,随意填写好您的信息,这些 信息其实没啥要求,可以随意填写,然后点击 Next。 www.openedv.com 43 ALIENTEK 战舰STM32开发板 图 3.3.1.1MDK 填写用户信息 2) 接着出现下面的界面,按图中所示选择之后点击“Finish”之后,MDK 便安装完成。 图 3.3.1.2 安装完成 3.3.2 添加 License Key MDK 针对每台机会有一个 CID,copy 这个 CID 到注册机处生成 License Key,然后再将这个 License Key 添加到 MDK 里面去注册。详细步骤后面会一一介绍。 1) 打开运行 MDK。这里要注意,有些版本的 windows 系统(如 Vista)需要右键点击快捷方式选 择“以管理员身份运行”,因为注册 license 需要管理员权限。打开 MDK 后有一个名字叫 “LPC2129 simulator”的默认 Project,暂时我们可以不用理会。 2) 点击:File->License Management,弹出一个 License Management 界面,copy 界面中的 (CID): www.openedv.com 44 ALIENTEK 战舰STM32开发板 图 3.3.2.1 License Management 选择 图 3.3.2.2 获取 CID 3) 打开光盘(软件资料\软件\MDK3.80A\注册.exe)下面的注册机 ,注册机我们会跟 MDK 安装包放在同一目录下面。 4) 接着会出现注册界面,黏贴刚才复制的 CID 到 CID 输入框,然后 Target 选择 ARM 之后,点 击“Generate”,30 位的 License Key 会在下图红色圈出的部分生成。License Key 的格 式:D0DY8-30KAK-0N8AM-X9Z14-A2NWP-J3LZZ。 www.openedv.com 45 ALIENTEK 战舰STM32开发板 图 3.3.2.3 生成 License Key 5) 将这个 License Key 黏贴到 Keil 的 License Management 界面的 New License Id Code 一 栏,然后点击“Add LIC”,添加成功后会出现成功提示。然后点击 Close 关闭这个界面即 可。到此 License Key 便添加完成。 图 3.3.2.4 添加 License Key 成功 3.3.3 新建工程模板 1) 回到 MDK 主界面,可以看到工程中有一个默认的工程,点击这个工程名字,然后选择菜单 Project->Close Project,就关闭掉这个工程了!这样整个 MDK 就是一个空的了,接下来 我们将建立我们的工程模版。 2) 在建立工程之前,我们建议用户在电脑的某个目录下面建立一个文件夹,后面所建立的工 www.openedv.com 46 ALIENTEK 战舰STM32开发板 程都可以放在这个文件夹下面,这里我们建立一个文件夹为 Template。 3) 点击 Keil 的菜单:Project –>New Uvision Project ,然后将目录定位到刚才建立的文件夹 Template 之下,在这个目录下面建立子文件夹 USER(我们的代码工程文件都是放在 USER 目录,很多人喜欢新建“Project”目录放在下面,这也是可以的,这个就看个人喜好了), 然后定位到 USER 目录下面,我们的工程文件就都保存到 USER 文件夹下面。工程命名为 Template,点击保存。 图 3.3.3.1 新建工程 www.openedv.com 47 ALIENTEK 战舰STM32开发板 图 3.3.3.2 定义工程名称 4) 接下来会出现一个选择 Device 的界面,就是选择我们的芯片型号,这里我们定位到 STMicroelectronics 下面的 STM32F103ZE(针对我们的战舰板子是这个型号,如果是其他芯 片,请选择对应的型号即可)。 www.openedv.com 48 ALIENTEK 战舰STM32开发板 图 3.3.3.3 选择芯片型号 5) 弹出对话框“Copy STM32 Startup Code to project ….”,询问是否添加启动代码到我们的工 程中,这里我们选择“否”,因为我们使用的 ST 固件库文件已经包含了启动文件。 6) 现在我们看看 USER 目录下面包含三个文件: 图 3.3.3.4 工程 USER 目录文件 7) 接 下 来 , 我 们 在 Template 工 程 目 录 下 面 , 新 建 3 个 文 件 夹 CORE, OBJ 以 及 STM32F10x_FWLib。CORE 用来存放核心文件和启动文件,OBJ 是用来存放编译过程文件 以及 hex 文件,STM32F10x_FWLib 文件夹顾名思义用来存放 ST 官方提供的库函数源码文 件。已有的 USER 目录除了用来放工程文件外,还用来存放主函数文件 main.c,以及其他包 括 system_stm32f10x.c 等等。 www.openedv.com 49 ALIENTEK 战舰STM32开发板 图 3.3.3.5 工程目录预览 8) 下面我们要将官方的固件库包里的源码文件复制到我们的工程目录文件夹下面。 打开官方固件库包,定位到我们之前准备好的固件库包的目录 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 官方库源码文件夹 9) 下面我们要将固件库包里面相关的启动文件复制到我们的工程目录 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 www.openedv.com 50 ALIENTEK 战舰STM32开发板 rm 下面,将里面 startup_stm32f10x_hd.s 文件复制到 CORE 下面。这里我们我之前已经解释了 不同容量的芯片使用不同的启动文件,我们的芯片 STM32F103ZET6 是大容量芯片,所以选择 这个启动文件。 现在看看我们的 CORE 文件夹下面的文件: 图 3.3.3.7 启动文件夹 10) 定位到目录: 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 目录文件浏览 11) 前面 10 个步骤,我们将需要的固件库相关文件复制到了我们的工程目录下面,下面我们将 这些文件加入我们的工程中去。右键点击 Target1,选择 Manage Components www.openedv.com 51 ALIENTEK 战舰STM32开发板 图 3.3.3.9 点击 Management Components 12) Project Targets 一栏,我们将 Target 名字修改为 Template,然后在 Groups 一栏删掉一个 Source Group1,建立三个 Groups:USER,CORE,FWLIB。然后点击 OK,可以看到我们的 Target 名字以及 Groups 情况。 www.openedv.com 52 ALIENTEK 战舰STM32开发板 图 3.3.3.10 图 3.3.3.11 13) 下面我们往 Group 里面添加我们需要的文件。我们按照步骤 12 的方法, 右键点击点击 Tempate,选择选择 Manage Components.然后选择需要添加文件的 Group,这里第一步我们 选 择 FWLIB , 然 后 点 击 右 边 的 Add Files, 定 位 到 我 们 刚 才 建 立 的 目 录 STM32F10x_FWLib/src 下面,将里面所有的文件选中(Ctrl+A),然后点击 Add,然后 Close. 可以看到 Files 列表下面包含我们添加的文件。 这里需要说明一下,对于我们写代码,如果我们只用到了其中的某个外设,我们就可以不 用添加没有用到的外设的库文件。例如我只用 GPIO,我可以只用添加 stm32f10x_gpio.c 而 其他的可以不用添加。这里我们全部添加进来是为了后面方便,不用每次添加,当然这样 的坏处是工程太大,编译起来速度慢,用户可以自行选择。 www.openedv.com 53 ALIENTEK 战舰STM32开发板 图 3.3.3.12 14) 用同样的方法,将 Groups 定位到 CORE 和 USER 下面,添加需要的文件。这里 我们的 CORE 下面需要添加的文件为 core_cm3.c,startup_stm32f10x_hd.s,USER 目录下面需要 添加的文件为 main.c,stm32f10x_it.c,system_stm32f10x.c. 这样我们需要添加的文件已经添加到我们的工程中去了,最后点击 OK,回到工程主界面。 www.openedv.com 54 ALIENTEK 战舰STM32开发板 图 3.3.3.13 www.openedv.com 图 3.3.3.14 55 ALIENTEK 战舰STM32开发板 图 3.3.3.15 15) 接下来我们要编译工程,在编译之前我们首先要选择编译中间文件编译后存放目录。 方法是点击魔术棒,然后选择“Output”选项下面的“Select folder for objects…”,然后选 择目录为我们上面新建的 OBJ 目录。 www.openedv.com 图 3.3.3.16 56 ALIENTEK 战舰STM32开发板 16) 下面我们点击编译按钮 编译工程,可以看到很多报错,因为找不到库文件。 图 3.3.3.17 17) 下面我们要告诉 MDK,在哪些路径之下搜索需要的头文件,也就是头文件目录。回到工 程主菜单,点击魔术棒 ,出来一个菜单,然后点击 c/c++选项.然后点击 Include Paths 右边的按钮。弹出一个添加 path 的对话框,然后我们将图上面的 3 个目录添加进去。记住, keil 只会在一级目录查找,所以如果你的目录下面还有子目录,记得 path 一定要定位到最 后一级子目录。然后点击 OK. www.openedv.com 57 ALIENTEK 战舰STM32开发板 图 3.3.3.18 www.openedv.com 图 3.3.3.219 58 ALIENTEK 战舰STM32开发板 图 3.3.3.20 18) 接下来,我们再来编译工程,可以看到又报了很多同样的错误。为什么呢?? 我们可以双击错误,然后会自动定位到文件 stm32f10x.h 中出错的地方,可以看到代码: #if !defined (STM32F10X_LD) && !defined (STM32F10X_LD_VL) && !defined (STM32F10X_MD) && !defined (STM32F10X_MD_VL) && !defined (STM32F10X_HD) && !defined (STM32F10X_HD_VL) && !defined (STM32F10X_XL) && !defined (STM32F10X_CL) #error "Please select first the target STM32F10x device used in your application (in stm32f10x.h file)"#endif 这是因为 3.5 版本的库函数在配置和选择外设的时候通过宏定义来选择的,所以我们需要 配置一个全局的宏定义变量。按照步骤 16,定位到 c/c++界面,然后填写 “STM32F10X_HD,USE_STDPERIPH_DRIVER”到 Define 输入框里面。 这里解释一下,如果你用的是中容量那么 STM32F10X_HD 修改为 STM32F10X_MD,小容 量修改为 STM32F10X_LD. 然后点击 OK。 www.openedv.com 59 ALIENTEK 战舰STM32开发板 图 3.3.3.21 19) 这次在编译之前,我们记得打开工程 USUR 下面的 main.c,复制下面代码到 main.c 覆盖已 有代码,然后进行编译。(记得在代码的最后面加上一个回车,否则会有警告),可以看到, 这次编译已经成功了。 #include "stm32f10x.h" void Delay(u32 count) { u32 i=0; for(;iPB.5 端口配置 //推挽输出 //IO 口速度为 50MHz //初始化 GPIOB.5 //PB.5 输出高 //LED1-->PE.5 推挽输出 //初始化 GPIO www.openedv.com 60 ALIENTEK 战舰STM32开发板 GPIO_SetBits(GPIOE,GPIO_Pin_5); 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); } } //PE.5 输出高 图 3.3.3.22 20) 这样一个工程模版建立完毕。下面还需要配置,让编译之后能够生成 hex 文件。同样点击 魔术棒,进入配置菜单,选择 Output。然后勾上下三个选项。 其中 Create HEX file 是编 译生成 hex 文件,Browser Information 是可以查看变量和函数定义。还有就是我们要选择 生产的 hex 文件和项目中间文件放在哪个目录,点击“Select folder for Objects…”定位目 录,我们的选择定位到上面建立的 OBJ 目录下面。 www.openedv.com 61 ALIENTEK 战舰STM32开发板 图 3.3.3.23 21) 重新编译代码,可以看到生成了 hex 文件在 OBJ 目录下面,这个文件我们用 mcuisp 下载 到 mcu 即可。 到这里,一个基于固件库 V3.5 的工程模板就建立了。 22) 实际上经过前面 21 个步骤,我们的工程模板已经建立完成。但是在我们 ALIENTEK 提供 的实验中,每个实验都有一个 SYSTEM 文件夹,下面有 3 个子目录分别为 sys,usart,delay, 存放的是我们每个实验都要使用到的共用代码,该代码是由我们 ALIENTEK 编写,该代码 的原理在我们第五章会有详细的讲解,我们这里只是引入到工程中,方便后面的实验建立 工程。 首先,找到我们实验光盘,打开任何一个固件库的实验,可以看到下面有一个 SYSTEM 文 件夹,比如我们打开实验 1 的工程目录如下: www.openedv.com 62 ALIENTEK 战舰STM32开发板 图 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 文件。 www.openedv.com 63 ALIENTEK 战舰STM32开发板 图 3.3.3.26 接下来我们将对应的三个目录(sys,usart,delay)加入到 PATH 中去,因为每个目录下面都有相 应的.h 头文件,这请参考步骤 17 即可,加入后的截图是: 图 3.3.3.27 最后点击 OK。这样我们的工程模板就彻底完成了。我们建立好的工程模板在我们光盘的实验 目录里面有,名字为“实验 0 Template 工程模板”大家可以打开对照一下。 www.openedv.com 64 ALIENTEK 战舰STM32开发板 3.4 MDK 下的程序下载与调试 上一节,我们学会了如何在 MDK 下创建 STM32 工程。本节,我们将向读者介绍 STM32 的代码下载以及调试。这里的调试包括了软件仿真和硬件调试(在线调试)。通过本章的学习, 你将了解到:1、STM32 的程序下载;2、STM32 在 MDK 下的软件仿真;3、利用 JLINK 对 STM32 进行在线调试。 3.4.1 STM32 软件仿真 MDK 的一个强大的功能就是提供软件仿真,通过软件仿真,我们可以发现很多将要出现 的问题,避免了下载到 STM32 里面来查这些错误,这样最大的好处是能很方便的检查程序存 在的问题,因为在 MDK 的仿真下面,你可以查看很多硬件相关的寄存器,通过观察这些寄存 器,你可以知道代码是不是真正有效。另外一个优点是不必频繁的刷机,从而延长了 STM32 的 FLASH 寿命(STM32 的 FLASH 寿命≥1W 次)。当然,软件仿真不是万能的,很多问题还 是要到在线调试才能发现。废话不多说了,接下来我们开始进行软件仿真。 上一章,我们创立了一个测试 STM32 串口 1 的工程,本节我们将教大家如何在 MDK3.80A 的软件环境下仿真这个工程,以验证我们代码的正确性。 在开始软件仿真之前,先检查一下配置是不是正确,在 IDE 里面点击 ,确定 Target 选 项卡内容如图 3.4.1.1 所示(主要检查芯片型号和晶振频率,其他的一般默认就可以): 图 3.4.1.1 Target 选项卡 确认了芯片以及外部晶振频率(8.0Mhz)之后,基本上就确定了 MDK3.80A 软件仿真的硬 件环境了,接下来,我们再点击 Debug 选项卡,设置为如图 3.4.1.2 所示: www.openedv.com 65 ALIENTEK 战舰STM32开发板 图 3.4.1.2 Debug 选项卡 在图 3.4.22 中,我们主要要确认的是 Use Simulator 是否选择(因为如果选择右边的 Use, 那就是用 ULINK 进行硬件 Debug 了,这个将在下面介绍),其他的采用默认的就可以。确认了 这项之后,我们便可以选择 OK,退出 Options for Target 对话框了。 接下来,我们点击 (开始/停止仿真按钮),开始仿真,出现如图 3.4.1.3 所示界面: 图 3.4.1.3 开始仿真 可以发现,多出了一个工具条,这就是 Debug 工具条,这个工具条在我们仿真的时候是非 www.openedv.com 66 ALIENTEK 战舰STM32开发板 常有用的,下面简单介绍一下 Debug 工具条相关按钮的功能。Debug 工具条部分按钮的功能如 图 3.4.1.4 所示: 图 3.4.1.4 Debug 工具条 复位:其功能等同于硬件上按复位按钮。相当于实现了一次硬复位。按下该按钮之后,代 码会重新从头开始执行。 执行到断点处:该按钮用来快速执行到断点处,有时候你并不需要观看每步是怎么执行的, 而是想快速的执行到程序的某个地方看结果,这个按钮就可以实现这样的功能,前提是你在查 看的地方设置了断点。 挂起:此按钮在程序一直执行的时候会变为有效,通过按该按钮,就可以使程序停止下来, 进入到单步调试状态。 执行进去:该按钮用来实现执行到某个函数里面去的功能,在没有函数的情况下,是等同 于执行过去按钮的。 执行过去:在碰到有函数的地方,通过该按钮就可以单步执行过这个函数,而不进入这个 函数单步执行。 执行出去:该按钮是在进入了函数单步调试的时候,有时候你可能不必再执行该函数的剩 余部分了,通过该按钮就直接一步执行完函数余下的部分,并跳出函数,回到函数被调用的位 置。 执行到光标处:该按钮可以迅速的使程序运行到光标处,其实是挺像执行到断点处按钮功 能,但是两者是有区别的,断点可以有多个,但是光标所在处只有一个。 汇编窗口:通过该按钮,就可以查看汇编代码,这对分析程序很有用。 观看变量/堆栈窗口:该按钮按下,会弹出一个显示变量的窗口,在里面可以查看各种你想 要看的变量值,也是很常用的一个调试窗口。 串口打印窗口:该按钮按下,会弹出一个类似串口调试助手界面的窗口,用来显示从串口 打印出来的内容。 内存查看窗口:该按钮按下,会弹出一个内存查看窗口,可以在里面输入你要查看的内存 地址,然后观察这一片内存的变化情况。是很常用的一个调试窗口 性能分析窗口:按下该按钮,会弹出一个观看各个函数执行时间和所占百分比的窗口,用 来分析函数的性能是比较有用的。 逻辑分析窗口:按下该按钮会弹出一个逻辑分析窗口,通过 SETUP 按钮新建一些 IO 口, 就可以观察这些 IO 口的电平变化情况,以多种形式显示出来,比较直观。 Debug 工具条上的其他几个按钮用的比较少,我们这里就不介绍了。以上介绍的是比较常 用的,当然也不是每次都用得着这么多,具体看你程序调试的时候有没有必要观看这些东西, 来决定要不要看。 这样,我们在上面的仿真界面里面选内存查看窗口、串口打印窗口。然后调节一下这两个 窗口的位置,如图 3.4.1.5 所示: www.openedv.com 67 ALIENTEK 战舰STM32开发板 图 3.4.1.5 调出仿真串口打印窗口 我们把光标放到 main.c 的 09 行的空白处,然后双击鼠标左键,可以看到在 09 行的左边出 现了一个红框,即表示设置了一个断点(也可以通过鼠标右键弹出菜单来加入),再次双击则取 消)。 然后我们点击 ,执行到该断点处,如图 3.4.1.6 所示: www.openedv.com 68 ALIENTEK 战舰STM32开发板 图 3.4.1.6 执行到断点处 我们现在先不忙着往下执行,点击菜单栏的 Peripherals->USARTs->USART 1。可以看到, 有很多外设可以查看,这里我们查看的是串口 1 的情况。如图 3.4.1.7 所示: 图 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)所示的串口信息。大家可以对比一下这两个图 www.openedv.com 69 ALIENTEK 战舰STM32开发板 的区别,就知道在 uart_init(9600);这个函数里面大概执行了哪些操作。 通过图 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 仿真持续时间 至此,我们软件仿真就结束了,通过软件仿真,我们在 MDK3.80A 中验证了代码的正确性, 接下来我们下载代码到硬件上来真正验证一下我们的代码是否在硬件上也是可行的。 3.4.2 STM32 程序下载 STM32 的程序下载有多种方法:USB、串口、JTAG、SWD 等,这几种方式,都可以用来 给 STM32 下载代码。不过,我们最常用的,最经济的,就是通过串口给 STM32 下载代码。本 节,我们将向大家介绍,如何利用串口给 STM32 下载代码。 www.openedv.com 70 ALIENTEK 战舰STM32开发板 STM32 的串口下载一般是通过串口 1 下载的,本指南的实验平台 ALIENTEK 战舰 STM32 开发板,不是通过 RS232 串口下载的,而是通过自带的 USB 串口来下载。看起来像是 USB 下 载(只需一根 USB 线,并不需要串口线)的,实际上,是通过 USB 转成串口,然后再下载的。 下面,我们就一步步教大家如何在实验平台上利用 USB 串口来下载代码。 首先要在板子上设置一下,在板子上把 RXD 和 PA(9 STM32 的 TXD),TXD 和 PA10(STM32 的 RXD)通过跳线帽连接起来,这样我们就把 CH340G 和 MCU 的串口 1 连接上了。这里由于 ALIENTEK 这款开发板自带了一键下载电路,所以我们并不需要去关心 BOOT0 和 BOOT1 的 状态,但是为了让下下载完后可以按复位执行程序,我们建议大家把 BOOT1 和 BOOT0 都设置 为 0。设置完成如下图所示: 图 3.4.2.1 开发板串口下载跳线设置 接着我们在 USB_232 处插入 USB 线,并接上电脑,如果之前没有安装 CH340G 的驱动(如 果已经安装过了驱动,则应该能在设备管理器里面看到 USB 串口,如果不能则要先卸载之前的 驱动,卸载完后重启电脑,再重新安装我们提供的驱动),则电脑会提示找到新硬件,如图 3.4.2.2 所示: www.openedv.com 71 ALIENTEK 战舰STM32开发板 图 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,这个一定是一样的。 如果没找到 USB 串口,则有可能是你安装有误,或者系统不兼容。 在安装了 USB 串口驱动之后,我们就可以开始串口下载代码了,这里我们的串口下载软件 选 择 的 是 mcuisp , 该 软 件 属 于 第 三 方 软 件 , 由 单 片 机 在 线 编 程 网 提 供 , 大 家 可 以 去 www.mcuisp.com 免费下载,本指南的光盘也附带了这个软件,版本为 V0.993。该软件启动界 面如图 3.4.2.5 所示: www.openedv.com 72 ALIENTEK 战舰STM32开发板 图 3.4.2.5 mcuisp 启动界面 然后我们选择要下载的 Hex 文件,以前面我们新建的工程为例,因为我们前面在工程建立 的时候,就已经设置了生成 Hex 文件,所以编译的时候已经生成了 Hex 文件,我们只需要找到 这个 Hex 文件下载即可。 用 mcuisp 软件打开 OBJ 文件夹,找到 TEST.hex,打开并进行相应设置后,如图 3.4.2.6 所 示: 图 3.4.2.6 mcuisp 设置 图 4.2.6 中圈中的设置,是我们建议的设置。编程后执行,这个选项在无一键下载功能的条 件下是很有用的,当选中该选项之后,可以在下载完程序之后自动运行代码。否则,还需要按 复位键,才能开始运行刚刚下载的代码。 编程前重装文件,该选项也比较有用,当选中该选项之后,mcuisp 会在每次编程之前,将 hex 文件重新装载一遍,这对于代码调试的时候是比较有用的。特别提醒:不要选择使用 RamIsp, 否则,可能没法正常下载。 最后,我们选择的 DTR 的低电平复位,RTS 高电平进 BootLoader,这个选择项选中,mcuisp www.openedv.com 73 ALIENTEK 战舰STM32开发板 就会通过 DTR 和 RTS 信号来控制板载的一键下载功能电路,以实现一键下载功能。如果不选 择,则无法实现一键下载功能。这个是必要的选项(在 BOOT0 接 GND 的条件下)。 在装载了 hex 文件之后,我们要下载代码还需要选择串口,这里 mcuisp 有智能串口搜索功 能。每次打开 mcuisp 软件,软件会自动去搜索当前电脑上可用的串口,然后选中一个作为默认 的串口(一般是你最后一次关闭时所选则的串口)。也可以通过点击菜单栏的搜索串口,来实 现自动搜索当前可用串口。串口波特率则可以通过 bps 那里设置,对于 STM32,该波特率最大 为 230400bps,这里我们一般选择最高的波特率:460800,让 mcuisp 自动去同步。找到 CH340 虚拟的串口,如图 3.4.2.7 所示: 图 3.4.2.7 CH340 虚拟串口 从之前 USB 串口的安装可知,开发板的 USB 串口被识别为 COM3 了(如果你的电脑是被 识别为其他的串口,则选择相应的串口即可),所以我们选择 COM3。选择了相应串口之后, 我们就可以通过按开始编程(P)这个按钮,一键下载代码到 STM32 上,下载成功后如图 3.4.2.8 所示: www.openedv.com 图 3.4.2.8 下载完成 74 ALIENTEK 战舰STM32开发板 图 4.2.8 中,我们用圈圈圈出了 mcuisp 对一键下载电路的控制过程,其实就是控制 DTR 和 RTS 电平的变化,控制 BOOT0 和 RESET,从而实现自动下载。另外,界面提示已经下载完成 (如果老提示:开始连接…,需要检查一下,开发板的设置是否正确,是否有其他因素干扰等), 并且从 0X80000000 处开始运行了,我们打开串口调试助手选择 COM3,会发现从 ALIENTEK 战舰 STM32 开发板发回来的信息,如图 3.4.2.9 所示: 图 3.4.2.9 程序开始运行了 接收到的数据和我们仿真的是一样的,证明程序没有问题。至此,说明我们下载代码成功 了,并且也从硬件上验证了我们代码的正确性。 3.4.3 STM32 硬件调试 上一节,我们介绍了如何通过利用串口给 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 栏选择仿真工具为 Cortex-M3 J-LINK,如图 3.4.3.1 所示: www.openedv.com 75 ALIENTEK 战舰STM32开发板 图 3.4.3.1 Debug 选项卡设置 上图中我们还勾选了 Run to main(),该选项选中后,只要点击仿真就会直接运行到 main 函 数,如果没选择这个选项,则会先执行 startup_stm32f10x_hd.s 文件的 Reset_Handler,再跳到 main 函数。 然后我们点击 Settings,设置 J-LINK 的一些参数,如图 3.4.3.2 所示: 图 3.4.3.2 J-LINK 模式设置 图 4.3.2 中,我们使用 J-LINK V8 的 SW 模式调试,因为我们 JTAG 需要占用比 SW 模式多 很多的 IO 口,而在 ALIENTEK 战舰 STM32 开发板上这些 IO 口可能被其他外设用到,可能造 成部分外设无法使用。所以,我们建议大家在调试的时候,一定要选择 SW 模式。Max Clock, www.openedv.com 76 ALIENTEK 战舰STM32开发板 可以点击 Auto Clk 来自动设置,图 4.3.2 中我们设置 SWD 的调试速度为 10Mhz,这里,如果你 的 USB 数据线比较差,那么可能会出问题,此时,你可以通过降低这里的速率来试试。 单击 OK,完成此部分设置,接下来我们还需要在 Utilities 选项卡里面设置下载时的目标编 程器,如图 3.4.3.3 所示: 图 3.4.3.3 FLASH 编程器选择 图 3.4.3.3 中,我们选择 J-LINK 来调试 Cortex M3,然后点击 Settings,设置如图 3.4.3.4 所 示: www.openedv.com 77 ALIENTEK 战舰STM32开发板 图 3.4.3.4 编程设置 这里要根据不同的 MCU 选择 FLASH 的大小,因为我们开发板使用的是 STM32F103ZET6, 其 FLASH 大小为 512KB,所以我们点击 Add,并在 Programming Algorithm 里面选择 512K 型 号的 STM32。然后选中 Reset and Run 选项,以实现在编程后自动启动,其他默认设置即可。 设置完成之后,如图 3.4.3.4 所示。 在设置完之后,点击 OK,然后再点击 OK,回到 IDE 界面,编译一下工程。再点击 , 开始仿真(如果开发板的代码没被更新过,则会先更新代码,再仿真,你也可以通过按 ,只 下载代码,而不进入仿真。特别注意:开发板上的 B0 和 B1 都要设置到 GND,否则代码下载 后不会自动运行的!),如图 3.4.3.5 所示: 图 3.4.3.5 开始仿真 因为我们之前勾选了 Run to main()选项,所以,程序直接就运行到了 main 函数的入口处, 我们在 delay_init()处设置了一个断点,点击 ,程序将会快速执行到该处。如图 3.4.3.6 所示: www.openedv.com 78 ALIENTEK 战舰STM32开发板 图 3.4.3.6 程序运行到断点处 接下来,我们就可以和软件仿真一样的开始操作了,不过这是真正的在硬件上的运行,而 不是软件仿真!其结果更可信。硬件调试就给大家介绍到这里。 3.5 RVMDK 使用技巧 通过前面的学习,我们已经了解了如何在 RVMDK 里面建立属于自己的工程。下面,我们 将向大家介绍 RVMDK 软件的一些使用技巧,这些技巧在代码编辑和编写方面会非常有用,希 望大家好好掌握,最好实际操作一下,加深印象。 3.5.1 文本美化 文本美化,主要是设置一些关键字、注释、数字等的颜色和字体。前面我们在介绍 RVMDK 新建工程的时候看到界面如图 3.5.1.1 所示: www.openedv.com 79 ALIENTEK 战舰STM32开发板 图 3.5.1.1 MDK 新建工程界面 这是 MDK 默认的设置,可以看到其中的关键字和注释等字体的颜色不是很漂亮,而 MDK 提供了我们自定义字体颜色的功能。我们可以在工具条上点击 5.1.2 所示界面: (编辑配置对话框)弹出如图 3. 图 3. 5.1.2 编辑配置对话框 在该对话框中我们选择 Colors&Fonts 选项卡,在该选项卡内,我们就可以设置自己的代码 的子体和颜色了。由于我们使用的是 C 语言,故在 Text File Types 下面选择 ARM:Editor C Files www.openedv.com 80 ALIENTEK 战舰STM32开发板 在右边就可以看到相应的元素了。如图 3. 5.1.3 所示: 图 3. 5.1.3 Colors&Fonts 选项卡 然后点击各个元素修改为你喜欢的颜色,当然也可以在 Font 栏设置你字体的类型,以及字 体的大小等。设置成之后,点击 OK,就可以在主界面看到你所修改后的结果,例如我修改后 的代码显示效果如图 3. 5.1.4 所示: 图 3.5.1.4 设置完后显示效果 这就比开始的效果好看一些了。当然你觉得字体小了可以在刚刚的对话框 Font 栏设置大一 www.openedv.com 81 ALIENTEK 战舰STM32开发板 点,觉得大了也可以设置小一点,总之设置到你认为可以为止。 细心的读者可能会发现,上面的代码里面有一个 u8,还是黑色的,这是一个用户自定义的 关键字,为什么不显示蓝色(假定刚刚已经设置了用户自定义关键字颜色为蓝色)呢?这就又 要回到我们刚刚的配置对话框了,单这次我们要选择 User Keywords 选项卡,同样选择 ARM: Editor C Files,在右边的 User Keywords 对话框下面输入你自己定义的关键字,如图 3. 5.1.5 所 示: 图 3. 5.1.5 用户自定义关键字 图 3.3.5 中我定义了 u8、u16、u32 等 3 个关键字,这样在以后的代码编辑里面只要出现这 三个关键字,肯定就会变成蓝色。点击 OK,再回到主界面,可以看到 u8 变成了蓝色了,如图 3. 5.1.6 所示: www.openedv.com 82 ALIENTEK 战舰STM32开发板 图 3. 5.1.6 设置完后显示效果 其实这个编辑配置对话框里面,还可以对其他很多功能进行设置,比如按 TAB 键右移多少 位,快捷键修改等,有兴趣的大家可以自己摸索一下。文本美化的技巧就为大家介绍到这里, 接下来我们为大家介绍 RVMDK 的代码编辑技巧。 3.5.2 代码编辑技巧 这里给大家介绍几个我常用的技巧,这些小技巧能给我们的代码编辑带来很大的方便,相 信对你的代码编写一定会有所帮助。 1)TAB 键的妙用 首先要介绍的就是 TAB 键的使用,这个键在很多编译器里面都是用来空位的,每按一下移 空几个位。如果你是经常编写程序的对这个键一定再熟悉不过了。但是 MDK 的 TAB 键和一般 编译器的 TAB 键有不同的地方,和 C++的 TAB 键差不多。MDK 的 TAB 键支持块操作。也就 是可以让一片代码整体右移固定的几个位,也可以通过 SHIFT+TAB 键整体左移固定的几个位。 假设我们前面的串口 1 中断响应函数如图 3. 5.2.1 所示: www.openedv.com 83 ALIENTEK 战舰STM32开发板 图 3. 5.2.1 头大的代码 图 3. 5.2.1 中这样的代码大家肯定不会喜欢,这还只是短短的 30 来行代码,如果你的代码 有几千行,全部是这个样子,不头大才怪。看到这样的代码我们就可以通过 TAB 键的妙用来快 速修改为比较规范的代码格式。 选中一块然后按 TAB 键,你可以看到整块代码都跟着右移了一定距离,如图 3. 5.2.2 所示: 图 3. 5.2.2 代码整体偏移 接下来我们就是要多选几次,然后多按几次 TAB 键就可以达到迅速使代码规范化的目的, www.openedv.com 84 ALIENTEK 战舰STM32开发板 最终效果如图 3. 5.2.3 所示 图 3. 5.2.3 修改后的代码 图 3. 5.2.3 中的代码相对于图 3. 5.2.1 中的要好看多了,经过这样的整理之后,整个代码一 下就变得有条理多了,看起来很舒服。 2) 快速定位函数/变量被定义的地方 上一节,我们介绍了 TAB 键的功能,接下来我们介绍一下如何快速查看一个函数或者变量 所定义的地方。 大家在调试代码或编写代码的时候,一定有想看看某个函数是在那个地方定义的,具体里 面的内容是怎么样的,也可能想看看某个变量或数组是在哪个地方定义的等。尤其在调试代码 或者看别人代码的时候,如果编译器没有快速定位的功能的时候,你只能慢慢的自己找,代码 量比较少还好,如果代码量一大,那就郁闷了,有时候要花很久的时间来找这个函数到底在哪 里。型号 MDK 提供了这样的快速定位的功能(顺便说一下 CVAVR 的 2.0 以后的版本也有这个 功能)。只要你把光标放到这个函数/变量(xxx)的上面(xxx 为你想要查看的函数或变量的名 字),然后右键,弹出如图 3.3.2.4 所示的菜单栏 : www.openedv.com 85 ALIENTEK 战舰STM32开发板 图 3.3.2.4 快速定位 在图 3.3.2.4 中,我们找到 Go to Definition Of‘delay_init’ 这个地方,然后单击左键就可 以快速跳到 delay_init 函数的定义处(注意要先在 Options for Target 的 Output 选项卡里面勾选 Browse Information 选项,再编译,再定位,否则无法定位!)。如图 3.3.2.5 所示: 图 3.3.2.5 定位结果 对于变量,我们也可以按这样的操作快速来定位这个变量被定义的地方,大大缩短了你查 找代码的时间。细心的大家会发现上面还有一个类似的选项,就是 Go to Reference To ‘delay_init’,这个是快速跳到该函数被声明的地方,有时候也会用到,但不如前者使用得多。 很多时候,我们利用 Go to Definition/ Reference 看完函数/变量的定义/申明后,又想返回之 前的代码继续看,此时我们可以通过 IDE 上的 之前的位置,这个按钮非常好用! 按钮(Back to previous position)快速的返回 www.openedv.com 86 ALIENTEK 战舰STM32开发板 3) 快速注释与快速消注释 接下来,我们介绍一下快速注释与快速消注释的方法。在调试代码的时候,你可能会想注 释某一片的代码,来看看执行的情况,MDK 提供了这样的快速注释/消注释块代码的功能。也 是通过右键实现的。这个操作比较简单,就是先选中你要注释的代码区,然后右键,选择 Advanced->Comment Selection 就可以了。 以 delay_init 函数为例,比如我要注释掉下图中所选中区域的代码,如图 3. 5.2.6 所示: 图 3. 5.2.6 选中要注释的区域 我们只要在选中了之后,选择右键,再选择 Advanced->Comment Selection 就可以把这段代 码注释掉了。执行这个操作以后的结果如图 3. 5.2.7 所示: 图 3. 5.2.7 注释完毕 这样就快速的注释掉了一片代码,而在某些时候,我们又希望这段注释的代码能快速的取 消注释,MDK 也提供了这个功能。与注释类似,先选中被注释掉的地方,然后通过右键 ->Advanced,不过这里选择的是 Uncomment Selection。 www.openedv.com 87 ALIENTEK 战舰STM32开发板 3.5.3 其他小技巧 除了前面介绍的几个比较常用的技巧,这里还介绍几个其他的小技巧,希望能让你的代码 编写如虎添翼。 第一个是快速打开头文件。在将光标放到要打开的引用头文件上,然后右键选择 Open Document“XXX”,就可以快速打开这个文件了(XXX 是你要打开的头文件名字)。如图 3. 5.3.1 所示: 图 3. 5.3.1 快速打开头文件 第二个小技巧是查找替换功能。这个和 WORD 等很多文档操作的替换功能是差不多的, 在 MDK 里面查找替换的快捷键是“CTRL+H”,只要你按下该按钮就会调出如图 3.3.3.2 所示界 面: 图 3. 5.3.2 替换文本 这个替换的功能在有的时候是很有用的,它的用法与其他编辑工具或编译器的差不多,相 信各位都不陌生了,这里就不在啰唆了。 第三个小技巧是跨文件查找功能,先双击你要找的函数/变量名(这里我们还是以系统时钟 初始化函数:delay_init 为例),然后再点击 IDE 上面的 ,弹出如图 3. 5.3.3 所示对话框: www.openedv.com 88 ALIENTEK 战舰STM32开发板 图 3. 5.3.3 跨文件查找 点击 Find,MDK 就会帮你找出所有含有 delay_init 字段的文件并列出其所在位置,如图 3. 5.3.4 所示: 图 3. 5.3.4 查找结果 该方法可以很方便的查找各种函数/变量,而且可以限定搜索范围(比如只查找.c 文件和.h 文件等),是非常实用的一个技巧。 3.5.4 调试技巧 MDK 自带了很多基础例程,在这里我们要介绍的是如何利用 MDK 自带的这些例程来 快速编写代码。当我开始接触 STM32 的时候,基本上没有直接操作寄存器的例子,网上 的所有例子几乎都是使用 ST 自带的例程。也就是 MDK 自带的使用库函数的版本。所以在 开始的时候,我就根据 MDK 提供的例子,对照数据手册,再结合 MDK 提供的查看寄存 器的功能,一步步把带库函数的例子改成了直接操作寄存器的。一开始会比较难,但是当 你掌握规律了之后,就可以很快的把 MDK 的使用库函数的代码改为直接操作寄存器的代 码。这里总结几个要点: 1,一款实用的开发板。 这个是实验的基础,有时候软件仿真通过了,在板上并不一定能跑起来,而且有个开 发板在手,什么东西都可以直观的看到,效果不是仿真能比的。但开发板不宜多,多了的 话连自己都不知道该学哪个了,觉得这个也还可以,那个也不错,那就这个学半天,那个 学半天,结果学个四不像。倒不如从一而终,学完一个在学另外一个。 2,两本参考资料,即《STM32 参考手册》和《Cortex-M3 权威指南》。 《STM32 参考手册》是 ST 出的官方资料,有 STM32 的详细介绍,包括了 STM32 的 各种寄存器定义以及功能等,是学习 STM32 的必备资料之一。而《Cortex-M3 权威指南》 则是对《STM32 参考手册》的补充,后者一般认为使用 STM32 的人都对 CM3 有了较深的 了解,所以 Cortex-M3 的很多东西它只是一笔带过,但前者对 Cortex-M3 有非常详细的说 明,这样两者搭配,你就基本上任何问题都能得到解决了。 3,多做实验,多做笔记。 一个初学者,一开始对 STM32 一般是没有概念的,所以首先要做的就是多做实验, www.openedv.com 89 ALIENTEK 战舰STM32开发板 一定要相信实践出真知,结合上面 2 本手册,你很快就会熟悉 STM32,进而随心所欲。其 次要多做笔记,在你不知道的时候,找 MDK 的例子,找第二点中的两本手册,当你碰到 新的知识点的时候,把它记下来,俗话说:好记性不如烂笔头。将你刚学到的东西用笔记 下了,对以后没有坏处。 只要以上三点做好了,学习 STM32 基本上就不会有什么问题了。当你有需要用的东 西,自己写代码写不出来了,就可以在 MDK 自带的例子中找找,看看是否有相关的例程。 对于 STM32 的外设,MDK 基本都是带有例程的,所以一般你的问题,可以在 MDK 自带 的例程中找到答案。 MDK 的例子分为 2 部分,一部分是与 USB 无关的,这部分代码存放在:D: \KEIL3.80A\ARM\Examples\ST\STM32F10xFWLib\Examples 目录下,而另外一部分与 USB 相关的例子则存放在:D:\KEIL3.80A\ARM\Examples\ST\STM32F10xUSBLib\Demos 目录 下(D 盘是我 MDK3.80A 的安装盘,所以这里路径是这样的,如果你安装在其他位置,修 改为相应的目录即可以)。 接下来我们用一个实例,来说明如何参考 MDK 的例子为自己所用。希望能起到抛砖 引玉的作用。这里以一个 IO 口翻转为例,其实就是 LED 的闪烁,看看如何借用 MDK 的 代码。首先打开 D:\KEIL3.80A\ARM\Examples\ST\STM32F10xFWLib\Examples 目录,可 以看到很多例子,如图 3. 5.4.1 所示: 图 3. 5.4.1 ST 提供的例程 IO 口翻转的例子在 GPIO 目录下的 IOToggle 下,我们将这个目录下面的所有文件拷贝 到 D:\KEIL3.80A\ARM\Examples\ST\STM32F10xFWLib\Project 里面,这里会提示如图 3. 5.4.2 所示的信息: www.openedv.com 90 ALIENTEK 战舰STM32开发板 图 3. 5.4.2 替换原有文件 我们选择全部就可以了。然后单击 Project.Uv2,打开工程,如图 3. 5.4.3 所示: 图 3. 5.4.3 替换后的工程 然后点击 ,编译一遍。可以看到如图 3.3.4.4 所示的编译结果: 图 3. 5.4.4 编译新工程 提示没有错误,没有警告。说明这个工程是可以用的。关于这个工程是如何使用的, 在 readme.txt 里面是有详细说明的,在使用之前最好先看看这个说明。重点看看硬件环境 的说明,如图 3. 5.4.5 所示: www.openedv.com 91 ALIENTEK 战舰STM32开发板 图 3. 5.4.5 查看工程说明 从图 3. 5.4.5 的说明可以知道,这个 LED 的翻转程序,对两款板子(STM3210B-EVAL 和 STM3210E-EVAL)分别是连在哪几个 IO 口上的,我们这个是在 USE_STM3210E_EVAL 板上运行的,所以使用的是 PF.6~9。 接下来我们要做的就是一步步跟踪代码,然后针对你的疑问点,打开 Peripherals 里面 的相关外设,查看寄存器,看看 MDK 的示例代码是如何一步步修改里面的寄存器来实现 的。对于外设的配置,MDK 一般都是调用库函数实现的,无法直接查看,这就需要你对 照手册,慢慢摸索了,根据从寄存器看到的结果,大概也就能推出 MDK 是如何实现这样 的操作了。其次一个重要的方法是通过查看汇编代码,来看到底是如何操作的,由于作者 对汇编不熟悉,这里就不废话了,免得误导大家。 这样对照着 MDK 的例子,看看自己的代码在哪些地方和它有不一样的地方,如果出 了问题,很可能就在这些不同的地方,只要根据 MDK 的示例来修改,一般你的问题就能 得到解决。当然,这过程中需要多多查看手册,看看手册里怎么说的,MDK 又是怎么做 的。 www.openedv.com 92 ALIENTEK 战舰STM32开发板 第四章 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; //设置相应位的值,不改变其他位的值 2) 移位操作提高代码的可读性。 www.openedv.com 93 ALIENTEK 战舰STM32开发板 移位操作在单片机开发中也非常重要,下面让我们看看固件库的 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 www.openedv.com 94 ALIENTEK 战舰STM32开发板 它的作用是:当标识符已经被定义过(一般是用#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 申明函数在外部定义的应用,这里我们就不多讲解了。 www.openedv.com 95 ALIENTEK 战舰STM32开发板 4.1.5 typedef 类型别名 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; www.openedv.com 96 ALIENTEK 战舰STM32开发板 结构体成员变量的引用方法是: 结构体变量名字.成员名 比如要引用 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); 这样,任何时候,我们只需要修改结构体成员变量,往结构体中间加入新的成员变量,而不需 要修改函数定义就可以达到修改入口参数同样的目的了。这样的好处是不用修改任何函数定义 就可以达到增加变量的目的。 www.openedv.com 97 ALIENTEK 战舰STM32开发板 理解了结构体在这个例子中间的作用吗?在以后的开发过程中,如果你的变量定义过多, 如果某几个变量是用来描述某一个对象,你可以考虑将这些变量定义在结构体中,这样也许可 以提高你的代码的可读性。 使用结构体组合参数,可以提高代码的可读性,不会觉得变量定义混乱。当然结构体的作 用就远远不止这个了,同时,MDK 中用结构体来定义外设也不仅仅只是这个作用,这里我们只 是举一个例子,通过最常用的场景,让大家理解结构体的一个作用而已。后面一节我们还会讲 解结构体的一些其他知识。 4.2 STM32 系统架构 STM32 的系统架构比 51 单片机就要强大很多了。STM32 系统架构的知识可以在《STM32 中文参考手册 V10》的 P25~28 有讲解,这里我们也把这一部分知识抽取出来讲解,是为了大 家在学习 STM32 之前对系统架构有一个初步的了解。这里的内容基本也是从中文参考手册中 参考过来的,让大家能通过我们手册也了解到,免除了到处找资料的麻烦吧。如果需要详细深 入的了解 STM32 的系统架构,还需要在网上搜索其他资料学习学习。 我们这里所讲的 STM32 系统架构主要针对的 STM32F103 这些非互联型芯片。首先我们看 看 STM32 的系统架构图: 图 4.2.1STM32 系统架构图 STM32 主系统主要由四个驱动单元和四个被动单元构成。 四个驱动单元是: 内核 DCode 总线; www.openedv.com 98 ALIENTEK 战舰STM32开发板 系统总线; 通用 DMA1; 通用 DMA2; 四被动单元是: AHB 到 APB 的桥:连接所有的 APB 设备; 内部 FlASH 闪存; 内部 SRAM; 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 的时钟系统图吧: www.openedv.com 99 ALIENTEK 战舰STM32开发板 图 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。倍频可选择为 www.openedv.com 100 ALIENTEK 战舰STM32开发板 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 www.openedv.com 101 ALIENTEK 战舰STM32开发板 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 www.openedv.com 102 ALIENTEK 战舰STM32开发板 4.4 端口复用和重映射 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 时钟使能 www.openedv.com 103 ALIENTEK 战舰STM32开发板 2) 复用的外设时钟使能 同时要初始化 GPIO 以及复用外设功能 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 ((uint32_t)0x00000002) #define GPIO_Remap_USART1 ((uint32_t)0x00000004) #define GPIO_Remap_USART2 ((uint32_t)0x00000008) #define GPIO_PartialRemap_USART3 ((uint32_t)0x00140010) www.openedv.com 104 ALIENTEK 战舰STM32开发板 #define GPIO_FullRemap_USART3 ((uint32_t)0x00140030) 从上面可以看出,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 中断优先级管理 这节我们将对 STM32 的重要只是中断管理做个详细的介绍。 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 { vu32 ISER[2]; u32 RESERVED0[30]; vu32 ICER[2]; u32 RSERVED1[30]; vu32 ISPR[2]; u32 RESERVED2[30]; vu32 ICPR[2]; u32 RESERVED3[30]; vu32 IABR[2]; u32 RESERVED4[62]; vu32 IPR[15]; www.openedv.com 105 ALIENTEK 战舰STM32开发板 } NVIC_TypeDef; STM32 的中断在这些寄存器的控制下有序的执行的。只有了解这些中断寄存器,才能了解 STM32 的中断。下面简要介绍这几个寄存器: ISER[2]:ISER 全称是:Interrupt Set-Enable Registers,这是一个中断使能寄存器组。上面 说了 STM32F103 的可屏蔽中断只有 60 个,这里用了 2 个 32 位的寄存器,总共可以表示 64 个 中断。而 STM32F103 只用了其中的前 60 位。ISER[0]的 bit0~bit31 分别对应中断 0~31。ISER[1] 的 bit0~27 对应中断 32~59;这样总共 60 个中断就分别对应上了。你要使能某个中断,必须设 置相应的 ISER 位为 1,使该中断被使能(这里仅仅是使能,还要配合中断分组、屏蔽、IO 口映 射等设置才算是一个完整的中断设置)。具体每一位对应哪个中断,请参考 stm32f10x_nvic..h 里 面的第 36 行处。 ICER[2]:全称是:Interrupt Clear-Enable Registers,是一个中断除能寄存器组。该寄存器组 与 ISER 的作用恰好相反,是用来清除某个中断的使能的。其对应位的功能,也和 ICER 一样。 这里要专门设置一个 ICER 来清除中断位,而不是向 ISER 写 0 来清除,是因为 NVIC 的这些寄 存器都是写 1 有效的,写 0 是无效的。具体为什么这么设计,请看《CM3 权威指南》第 125 页, NVIC 概览一章。 ISPR[2]:全称是:Interrupt Set-Pending Registers,是一个中断挂起控制寄存器组。每个位 对应的中断和 ISER 是一样的。通过置 1,可以将正在进行的中断挂起,而执行同级或更高级别 的中断。写 0 是无效的。 ICPR[2]:全称是:Interrupt Clear-Pending Registers,是一个中断解挂控制寄存器组。其作 用与 ISPR 相反,对应位也和 ISER 是一样的。通过设置 1,可以将挂起的中断接挂。写 0 无效。 IABR[2]:全称是:Active Bit Registers,是一个中断激活标志位寄存器组。这是一个只读 寄存器,通过它可以知道当前在执行的中断是哪一个。在中断执行完了由硬件自动清零。对应 位所代表的中断和 ISER 一样,如果为 1,则表示该位所对应的中断正在被执行。 IPR[15]:全称是:Interrupt Priority Registers,是一个中断优先级控制的寄存器组。这个寄 存器组相当重要!STM32 的中断分组与这个寄存器组密切相关。因为 STM32 的中断多达 60 多 个,所以 STM32 采用中断分组的办法来确定中断的优先级。IPR 寄存器组由 15 个 32bit 的寄存 器组成,每个可屏蔽中断占用 8bit,这样总共可以表示 15*4=60 个可屏蔽中断。刚好和 STM32 的可屏蔽中断数相等。IPR[0]的[31~24],[23~16],[15~8],[7~0]分别对应中中断 3~0,依次类 推,总共对应 60 个外部中断。而每个可屏蔽中断占用的 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 位是 www.openedv.com 106 ALIENTEK 战舰STM32开发板 响应优先级。每个中断,你可以设置抢占优先级为 0~7,响应优先级为 1 或 0。抢占优先级的 级别高于响应优先级。而数值越小所代表的优先级就越高。 这里需要注意两点:第一,如果两个中断的抢占优先级和响应优先级都是一样的话,则看 哪个中断先发生就先执行;第二,高优先级的抢占优先级是可以打断正在进行的低抢占优先级 中断的。而抢占优先级相同的中断,高优先级的响应优先级不可以打断低响应优先级的中断。 结合实例说明一下:假定设置中断优先级组为 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; www.openedv.com 107 ALIENTEK 战舰STM32开发板 uint8_t NVIC_IRQChannelPreemptionPriority; uint8_t NVIC_IRQChannelSubPriority; FunctionalState NVIC_IRQChannelCmd; } NVIC_InitTypeDef; NVIC_InitTypeDef 结构体中间有三个成员变量,这三个成员变量的作用是: NVIC_IRQChannel:定义初始化的是哪个中断,这个我们可以在 stm32f10x.h 中找到 每个中断对应的名字。例如 USART1_IRQn。 NVIC_IRQChannelPreemptionPriority:定义这个中断的抢占优先级别。 NVIC_IRQChannelSubPriority:定义这个中断的子优先级别。 NVIC_IRQChannelCmd:该中断是否使能。 比如我们要使能串口 1 的中断,同时设置抢占优先级为 1,子优先级位 2,初始化的方法是: USART_InitTypeDef USART_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 的几个寄存器的地址来讲解吧。 www.openedv.com 108 ALIENTEK 战舰STM32开发板 首先我们可以查看《STM32 中文参考手册 V10》中的寄存器地址映射表(P159): 图 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) www.openedv.com 109 ALIENTEK 战舰STM32开发板 所以我们便可以算出 GPIOA 的基地址位: 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 固件库开发的时候的一些小技巧,仅供初学者参考。这节的 www.openedv.com 110 ALIENTEK 战舰STM32开发板 知识大家可以在学习第一个跑马灯实验的时候参考一下,对初学者应该很有帮助。我们就用最 简单的 GPIO 初始化函数为例。 现在我们要初始化某个 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 函数体开始处,我们可以 看到在函数的开始处有如下几行: void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct) www.openedv.com 111 ALIENTEK 战舰STM32开发板 { …… /* 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)) 所以 GPIO_InitStruct->GPIO_Mode 成员的取值范围只能是上面定义的 8 种。这 8 中模式是通过 www.openedv.com 112 ALIENTEK 战舰STM32开发板 一个枚举类型组织在一起的。 同样的方法可以找出 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; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; www.openedv.com 113 ALIENTEK 战舰STM32开发板 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. This parameter can be a value of www.openedv.com 114 ALIENTEK 战舰STM32开发板 @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 是设置的哪个寄存器的哪个位,然后去中文参考手册查看该寄存 器相应位的定义以及前后文的描述。 这一节我们就讲解到这里,希望能对大家的开发有帮助。 www.openedv.com 115 ALIENTEK 战舰STM32开发板 第五章 SYSTEM 文件夹介绍 前面章节,我们介绍了如何在 MDK3.80A 下建立 STM32 工程,在这个新建的工程之 中,我们用到了一个 SYSTEM 文件夹里面的代码,此文件夹里面的代码由 ALIENTEK 提 供,包含了几乎每个实验都可能用到的延时函数,位带操作,串口打印代码等。这里我们 组织在 SYSTEM 文件夹下面,目的也就是让这些常用的代码能随用随调。 SYSTEM 文件夹下包含了 delay、sys、usart 等三个文件夹。分别包含了 delay.c、sys.c、 usart.c 及其头文件 delay.h,sys.h,usart.h。 本章,我们将向大家介绍这些代码,通过这章的学习,大家将了解到这些代码的由来, 也希望大家可以灵活使用 SYSTEM 文件夹提供的函数,实际应用到自己的项目中去。 本章包括如下 3 个小结: 5.1,delay 文件夹代码介绍; 5.2,sys 文件夹代码介绍; 5.3,usart 文件夹代码介绍; 5.1 delay 文件夹代码介绍 delay 文件夹内包含了 delay.c 和 delay.h 两个文件,这两个文件用来实现系统的延时功 能,其中包含 3 个函数(这里我们不讲 SysTick_Handler 函数,该函数在讲 ucos 的时候再 介绍): void delay_init(u8 SYSCLK); void delay_ms(u16 nms); void delay_us(u32 nus); 下面分别介绍这三个函数,在介绍之前,我们先了解一下编程思想:CM3 内核的处理 器,内部包含了一个 SysTick 定时器,SysTick 是一个 24 位的倒计数定时器,当计到 0 时, 将从 RELOAD 寄存器中自动重装载定时初值。只要不把它在 SysTick 控制及状态寄存器 中的使能位清除,就永不停息。SysTick 在《STM32 的参考手册》(这里是指 V10.0 版本, 下同)里面介绍的很简单,其详细介绍,请参阅《Cortex-M3 权威指南》第 133 页。我们 就是利用 STM32 的内部 SysTick 来实现延时的,这样既不占用中断,也不占用系统定时器。 这里我们将介绍的是 ALIENTEK 提供的最新版本的延时函数,该版本的延时函数支持 在 ucos 下面使用,它可以和 ucos 共用 systick 定时器。首先我们简单介绍下 ucos 的时钟: ucos 运 行 需 要 一 个 系 统 时 钟 节 拍 ( 类 似 “ 心 跳 ”), 而 这 个 节 拍 是 固 定 的 ( 由 OS_TICKS_PER_SEC 设置),比如 5ms(设置:OS_TICKS_PER_SEC=200 即可),在 STM32 下面,一般是由 systick 来提供这个节拍,也就是 systick 要设置为 5ms 中断一次,为 ucos 提供时钟节拍,而且这个时钟一般是不能被打断的(否则就不准了)。 因为在 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 时间到了。 下面我们开始介绍这几个函数。 www.openedv.com 116 ALIENTEK 战舰STM32开发板 5.1.1 delay_init 函数 该函数用来初始化 2 个重要参数:fac_us 以及 fac_ms;同时把 SysTick 的时钟源选择 为外部时钟,如果使用了 ucos,那么还会根据 OS_TICKS_PER_SEC 的配置情况,来配置 SysTick 的中断时间,并开启 SysTick 中断。具体代码如下: //初始化延迟函数 //SYSTICK 的时钟固定为 HCLK 时钟的 1/8 void delay_init() { //如果 OS_CRITICAL_METHOD 定义了,说明使用 ucosII 了. #ifdef OS_CRITICAL_METHOD u32 reload; #endif SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);//选择外部时钟 HCLK/8 fac_us=SystemCoreClock/8000000; //为系统时钟的 1/8 //如果 OS_CRITICAL_METHOD 定义了,说明使用 ucosII 了. #ifdef OS_CRITICAL_METHOD reload=SystemCoreClock/8000000; //每秒钟的计数次数 单位为 K reload*=1000000/OS_TICKS_PER_SEC;//根据 OS_TICKS_PER_SEC 设定溢出时间 //reload 为 24 位寄存器,最大值:16777216,在 72M 下, //约 1.86s 左右 fac_ms=1000/OS_TICKS_PER_SEC;//代表 ucos 可以延时的最少单位 SysTick->CTRL|=SysTick_CTRL_TICKINT_Msk; //开启 SYSTICK 中断 SysTick->LOAD=reload; //每 1/OS_TICKS_PER_SEC 秒中断一次 SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk; //开启 SYSTICK #else fac_ms=(u16)fac_us*1000;//非 ucos 下,代表每个 ms 需要的 systick 时钟数 #endif } 可以看到,delay_init 函数使用了条件编译,来选择不同的初始化过程,如果不使用 ucos 的时候,就和《不完全手册》介绍的方法是一样的,而如果使用 ucos 的时候,则会进行一 些不同的配置,这里的条件编译是根据 OS_CRITICAL_METHOD 这个宏来确定的,因为 只要使用了 ucos,就一定会定义 OS_CRITICAL_METHOD 这个宏。 SysTick 是 MDK 定义了的一个结构体(在 stm32f10x_map.h 里面),里面包含 CTRL、 LOAD、VAL、CALIB 等 4 个寄存器, SysTick->CTRL 的各位定义如图 5.1.1.1 所示: www.openedv.com 117 ALIENTEK 战舰STM32开发板 图 5.1.1.1 SysTick->CTRL 寄存器各位定义 SysTick-> LOAD 的定义如图 5.1.1.2 所示: 图 5.1.1.2 SysTick->LOAD 寄存器各位定义 SysTick-> VAL 的定义如图 5.1.1.3 所示: 图 5.1.1.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 的值。 在不使用 ucos 的时候:fac_us,为 us 延时的基数,也就是延时 1us,SysTick->LOAD 所应设置的值。fac_ms 为 ms 延时的基数,也就是延时 1ms,SysTick->LOAD 所应设置的 值。fac_us 为 8 位整形数据,fac_ms 为 16 位整形数据。正因为如此,系统时钟如果不是 8 的倍数,则会导致延时函数不准确,这也是我们推荐外部时钟选择 8M 的原因。这点大家 要特别留意。 当 使 用 ucos 的 时 候 , fac_us , 还 是 us 延 时 的 基 数 , 不 过 这 个 值 不 会 被 写 到 SysTick->LOAD 寄存器来实现延时,而是通过时钟摘取的办法实现的(后面会介绍)。而 fac_ms 则 代 表 ucos 自 带 的 延 时 函 数 所 能 实 现 的 最 小 延 时 时 间 ( 如 OS_TICKS_PER_SEC=200,那么 fac_ms 就是 5ms)。 5.1.2 delay_us 函数 该函数用来延时指定的 us,其参数 nus 为要延时的微秒数。该函数有使用 ucos 和不使 www.openedv.com 118 ALIENTEK 战舰STM32开发板 用 ucos 两个版本,这里我们分别介绍,首先是不使用 ucos 的时候,实现函数如下: //延时 nus //nus 为要延时的 us 数. 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 被意外关闭导致的死循环。这里面有一行开 启 Systick 开始倒数代码需要解释一下: SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ; 其中 SysTick_CTRL_ENABLE_Msk 是 MDK 宏定义的一个变量,它的值就是 0x01,这行代 码的意思就是设置 SysTick->CTRL 的第一位为 1,使能定时器。 再来看看使用 ucos 的时候,delay_us 的实现函数如下: //延时 nus //nus 为要延时的 us 数. void delay_us(u32 nus) { u32 ticks; u32 told,tnow,tcnt=0; u32 reload=SysTick->LOAD; //LOAD 的值 ticks=nus*fac_us; //需要的节拍数 tcnt=0; OSSchedLock(); //阻止 ucos 调度,防止打断 us 延时 told=SysTick->VAL; //刚进入时的计数器值 while(1) { tnow=SysTick->VAL; if(tnow!=told) www.openedv.com 119 ALIENTEK 战舰STM32开发板 { if(tnow=ticks)break;//时间超过/等于要延迟的时间,则退出. } }; OSSchedUnlock(); //开启 ucos 调度 } 这里就正是利用了我们前面提到的时钟摘取法,ticks 是延时 nus 需要等待的 SysTick 计数次数(也就是延时时间),told 用于记录最近一次的 SysTick->VAL 值,然后 tnow 则是 当前的 SysTick->VAL 值,通过他们的对比累加,实现 SysTick 计数次数的统计,统计值存 放在 tcnt 里面,然后哦通过对比 tcnt 和 ticks,来判断延时是否到达,从而达到不修改 SysTick 实现 nus 的延时,从而可以和 ucos 共用一个 SysTick。 上面的 OSSchedLock 和 OSSchedUnlock 是 ucos 提供的两个函数,用于调度上锁和解 锁,这里为了防止 ucos 在 delay_us 的时候打断延时,可能导致的延时不准,所以我们利用 这两个函数来实现免打断,从而保证延时精度!同时,此时的 delay_us,可以实现最长 2^32us 的延时,大概是 4294 秒。 5.1.3 delay_ms 函数 该函数用来延时指定的 ms,其参数 nms 为要延时的微秒数。该函数同样有使用 ucos 和不使用 ucos 两个版本,这里我们分别介绍,首先是不使用 ucos 的时候,实现函数如下: //延时 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; //清空计数器 } www.openedv.com 120 ALIENTEK 战舰STM32开发板 此部分代码和 5.1.2 节的 delay_us(非 ucos 版本)大致一样,但是要注意因为 LOAD 仅仅是一个 24bit 的寄存器,延时的 ms 数不能太长。否则超出了 LOAD 的范围,高位会被 舍去,导致延时不准。最大延迟 ms 数可以通过公式:nms<=0xffffff*8*1000/SYSCLK 计算。 SYSCLK 单位为 Hz,nms 的单位为 ms。如果时钟为 72M,那么 nms 的最大值为 1864ms。 超过这个值,建议通过多次调用 delay_ms 实现,否则就会导致延时不准确。 再来看看使用 ucos 的时候,delay_ms 的实现函数如下: //延时 nms //nms:要延时的 ms 数 void delay_ms(u16 nms) { if(OSRunning==TRUE)//如果 os 已经在跑了 { if(nms>=fac_ms)//延时的时间大于 ucos 的最少时间周期 { OSTimeDly(nms/fac_ms);//ucos 延时 } nms%=fac_ms;//ucos 已经无法提供这么小的延时了,采用普通方式延时 } delay_us((u32)(nms*1000)); //普通方式延时 } 该函数中,OSRunning 是 ucos 正在运行的一个标志,OSTimeDly 是 ucos 提供的一个 基 于 ucos 时 钟 节 拍 的 延 时 函 数 , 其 参 数 代 表 延 时 的 时 钟 节 拍 数 ( 假 设 OS_TICKS_PER_SEC=200,那么 OSTimeDly(1),就代表延时 5ms)。 当 ucos 还未运行的时候,我们的 delay_ms 就是直接由 delay_us 实现的,ucos 下的 delay_us 可以实现很长的延时而不溢出!,所以放心的使用 delay_us 来实现 delay_ms,不过 由于 delay_us 的时候,任务调度被上锁了,所以还是建议不要用 delay_us 来延时很长的时 间,否则影响整个系统的性能。 当 ucos 运行的时候,我们的 delay_ms 函数将先判断延时时长是否大于等于 1 个 ucos 时钟节拍(fac_ms),当大于这个值的时候,我们就通过调用 ucos 的延时函数来实现(此时 任务可以调度),不足 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 口进行输入输出读取和控制。 www.openedv.com 121 ALIENTEK 战舰STM32开发板 位带操作简单的说,就是把每个比特膨胀为一个 32 位的字,当访问这些字的时候就达 到了访问比特的目的,比如说 BSRR 寄存器有 32 个位,那么可以映射到 32 个地址上,我 们去访问这 32 个地址就达到访问 32 个比特的目的。这样我们往某个地址写 1 就达到往对 应比特位写 1 的目的,同样往某个地址写 0 就达到往对应的比特位写 0 的目的。 寄存器32个位 32个对应地址 Bit31 Bit30 Bit29 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! www.openedv.com 122 ALIENTEK 战舰STM32开发板 #define PAout(n) BIT_ADDR(GPIOA_ODR_Addr,n) //输出 #define PAin(n) BIT_ADDR(GPIOA_IDR_Addr,n) //输入 #define PBout(n) BIT_ADDR(GPIOB_ODR_Addr,n) //输出 #define PBin(n) BIT_ADDR(GPIOB_IDR_Addr,n) //输入 #define PCout(n) BIT_ADDR(GPIOC_ODR_Addr,n) //输出 #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,实际是设置了寄存器的某个位,但是我们 的定义中可以跟踪过去看到却是通过计算访问了一个地址。上面一系列公式也就是计算 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.2.2 中断分组设置函数 在 sys.c 里面只有一个函数就是 void NVIC_Configuration()中断配置函数,在这个函数 里面我们只调用了固件库的中断分组配置函数,这只整个系统的中断分组为组别 2.这个函 数在系统初始化的时候调用即可,并且永远只需要调用一次。 void NVIC_Configuration(void) { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置 NVIC 中断分组 2:2 位抢占//优先级,2 位响应优先级 }} 5.3 usart 文件夹介绍 usart 文件夹内包含了 usart.c 和 usart.h 两个文件。这两个文件用于串口的初始化和中 www.openedv.com 123 ALIENTEK 战舰STM32开发板 断接收。这里只是针对串口 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; }; 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; www.openedv.com 124 ALIENTEK 战舰STM32开发板 USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA |RCC_APB2Periph_AFIO, 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 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 时钟,然后使能复用功能时 钟和内置外设时钟。 www.openedv.com 125 ALIENTEK 战舰STM32开发板 接下来我们要初始化相应的 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 的知识我们在跑马灯实例会讲解到,这里暂时不做深入的讲解。 紧接着,我们要进行 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;//一般设置为 9600; 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 { www.openedv.com 126 ALIENTEK 战舰STM32开发板 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); //使能串口 在开启串口中断和使能串口之后接下来就是写中断处理函数了,下面一节我们将着重讲解 中断处理函数。 5.3.3 USART1_IRQHandler 函数 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 bit13~0 接收完成 标志 接收到 0X0D 标志 接收到的有效数据个数 设计思路如下: 表 5.3.1.1 接收状态寄存器位定义表 www.openedv.com 127 ALIENTEK 战舰STM32开发板 当接收到从电脑发过来的数据,把接收到的数据保存在 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 { 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 } www.openedv.com 128 ALIENTEK 战舰STM32开发板 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,则不调用这两个函数(这两个函数用于 实现中断嵌套处理,这里我们先不理会)。 www.openedv.com 129 ALIENTEK 战舰STM32开发板 第三篇 实战篇 经过前两篇的学习,我们对 STM32 开发的软件和硬件平台都有了个比较深入的了解了, 接下来我们将通过实例,由浅入深,带大家一步步的学习 STM32。 STM32 的内部资源非常丰富,对于初学者来说,一般不知道从何开始。本篇将从 STM32 最简单的外设说起,然后一步步深入。每一个实例都配有详细的代码及解释,手把手教你如何 入手 STM32 的各种外设,通过本篇的学习,希望大家能学会 STM32 绝大部分外设的使用。 本篇总共分为 56 章,每一章即一个实例,下面就让我们开始精彩的 STM32 之旅。 我们固件库版本源码在光盘目录:程序源码\标准例程-V3.5 库函数版本\ 之下。 www.openedv.com 130 ALIENTEK 战舰STM32开发板 第六章 跑马灯实验 STM32 最简单的外设莫过于 IO 口的高低电平控制了,本章将通过一个经典的跑马灯程序, 带大家开启 STM32 之旅,通过本章的学习,你将了解到 STM32 的 IO 口作为输出使用的方法。 在本章中,我们将通过代码控制 ALIENTEK 战舰 STM32 开发板上的两个 LED:DS0 和 DS1 交替闪烁,实现类似跑马灯的效果。 本章分为如下四个小节: 6.1, STM32 IO 口简介 6.2, 硬件设计 6.3, 软件设计 6.4, 仿真与下载 www.openedv.com 131 ALIENTEK 战舰STM32开发板 6.1 STM32 IO 简介 本章将要实现的是控制 ALIENTEK 战舰 STM32 开发板上的两个 LED 实现一个类似跑马灯 的效果,该实验的关键在于如何控制 STM32 的 IO 口输出。了解了 STM32 的 IO 口如何输出的, 就可以实现跑马灯了。通过这一章的学习,你将初步掌握 STM32 基本 IO 口的使用,而这是迈 向 STM32 的第一步。 这一章节因为是第一个实验章节,所以我们在这一章将讲解一些知识为后面的实验做铺垫。 为了小节标号与后面实验章节一样,这里我们不另起一节来讲。 在讲解 STM32 的 GPIO 之前,首先打开我们光盘的第一个固件库版本实验工程跑马灯实验 工程(光盘目录为:“4,程序源码\标准例程-V3.5 库函数版本\实验 1 跑马灯/USER/LED.Uv2”), 可以看到我们的实验工程目录: 图 6.1.1 跑马灯实验目录结构 接下来我们逐一讲解一下我们的工程目录下面的组以及重要文件。 ① 组 FWLib 下面存放的是 ST 官方提供的固件库函数,里面的函数我们可以根据需要添加 和删除,但是一定要注意在头文件 stm32f10x_conf.h 文件中注释掉删除的源文件对应的 头文件,这里面的文件内容用户不需要修改。 ② 组 CORE 下面存放的是固件库必须的核心文件和启动文件。这里面的文件用户不需要修 改。 ③ 组 SYSTEM 是 ALIENTEK 提供的共用代码,这些代码的作用和讲解在第五章都有讲解, 大家可以翻过去看下。 www.openedv.com 132 ALIENTEK 战舰STM32开发板 ④ 组 HARDWARE 下面存放的是每个实验的外设驱动代码,他的实现是通过调用 FWLib 下面的固件库文件实现的,比如 led.c 里面调用 stm32f10x_gpio.c 里面的函数对 led 进行 初始化,这里面的函数是讲解的重点。后面的实验中可以看到会引入多个源文件。 ⑤ 组 USER 下面存放的主要是用户代码。但是 system_stm32f10x.c 文件用户不需要修改, 同时 stm32f10x_it.c 里面存放的是中断服务函数,这两个文件的作用在 3.1 节有讲解,大 家可以翻过去看看。Main.c 函数主要存放的是主函数了,这个大家应该很清楚。 针对第①步中怎么随意添加和删除固件库文件,这里我们稍微讲解一下。 首先从上面的图中可以看到,stm32f10x_gpio.c 源文件下面 include 了好几个头文件,其中有 一个 stm32f10x_conf.h,这个文件会被每个固件库源文件引用。我们可以打开看看里面的内容: 图 6.1.2 stm32f10x_conf 文件内容 从图中可以看出,在头文件 stm32f10x_conf.h 文件中,我们包含了四个.h 头文件,那是因 为我们的 FWLib 组下面引入了相应的 4 个.c 源文件。同时大家记住,后面三个源文件 stm32f10x_rcc.c,stm32f10x_usart.c 以及 misc.c 在每个实验基本都需要添加。在这个实验中,因 为 LED 是关系到 STM32 的 GPIO,所以我们增加了 stm32f10x_gpio.c 和头文件 stm32f10x_gpio.h 的引入。添加和删除固件库源文件的步骤是: 1. 在 stm32f10x_conf.h 文件 引 入 需 要的 .h 头文 件。 这 些 头 文件 在 每个 实验 的 目 录 \STM32F10x_FWLib\inc 下面都有存放。 2. 在 FWLib 下面加入步骤一中引入的.h 头文件对应的源文件。记住最好一一对应,否则 就有可能会报错。这些源文件在每个实验的\STM32F10x_FWLib\src 目录下面都有存 放。添加方法请参考 3.3 节的内容。 最后我们讲解一下这些组之间的层次结构: www.openedv.com 133 ALIENTEK 战舰STM32开发板 组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、开漏复用功能 每个 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 所示: www.openedv.com 134 ALIENTEK 战舰STM32开发板 表 6.1.4 STM32 的 IO 口位配置表 STM32 输出模式配置如表 6.1.5 所示: 表 6.1.5 STM32 输出模式配置表 接下来我们看看端口低配置寄存器 CRL 的描述,如图 6.1.6 所示: 图 6.1.6 端口低配置寄存器 CRL 各位描述 该寄存器的复位值为 0X4444 4444,从图 6.1.4 可以看到,复位值其实就是配置端口为浮空 www.openedv.com 135 ALIENTEK 战舰STM32开发板 输入模式。从上图还可以得出: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 { 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 //复用推挽 www.openedv.com 136 ALIENTEK 战舰STM32开发板 }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 所示: 图 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 所示: www.openedv.com 137 ALIENTEK 战舰STM32开发板 图 6.1.8 端口输出数据寄存器 ODR 各位描述 在固件库中设置 ODR 寄存器的值来控制 IO 口的输出状态是通过函数 GPIO_Write 来实现 的: void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal); 该函数一般用来往一次性一个 GPIO 的多个端口设值。 BSRR 寄存器是端口位设置/清除寄存器。该寄存器和 ODR 寄存器具有类似的作用,都可 以用来设置 GPIO 端口的输出位是 1 还是 0。下面我们看看该寄存器的描述如下图: 图 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 端口的输入和输出状态。比如我们要设 www.openedv.com 138 ALIENTEK 战舰STM32开发板 置 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 战舰 STM32 开发板上默 认是已经连接好了的。DS0 接 PB5,DS1 接 PE5。所以在硬件上不需要动任何东西。其连接原 理图如图 6.2.1 下: 图 6.2.1 LED 与 STM32 连接原理图 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 文件夹中都需要使用到,所以每个实验都会引用。 在 stm32f10x_conf.h 文件里面,我们注释掉其他不用的头文件,只引入以下头文件: #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" #include "stm32f10x_usart.h" #include "misc.h" 首先,找到之前 3.3 节新建的 Template 工程,在该文件夹下面新建一个 HARDWARE 的文 件夹,用来存储以后与硬件相关的代码,然后在 HARDWARE 文件夹下新建一个 LED 文件夹, 用来存放与 LED 相关的代码。如图 6.3.1 所示: www.openedv.com 139 ALIENTEK 战舰STM32开发板 图 6.3.1 新建 HARDWARE 文件夹 然后我们打开 USER 文件夹下的 LED.Uv2 工程(如果是使用的上面新建的工程模板,那么 就是 Template.Uv2,大家可以将其重命名为 LED.Uv2),按 按钮新建一个文件,然后保存在 HARDWARE->LED 文件夹下面,保存为 led.c。在该文件中输入如下代码: #include "led.h" //初始化 PB5 和 PE5 为输出口.并使能这两个口的时钟 //LED IO 初始化 void LED_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB| RCC_APB2Periph_GPIOE, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //使能 PB,PE 端口时钟 //LED0-->PB.5 推挽输出 //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_SetBits(GPIOB,GPIO_Pin_5); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //PB.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| www.openedv.com 140 ALIENTEK 战舰STM32开发板 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; //LED1-->GPIOE.5 推挽输出 GPIO_Init(GPIOE, &GPIO_InitStructure); //初始化 GPIOE.5 GPIO_SetBits(GPIOE,GPIO_Pin_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 中输入如下代码: #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 口操作。 如下: GPIO_SetBits(GPIOB, GPIO_Pin_5); //设置 GPIOB.5 输出 1,等同 LED0=1; GPIO_ResetBits (GPIOB, GPIO_Pin_5); //设置 GPIOB.5 输出 0,等同 LED0=0; 有兴趣的朋友不妨修改我们的位带操作为库函数直接操作,这样也有利于学习。 将 led.h 也保存一下。接着,我们在 Manage Components 管理里面新建一个 HARDWARE 的组,并把 led.c 加入到这个组里面,如图 6.3.2 所示: www.openedv.com 141 ALIENTEK 战舰STM32开发板 图 6.3.2 给工程新增 HARDWARE 组 单击 OK,回到工程,然后你会发现在 Project Workspace 里面多了一个 HARDWARE 的组, 在改组下面有一个 led.c 的文件。如图 6.3.3 所示: 图 6.3.3 新增 HARDWARE 组 然后用之前介绍的方法(在 3.3.3 节介绍的)将 led.h 头文件的路径加入到工程里面。回到 主界面,在 main 函数里面编写如下代码: #include "led.h" #include "delay.h" #include "sys.h" //ALIENTEK 战舰 STM32 开发板实验 1 www.openedv.com 142 ALIENTEK 战舰STM32开发板 //跑马灯实验 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 操作,我们也可以修改 main()函数,直接通过库函数来操作 IO 达到同样的效果,大家不妨试试。 int main(void) { delay_init(); //延时函数初始化 LED_Init(); //初始化与 LED 连接的硬件接口 while(1) { GPIO_ResetBits(GPIOB,GPIO_Pin_5); // PB5 输出低,LED0=0; GPIO_SetBits(GPIOE,GPIO_Pin_5); // PE5 输出高,LED1=1; delay_ms(300); //延时 300ms GPIO_SetBits(GPIOB,GPIO_Pin_5); //PB5 输出高,LED0=1; GPIO_ResetBits(GPIOE,GPIO_Pin_5); // PE5 输出低,LED1=0; delay_ms(300); //延时 300ms } } 将主函数替换为上面代码,然后重新执行,可以看到,结果跟用位带操作一样的效果。大 家可以对比一下。这个代码在我们光盘的实验代码“实验 1 跑马灯-库函数操作“中。 然后按 ,编译工程,得到结果如图 6.3.4 所示: www.openedv.com 143 ALIENTEK 战舰STM32开发板 图 6.3.4 编译结果 可以看到没有错误,也没有警告。接下来,我们就先进行软件仿真,验证一下是否有错误 的地方,然后下载到战舰 STM32 开发板看看实际运行的结果。 6.4 仿真与下载 此代码,我们先进行软件仿真,看看结果对不对,根据软件仿真的结果,然后再下载到 ALIENTEK 战舰 STM32 板子上面看运行是否正确。 首先,我们进行软件仿真(请先确保 Options for Target Debug 选项卡里面已经设置为 Use Simulator)。先按 开始仿真,接着按 ,显示逻辑分析窗口,点击 Setup,新建两个信号 PORTB.5 和 PORTE.5,如图 6.4.1 所示: 图 6.4.1 逻辑分析设置 Display Type 选择 bit,然后单击 Close 关闭该对话框,可以看到逻辑分析窗口出来了两个 信号,如图 6.4.2 所示: www.openedv.com 144 ALIENTEK 战舰STM32开发板 图 6.4.2 设置后的逻辑分析窗口 接着,点击 ,开始运行。运行一段时间之后,按 可以看到如图 6.4.3 所示的波形: 按钮,暂停仿真回到逻辑分析窗口, 图 6.4.3 仿真波形 这里注意 Gird 要调节到 0.25s 左右比较合适,可以通过 Zoom 里面的 In 按钮来放大波形, 通过 Out 按钮来缩小波形,或者按 All 显示全部波形。从上图中可以看到 PORTB.5 和 PORTE.5 www.openedv.com 145 ALIENTEK 战舰STM32开发板 交替输出,周期可以通过中间那根红线来测量。至此,我们的软件仿真已经顺利通过。 在软件仿真没有问题了之后,我们就可以把代码下载到开发板上,看看运行结果是否与我 们仿真的一致。运行结果如图 6.4.4 所示: 图 6.4.4 执行结果 至此,我们的第一章的学习就结束了,本章作为 STM32 的入门第一个例子,详细介绍了 STM32 的 IO 口操作,同时巩固了前面的学习,并进一步介绍了 MDK 的软件仿真功能。希望 大家好好理解一下。 www.openedv.com 146 ALIENTEK 战舰STM32开发板 第七章 蜂鸣器实验 上一章,我们介绍了 STM32 的 IO 口作为输出的使用,这一章,我们将通过另外一个例子 讲述 STM32 的 IO 口作为输出的使用。在本章中,我们将利用一个 IO 口来控制板载的有源蜂 鸣器,实现蜂鸣器控制。通过本章的学习,你将进一步了解 STM32 的 IO 口作为输出口使用的 方法。本章分为如下几个小节: 7.1 蜂鸣器简介 7.2 硬件设计 7.3 软件设计 7.4 仿真与下载 www.openedv.com 147 ALIENTEK 战舰STM32开发板 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 所示: www.openedv.com 148 ALIENTEK 战舰STM32开发板 图 7.2.1 蜂鸣器与 STM32 连接原理图 图中我们用到一个 NPN 三极管(S8050)来驱动蜂鸣器,R60 主要用于防止蜂鸣器的误发 声。当 PB.8 输出高电平的时候,蜂鸣器将发声,当 PB.8 输出低电平的时候,蜂鸣器停止发声。 7.3 软件设计 大家可以直接打开本实验工程。也可以按下面的步骤在实验 1 的基础上新建蜂鸣器实验工 程。复制上一章的 LED 实验工程,然后打开 USER 目录,把目录下面工程 LED.Uv2 重命名为 BEEP.Uv2。,然后在 HARDWARE 文件夹下新建一个 BEEP 文件夹,用来存放与蜂鸣器相关的 代码。如图 7.3.1 所示: 图 7.3.1 在 HARDWARE 下新增 BEEP 文件夹 然后我们打开 USER 文件夹下的 BEEP.Uv2 工程,按 按钮新建一个文件,然后保存在 HARDWAREBEEP 文件夹下面,保存为 beep.c。在该文件中输入如下代码: #include "beep.h" www.openedv.com 149 ALIENTEK 战舰STM32开发板 //初始化 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; //速度为 50MHz GPIO_Init(GPIOB, &GPIO_InitStructure); //根据参数初始化 GPIOB.8 GPIO_ResetBits(GPIOB,GPIO_Pin_8); //输出 0,关闭蜂鸣器输出 } 这段代码 仅包含 1 个函数:void BEEP_Init(void),该函数的作用就是使能 PORTB 的时钟, 同时配置 PB8 为推挽输出。这里的初始化内容跟实验 6 跑马灯实验几乎是一样的,这里就不做 深入的讲解。 保存 beep.c 代码,然后我们按同样的方法,新建一个 beep.h 文件,也保存在 BEEP 文件夹 下面。在 beep.h 中输入如下代码: #ifndef __BEEP_H #define __BEEP_H #include "sys.h" //蜂鸣器端口定义 #define BEEP PBout(8) // BEEP,蜂鸣器接口 void BEEP_Init(void); //初始化 #endif 和上一章一样,我们这里还是通过位带操作来实现某个 IO 口的输出控制,BEEP 就直接代 表了 PB8 的输出状态。我们只需要令 BEEP=1,就可以让蜂鸣器发声。 将 beep.h 也保存。接着,我们把 beep.c 加入到 HARDWARE 这个组里面,这一次我们通过 双击的方式来增加新的.c 文件,双击 HARDWARE,找到 beep.c,加入到 HARDWARE 里面, 如图 7.3.2 所示: www.openedv.com 150 ALIENTEK 战舰STM32开发板 图 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" //ALIENTEK 战舰 STM32 开发板实验 2 //蜂鸣器实验 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); } www.openedv.com 151 ALIENTEK 战舰STM32开发板 } 注意要将 BEEP 文件夹加入头文件包含路径,不能少,否则编译的时候会报错。这段代码 就实现前面 7.1 节所阐述的功能,同时加入了 DS0(LED0)的闪烁来提示程序运行(后面的代 码,我们基本都会加入这个),整个代码比较简单。 然后按 ,编译工程,得到结果如图 7.3.3 所示: 图 7.3.3 编译结果 可以看到没有错误,也没有警告。从编译信息可以看出,我们的代码占用 FLASH 大小为: 4864 字节(4528+336),所用的 SRAM 大小为:1888 个字节(52+1836)。 这里我们解释一下,编译结果里面的几个数据的意义: 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 开发板看看实际运行的结果。 7.4 仿真与下载 我们可以先用软件仿真,看看结果对不对,根据软件仿真的结果,然后再下载到战舰 STM32 开发板上面看运行是否正确。 首先,我们进行软件仿真。先按 开始仿真,接着按 ,显示逻辑分析窗口,点击 Setup, 新建 2 个信号:PORTB.5 和 PORTB.8,如图 7.4.1 所示: www.openedv.com 152 ALIENTEK 战舰STM32开发板 图 7.4.1 新建仿真信号 接着,点击 ,开始运行。运行一段时间之后,按 可以看到如图 7.4.2 所示的波形: 按钮,暂停仿真回到逻辑分析窗口, 图 7.4.2 仿真波形 从图中我们可以看出,PB.5 和 PB.8 同时输出高和低电平,和我们预计的效果是一样的, 周期是 0.6s,符合预期设计。接下来可以把代码下载到战舰 STM32 开发板上看看运行结果是否 正确。 下载完代码,可以看到 DS0 亮的时候蜂鸣器不叫,而 DS0 灭的时候,蜂鸣器叫(因为他 们的有效信号相反)。间隔为 0.3 秒左右,符合预期设计。 至此,我们的本章的学习就结束了。本章,作为 STM32 的入门第二个例子,进一步介绍 了 STM32 的 IO 作为输出口的使用方法,同时巩固了前面的学习。希望大家在开发板上实际验 证一下,从而加深印象。 www.openedv.com 153 ALIENTEK 战舰STM32开发板 第八章 按键输入实验 上两章,我们介绍了 STM32 的 IO 口作为输出的使用,这一章,我们将向大家介绍如何使 用 STM32 的 IO 口作为输入用。在本章中,我们将利用板载的 4 个按键,来控制板载的两个 LED 的亮灭。通过本章的学习,你将了解到 STM32 的 IO 口作为输入口的使用方法。本章分为如下 几个小节: 8.1 STM32 IO 口简介 8.2 硬件设计 8.3 软件设计 8.4 仿真与下载 www.openedv.com 154 ALIENTEK 战舰STM32开发板 8.1 STM32 IO 口简介 STM32 的 IO 口在上一章已经有了比较详细的介绍,这里我们不再多说。STM32 的 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 2) 蜂鸣器 3) 4 个按键:KEY0、KEY1、KEY2、和 KEY_UP。 DS0、蜂鸣器和 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; www.openedv.com 155 ALIENTEK 战舰STM32开发板 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA| RCC_APB2Periph_GPIOE,ENABLE); //使能 PORTA,PORTE 时钟 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 KEY_RIGHT; else if(KEY1==0)return KEY_DOWN; else if(KEY2==0)return KEY_LEFT; else if(KEY3==1)return KEY_UP; }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 的情况下,这个大家 www.openedv.com 156 ALIENTEK 战舰STM32开发板 要留意下。同时还有一点要注意的就是,该函数的按键扫描是有优先级的,最优先的是 KEY0, 第二优先的是 KEY1,接着 KEY2,最后是 KEY3(KEY3 对应 WK_UP 按键)。该函数有返回 值,如果有按键按下,则返回非 0 值,如果没有或者按键不正确,则返回 0。 接下来我们看看头文件 key.h 里面的代码: #ifndef __KEY_H #define __KEY_H #include "sys.h" #define KEY0 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_4) #define KEY1 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3) #define KEY2 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_2) #define KEY3 GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0) //读取按键 0 //读取按键 1 //读取按键 2 //读取按键 3(WK_UP) #define KEY_UP 4 #define KEY_LEFT 3 #define KEY_DOWN 2 #define KEY_RIGHT 1 void KEY_Init(void); u8 KEY_Scan(u8); //IO 初始化 //按键扫描函数 #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 KEY3 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 KEY3 PAin(0) //PA0 WK_UP 用库函数实现的好处是在各个 STM32 芯片上面的移植性非常好,不需要修改任何代码。 用位带操作的好处是简洁,至于使用哪种方法,看各位的爱好了。 在 key.h 中,我们还定义了 KEY_UP/KEY_DOWN /KEY_LEFT /KEY_RIGHT 等 4 个宏定义, 分别对应开发板上下左右(WK_UP/KEY1/KEY2/KEY0)按键按下时 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) www.openedv.com 157 ALIENTEK 战舰STM32开发板 { u8 t; delay_init(); LED_Init(); //延时函数初始化 //LED 端口初始化 KEY_Init(); BEEP_Init(); LED0=0; //初始化与按键连接的硬件接口 //初始化蜂鸣器端口 //先点亮红灯 while(1) { t=KEY_Scan(0); //得到键值 if(t) { switch(t) { case KEY_UP: //控制蜂鸣器 BEEP=!BEEP;break; case KEY_LEFT: //控制 LED0 翻转 LED0=!LED0;break; case KEY_DOWN: //控制 LED1 翻转 LED1=!LED1;break; case KEY_RIGHT: //同时控制 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 所示: www.openedv.com 158 ALIENTEK 战舰STM32开发板 图 8.4.1 新建仿真信号 然后再点击 PeripheralsGeneral Purpose I/OGPIOE,弹出 GPIOE 的查看窗口,如图 8.4.2 所示: 图 8.4.2 查看 GPIOE 寄存器 www.openedv.com 159 ALIENTEK 战舰STM32开发板 然后在 t=KEY_Scan();这里设置一个断点,按 直接执行到这里,然后在 General Purpose I/O E 窗口内的 Pins 里面勾选 2、3、4 位,这是虽然我们已经设置了这几个 IO 口为上拉输入,但 是 MDK 不会考虑 STM32 自带的上拉和下拉,所以我们得自己手动设置一下,来使得其初始状 态和外部硬件的状态一摸一样。如图 8.4.3 所示: 图 8.4.3 执行到断点处 本来我们还需要设置 PORTA.0 的,但是 GPIOA.0 是高电平有效,刚好默认的就满足要求, 不需要再去勾选 PORTA.0 了。所以这里我们可以省略一个 GPIOA.0 的设置。接着我们执行过 这句,可以看到 t 的值依旧为 0,也就是没有任何按键按下。接着我们再按 ,再次执行到 t=KEY_Scan();我们此次把 Pins 的 PE2 取消勾选,再次执行过这句,得到 t 的值为 3,如图 8.4.4 所示: www.openedv.com 160 ALIENTEK 战舰STM32开发板 图 8.4.4 按键扫描结果 然后按相似的方法,分别取消勾选 PE3 和 PE4,以及勾选 PA0,然后再把它们还原,可以 看到逻辑分析窗口的波形如图 8.4.5 所示: 图 8.4.5 仿真波形 从图 8.4.5 可以看出,当 PE2 按下的时候 PB5 翻转,PE3 按下的时候 PE5 翻转,PE4 按下 www.openedv.com 161 ALIENTEK 战舰STM32开发板 的时候 PB5 和 PE5 一起翻转,PA0 按下的时候 PB8 翻转,使我们想要得到的结果。因此,可 以确定软件仿真基本没有问题了。接下来可以把代码下载到战 STM32 开发板上看看运行结果 是否正确。 在下载完之后,我们可以按 KEY0、KEY1、KEY2 和 WK_UP 来看看 DS0 和 DS1 以及蜂 鸣器的变化,是否和我们仿真的结果一致(结果肯定是一致的)。 至此,我们的本章的学习就结束了。本章,作为 STM32 的入门第三个例子,介绍了 STM32 的 IO 作为输入的使用方法,同时巩固了前面的学习。希望大家在开发板上实际验证一下,从 而加深印象。 www.openedv.com 162 ALIENTEK 战舰STM32开发板 第九章 串口实验 前面两章介绍了 STM32 的 IO 口操作。这一章我们将学习 STM32 的串口,教大家如何使 用 STM32 的串口来发送和接收数据。本章将实现如下功能:STM32 通过串口和上位机的对话, STM32 在收到上位机发过来的字符串后,原原本本的返回给上位机。本章分为如下几个小节: 9.1 STM32 串口简介 9.2 硬件设计 9.3 软件设计 9.4 下载验证 www.openedv.com 163 ALIENTEK 战舰STM32开发板 9.1 STM32 串口简介 串口作为 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; //一般设置为 9600; www.openedv.com 164 ALIENTEK 战舰STM32开发板 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); //初始化串口 从上面的初始化格式可以看出初始化需要设置的参数为:波特率,字长,停止位,奇偶校验位, 硬件数据流控制,模式(收,发)。我们可以根据需要设置这些参数。 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); www.openedv.com 165 ALIENTEK 战舰STM32开发板 这些标识号在 MDK 里面是通过宏定义定义的: #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 所示: www.openedv.com 166 ALIENTEK 战舰STM32开发板 图 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); //复位串口 1 //③GPIO 端口模式设置 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; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); //④串口参数初始化 USART_InitStructure.USART_BaudRate = bound; //USART1_RX PA.10 //浮空输入 //初始化 GPIOA.10 //波特率设置 www.openedv.com 167 ALIENTEK 战舰STM32开发板 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); //初始化串口 #if EN_USART1_RX //如果使能了接收 //⑤初始化 NVIC NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ; //抢占优先级 3 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //子优先级 3 //IRQ 通道使能 NVIC_Init(&NVIC_InitStructure); //⑤开启中断 //中断优先级初始化 USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //开启中断 #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 的 www.openedv.com 168 ALIENTEK 战舰STM32开发板 NVIC 中断。这里我们把串口 1 中断放在组 2,优先级设置为组 2 里面的最低。 接下来,根据之前讲解的步骤 7,还要编写中断服务函数。串口 1 的中断服务函数 USART1_IRQHandler,在 5.3.3 已经有详细介绍了,这里我们就不再介绍了,大家可以翻过去 看看。 介绍完了这两个函数,我们回到 main.c,在 main.c 里面编写如下代码: #include "led.h" #include "delay.h" #include "key.h" #include "sys.h" #include "usart.h" int main(void) { u8 t; u8 len; u16 times=0; delay_init(); //延时函数初始化 NVIC_Configuration(); //设置 NVIC 中断分组 2 uart_init(9600); //串口初始化波特率为 9600 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 所 示: www.openedv.com 186 ALIENTEK 战舰STM32开发板 表 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 的时候,如果该位设置,并开启了中断,则会产生中断,我们可以在中 www.openedv.com 187 ALIENTEK 战舰STM32开发板 断里面向 WWDG_CR 重新写入计数器的值,来达到喂狗的目的。注意这里在进入中断后,必 须在不大于 1 个窗口看门狗计数周期的时间(在 PCLK1 频率为 36M 且 WDGTB 为 0 的条件下, 该时间为 113us)内重新写 WWDG_CR,否则,看门狗将产生复位! 最后我们要介绍的是状态寄存器(WWDG_SR),该寄存器用来记录当前是否有提前唤醒 的标志。该寄存器仅有位 0 有效,其他都是保留位。当计数器值达到 40h 时,此位由硬件置 1。 它必须通过软件写 0 来清除。对此位写 1 无效。即使中断未被使能,在计数器的值达到 0X40 的时候,此位也会被置 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); 这个函数就一个入口参数为窗口值,很容易理解。 设置分频数的函数是: 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 的内部资源,只需要软件设置好即可 www.openedv.com 188 ALIENTEK 战舰STM32开发板 正常工作。我们通过 DS0 和 DS1 来指示 STM32 的复位情况和窗口看门狗的喂狗情况。 12.3 软件设计 打开我们的窗口看门狗实验可以看到,相对于独立看门狗,我们只增加了窗口看门狗相关 的库函数支持文件 stm32f10x_wwdg.c/stm32f10x_wwdg./c,然后在 wdg.c 加入如下代码(之前代 码保留): //保存 WWDG 计数器的设置值,默认为最大. 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_Init(&NVIC_InitStructure); //NVIC 初始化 } void WWDG_IRQHandler(void) { WWDG_SetCounter(WWDG_CNT); //当禁掉此句后,窗口看门狗将产生复位 www.openedv.com 189 ALIENTEK 战舰STM32开发板 WWDG_ClearFlag(); LED1=!LED1; //清除提前唤醒中断标志位 //LED 状态翻转 } 新增的这四个函数都比较简单,第一个函数 void WWDG_Init(u8 tr,u8 wr,u8 fprer)用来 设置 WWDG 的初始化值。包括看门狗计数器的值和看门狗比较值等。该函数就是按照我们上 面的 4 个思路设计出来的代码。注意到这里有个全局变量 WWDG_CNT,该变量用来保存最初 设置 WWDG_CR 计数器的值。在后续的中断服务函数里面,就又把该数值放回到 WWDG_CR 上。 WWDG_Set_Counter()函数比较简单,就是用来重设窗口看门狗的计数器值的。该函数很简 单,我们就不多说了。 然后是中断分组函数,这个函数非常简单,之前有讲解,这里不重复。 最后中断服务函数里面,先重设窗口看门狗的计数器值,然后清除提前唤醒中断标志。最 后对 LED0(DS0)取反,来监测中断服务函数的执行了状况。我们再把这几个函数名加入到头 函数里面去,以方便其他文件调用。 在完成了以上部分之后,我们就回到主函数,代码如下: int main(void) { delay_init(); NVIC_Configuration(); uart_init(9600); LED_Init(); KEY_Init(); //延时函数初始化 //设置 NVIC 中断分组 2 //串口初始化波特率为 9600 //LED 初始化 //按键初始化 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 次左右,和我们预期的一致,说明我们的实验是成功的。 www.openedv.com 190 ALIENTEK 战舰STM32开发板 第十三章 定时器中断实验 这一章,我们将向大家介绍如何使用 STM32 的通用定时器,STM32 的定时器功能十分强 大,有 TIME1 和 TIME8 等高级定时器,也有 TIME2~TIME5 等通用定时器,还有 TIME6 和 TIME7 等基本定时器。在《STM32 参考手册》里面,定时器的介绍占了 1/5 的篇幅,足见其重 要性。在本章中,我们将利用 TIM3 的定时器中断来控制 DS1 的翻转,在主函数用 DS0 的翻转 来提示程序正在运行。本章,我们选择难度适中的通用定时器来介绍,本章将分为如下几个部 分: 13.1 STM32 通用定时器简介 13.2 硬件设计 13.3 软件设计 13.4 下载验证 www.openedv.com 191 ALIENTEK 战舰STM32开发板 13.1 STM32 通用定时器简介 STM32 的通用定时器是一个通过可编程预分频器(PSC)驱动的 16 位自动装载计数器 (CNT)构成。STM32 的通用定时器可以被用于:测量输入信号的脉冲长度(输入捕获)或者产 生输出波形(输出比较和 PWM)等。 使用定时器预分频器和 RCC 时钟控制器预分频器,脉冲长 度和波形周期可以在几个微秒到几个毫秒间调整。STM32 的每个通用定时器都是完全独立的, 没有互相共享的任何资源。 STM3 的通用 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 所示: www.openedv.com 192 ALIENTEK 战舰STM32开发板 www.openedv.com 193 ALIENTEK 战舰STM32开发板 图 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 所示: www.openedv.com 194 ALIENTEK 战舰STM32开发板 图 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 是用来设置分频系数的,刚才上面有讲解。 www.openedv.com 195 ALIENTEK 战舰STM32开发板 第二个参数 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){} 固件库中清除中断标志位的函数是: www.openedv.com 196 ALIENTEK 战舰STM32开发板 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; //设置时钟分割 www.openedv.com 197 ALIENTEK 战舰STM32开发板 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM 向上计数 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_Configuration(); //设置 NVIC 中断分组 2:2 位抢占优先级,2 位响应优先级 www.openedv.com 198 ALIENTEK 战舰STM32开发板 uart_init(9600); LED_Init(); //串口初始化波特率为 9600 //LED 端口初始化 TIM3_Int_Init(4999,7199); //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 一次)。 www.openedv.com 199 ALIENTEK 战舰STM32开发板 第十四章 PWM 输出实验 上一章,我们介绍了 STM32 的通用定时器 TIM3,用该定时器的中断来控制 DS1 的闪烁, 这一章,我们将向大家介绍如何使用 STM32 的 TIM3 来产生 PWM 输出。在本章中,我们将利 用 TIM3 的通道 2,把通道 2 重映射到 PB5,产生 PWM 来控制 DS0 的亮度。本章分为如下几 个部分: 14.1 PWM 简介 14.2 硬件设计 14.3 软件设计 14.4 下载验证 www.openedv.com 200 ALIENTEK 战舰STM32开发板 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_CCMR1 控制 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 所示: www.openedv.com 201 ALIENTEK 战舰STM32开发板 图 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 的亮度。下面我们介绍通过库函数来配置该 功能的步骤。 www.openedv.com 202 ALIENTEK 战舰STM32开发板 首先要提到的是,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; www.openedv.com 203 ALIENTEK 战舰STM32开发板 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 里面加入了 如下代码: www.openedv.com 204 ALIENTEK 战舰STM32开发板 //TIM3 PWM 部分初始化 //PWM 输出初始化 //arr:自动重装值 //psc:时钟预分频数 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 的声明,这个就没什么需要讲解 www.openedv.com 205 ALIENTEK 战舰STM32开发板 咯。 接下来,我们修改主程序里面的 main 函数如下: int main(void) { u16 led0pwmval=0; u8 dir=1; delay_init(); //延时函数初始化 NVIC_Configuration(); //设置 NVIC 中断分组 2:2 位抢占优先级,2 位响应优先级 uart_init(9600); //串口初始化波特率为 9600 LED_Init(); //LED 端口初始化 TIM3_PWM_Init(899,0); //不分频,PWM 频率=72000/900=8Khz 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 所示: www.openedv.com 206 ALIENTEK 战舰STM32开发板 图 14.4.1 PWM 控制 DS0 亮度 www.openedv.com 207 ALIENTEK 战舰STM32开发板 第十五章 输入捕获实验 上一章,我们介绍了 STM32 的通用定时器作为 PWM 输出的使用方法,这一章,我们将向 大家介绍通用定时器作为输入捕获的使用。在本章中,我们将用 TIM5 的通道 1(PA0)来做输 入捕获,捕获 PA0 上高电平的脉宽(用 WK_UP 按键输入高电平),通过串口打印高电平脉宽 时间,从本章分为如下几个部分: 15.1 输入捕获简介 15.2 硬件设计 15.3 软件设计 15.4 下载验证 www.openedv.com 208 ALIENTEK 战舰STM32开发板 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 所示: www.openedv.com 209 ALIENTEK 战舰STM32开发板 图 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 就是了。 输入捕获 1 滤波器 IC1F[3:0],这个用来设置输入采样频率和数字滤波器长度。其中, 是定时器的输入频率(TIMxCLK),一般为 72Mhz,而 则是根据 TIMx_CR1 的 CKD[1:0] 的设置来确定的,如果 CKD[1:0]设置为 00,那么 = 。N 值就是滤波长度,举个简 单的例子:假设 IC1F[3:0]=0011,并设置 IC1 映射到通道 1 上,且为上升沿触发,那么在捕获 到上升沿的时候,再以 的频率,连续采样到 8 次通道 1 的电平,如果都是高电平,则说 明却是一个有效的触发,就会触发输入捕获中断(如果开启了的话)。这样可以滤除那些高电平 脉宽低于 8 个采样周期的脉冲信号,从而达到滤波的效果。这里,我们不做滤波处理,所以设 置 IC1F[3:0]=0000,只要采集到上升沿,就触发捕获。 www.openedv.com 210 ALIENTEK 战舰STM32开发板 再来看看捕获/比较使能寄存器: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; TIM_TimeBaseStructure.TIM_Period = arr; //设定计数器自动重装值 www.openedv.com 211 ALIENTEK 战舰STM32开发板 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 寄存器) 因为我们要捕获的是高电平信号的脉宽,所以,第一次捕获是上升沿,第二次捕获时下降 沿,必须在捕获上升沿之后,设置捕获边沿为下降沿,同时,如果脉宽比较长,那么定时器就 会溢出,对溢出必须做处理,否则结果就不准了。这两件事,我们都在中断里面做,所以必须 开启捕获中断和更新中断。 www.openedv.com 212 ALIENTEK 战舰STM32开发板 这里我们使用定时器的开中断函数 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 时钟 www.openedv.com 213 ALIENTEK 战舰STM32开发板 //初始化 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) { www.openedv.com 214 ALIENTEK 战舰STM32开发板 if(TIM5CH1_CAPTURE_STA&0X40) //已经捕获到高电平了 { 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 所 示: bit7 捕获完成标志 TIM5CH1_CAPTURE_STA bit6 bit5~0 捕获到高电平标志 捕获高电平后定时器溢出的次数 表 15.3.1 TIM5CH1_CAPTURE_STA 各位描述 另外一个变量 TIM5CH1_CAPTURE_VAL,则用来记录捕获到下降沿的时候,TIM5_CNT 的值。 现在我们来介绍一下,捕获高电平脉宽的思路:首先,设置 TIM5_CH1 捕获上升沿,这在 www.openedv.com 215 ALIENTEK 战舰STM32开发板 TIM5_Cap_Init 函数执行的时候就设置好了,然后等待上升沿中断到来,当捕获到上升沿中断, 此时如果 TIM5CH1_CAPTURE_STA 的第 6 位为 0,则表示还没有捕获到新的上升沿,就先把 TIM5CH1_CAPTURE_STA、TIM5CH1_CAPTURE_VAL 和 TIM5->CNT 等清零,然后再设置 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 比较简单,这里我们就不多说了。 接下来,我们修改主程序里面的 main 函数如下: extern u8 TIM5CH1_CAPTURE_STA; //输入捕获状态 extern u16 TIM5CH1_CAPTURE_VAL; //输入捕获值 int main(void) { u32 temp=0; delay_init(); //延时函数初始化 NVIC_Configuration(); //设置 NVIC 中断分组 2 uart_init(9600); //串口初始化波特率为 9600 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; www.openedv.com 216 ALIENTEK 战舰STM32开发板 temp*=65536;//溢出时间总和 temp+=TIM5CH1_CAPTURE_VAL;//得到总的高电平时间 printf("HIGH:%d us\r\n",temp); TIM5CH1_CAPTURE_STA=0; //打印总的高点平时间 //开启下一次捕获 } } } 该 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 输出的高电平是如何变化 www.openedv.com 217 ALIENTEK 战舰STM32开发板 的。 www.openedv.com 218 ALIENTEK 战舰STM32开发板 第十六章 电容触摸按键实验 上一章,我们介绍了 STM32 的输入捕获功能及其使用。这一章,我们将向大家介绍如何 通过输入捕获功能,来做一个电容触摸按键。在本章中,我们将用 TIM5 的通道 2(PA1)来做 输入捕获,并实现一个简单的电容触摸按键,通过该按键控制 DS1 的亮灭。从本章分为如下几 个部分: 16.1 电容触摸按键简介 16.2 硬件设计 16.3 软件设计 16.4 下载验证 www.openedv.com 219 ALIENTEK 战舰STM32开发板 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 的 www.openedv.com 220 ALIENTEK 战舰STM32开发板 对比,来判断是不是有触摸发生。 关于输入捕获的配置,在上一章我们已经有详细介绍了,这里我们就不再介绍。至此,电 容触摸按键的原理介绍完毕。 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() { u16 buf[10]; u16 temp; u8 j,i; //以 1Mhz 的频率计数 www.openedv.com 221 ALIENTEK 战舰STM32开发板 TIM5_CH2_Cap_Init(TPAD_ARR_MAX_VAL,SystemCoreClock/1000000-1); for(i=0;i<10;i++) //连续读取 10 次 { buf[i]=TPAD_Get_Val(); 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); //初始化遇到超过 TPAD_ARR_MAX_VAL/2 的数值,不正常! if(tpad_default_val>TPAD_ARR_MAX_VAL/2)return 1; 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); //初始化 GPIOA.1 //PA.1 输出 0,放电 delay_ms(5); TIM_SetCounter(TIM5,0); TIM_ClearITPendingBit(TIM5, TIM_IT_CC2|TIM_IT_Update); GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING; //延时 5ms //归 0 //清除中断标志 //GPIOA.1 浮空输入 GPIO_Init(GPIOA, &GPIO_InitStructure); } //得到定时器捕获值 //如果超时,则直接返回定时器的计数值. u16 TPAD_Get_Val(void) { www.openedv.com 222 ALIENTEK 战舰STM32开发板 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 80 //触摸的门限值,也就是必须大于 //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; //支持连按的时候,设置采样次数为 6 次 keyen=0; //支持连按 } rval=TPAD_Get_MaxVal(sample); if(rval>(tpad_default_val+TPAD_GATE_VAL))//大于 //tpad_default_val+TPAD_GATE_VAL,有效 { rval=TPAD_Get_MaxVal(sample); if((keyen==0)&&(rval>(tpad_default_val+TPAD_GATE_VAL)))//大于 //tpad_default_val+TPAD_GATE_VAL,有效 www.openedv.com 223 ALIENTEK 战舰STM32开发板 { res=1; } //printf("r:%d\r\n",rval); keyen=5; //至少要再过 5 次之后才能按键有效 }else if(keyen>2)keyen=2; //如果检测到按键松开,则直接将次数将为 2,以提高响应速度 if(keyen)keyen--; 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 函数,该函数和上一章的输入捕获函数基本一样,不同的是, www.openedv.com 224 ALIENTEK 战舰STM32开发板 这里我们设置的是 CH2 通道,并开启了输入滤波器。通过该函数的设置,我们将可以捕获 PA1 上的上升沿。关于配置的详细介绍大家可以看第 15 章输入捕获实验讲解。 我们再来看看 TPAD_Get_Val 函数,该函数用于得到定时器的一次捕获值。该函数先调用 TPAD_Reset,将电容放电,同时设置 TIM5_CNT 寄存器为 0,然后死循环等待发生上升沿捕获 (或计数溢出),将捕获到的值(或溢出值)作为返回值返回。 接着我们介绍 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_Configuration(); uart_init(9600); LED_Init(); TPAD_Init(); //设置 NVIC 中断分组 2:2 位抢占优先级,2 位响应优先级 //串口初始化波特率为 9600 //LED 端口初始化 //初始化触摸按键 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()函数执行之后,就开始触摸按键的扫描,当有触摸的 www.openedv.com 225 ALIENTEK 战舰STM32开发板 时候,对 DS1 取反,而 DS0 则有规律的间隔取反,提示程序正在运行。 这里还要提醒一下大家,不要把 uart_init(9600);去掉,因为在 TPAD_Init 函数里面,我们有 用到 printf,如果你去掉了 uart_init,就会导致 printf 无法执行,从而死机。 至此,我们的软件设计就完成了。 16.4 下载验证 在完成软件设计之后,将我们将编译好的文件下载到战舰 STM32 开发板上,可以看到 DS0 慢速闪烁,此时,我们用手指触摸 ALIENTEK 战舰 STM32 开发板上的 TPAD(右下角的 LOGO 标志),就可以控制 DS1 的亮灭了。不过你要确保 TPAD 和 ADC 的跳线帽连接上了哦!如图 16.4.1 所示: 图 16.4.1 触摸区域和跳线帽短接方式 同时大家可以打开串口调试助手,每次复位的时候,会收到 tpad_default_val 的值,一般为 70 左右,根据 16.1 节提到的公式,我们可以计算出 Cs 的容值为 27pF 左右。 www.openedv.com 226 ALIENTEK 战舰STM32开发板 第十七章 OLED 显示实验 前面几章的实例,均没涉及到液晶显示,这一章,我们将向大家介绍 OLED 的使用。在本 章中,我们将利用战舰 STM32 开发板上的 OLED 模块接口(与摄像头共用的这个),来点亮 OLED,并实现 ASCII 字符的显示。本章分为如下几个部分: 17.1 OLED 简介 17.2 硬件设计 17.3 软件设计 17.4 下载验证 www.openedv.com 227 ALIENTEK 战舰STM32开发板 17.1 OLED 简介 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 所示: 表 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 所示: www.openedv.com 228 ALIENTEK 战舰STM32开发板 图 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])上; www.openedv.com 229 ALIENTEK 战舰STM32开发板 在 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 所示: www.openedv.com 230 ALIENTEK 战舰STM32开发板 图 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 所示: www.openedv.com 231 ALIENTEK 战舰STM32开发板 表 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 的页地址。 www.openedv.com 232 ALIENTEK 战舰STM32开发板 第五个指令为 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 模块来显示字符和数字了,在后面我们 还将会给大家介绍显示汉字的方法。这一部分就先介绍到这里。 www.openedv.com 233 ALIENTEK 战舰STM32开发板 17.2 硬件设计 本实验用到的硬件资源有: 1) 指示灯 DS0 2) OLED 模块 OLED 模块的电路在 17.1 节已有详细说明了,这里我们介绍 OLED 模块与 ALIETEK 战舰 STM32 开发板的连接,战舰 STM32 开发板有两个地方可以接 OLED 模块,第一个是左下角的 摄像头模块/OLED 模块共用接口,第二个是 LCD 模块和 OLED 模块的共用接口,不论哪个共 用接口,OLED 都是靠左插的。这里我们选择摄像头模块/OLED 模块共用接口来接 OLED 模块, OLED 模块同战舰 STM32 开发板的连接图如图 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 所示: www.openedv.com 234 ALIENTEK 战舰STM32开发板 图 17.2.2 OLED 模块与开发板连接实物图 17.3 软件设计 我们直接打开 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; //PD3,PD6 推挽输出 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOD, &GPIO_InitStructure); //推挽输出 //速度 50MHz //初始化 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 推挽输出 www.openedv.com 235 ALIENTEK 战舰STM32开发板 GPIO_Init(GPIOC, &GPIO_InitStructure); GPIO_SetBits(GPIOC,GPIO_Pin_0|GPIO_Pin_1); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15; GPIO_Init(GPIOG, &GPIO_InitStructure); GPIO_SetBits(GPIOG,GPIO_Pin_15); #endif OLED_RST=0; delay_ms(100); OLED_RST=1; //PC0,1 OUT 输出高 //PG15 OUT 推挽输出 RST //PG15 OUT 输出高 OLED_WR_Byte(0xAE,OLED_CMD); //关闭显示 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 上。该函数代码如下: www.openedv.com 236 ALIENTEK 战舰STM32开发板 //更新显存到 LCD void OLED_Refresh_Gram(void) { u8 i,n; for(i=0;i<8;i++) { OLED_WR_Byte (0xb0+i,OLED_CMD); //设置页地址(0~7) OLED_WR_Byte (0x00,OLED_CMD); //设置显示位置—列低地址 OLED_WR_Byte (0x10,OLED_CMD); //设置显示位置—列高地址 for(n=0;n<128;n++)OLED_WR_Byte(OLED_GRAM[n][i],OLED_DATA); } } 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 www.openedv.com 237 ALIENTEK 战舰STM32开发板 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; } 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 模块上去的。要显示字符,我们先要 www.openedv.com 238 ALIENTEK 战舰STM32开发板 有字符的点阵数据,ASCII 常用的字符集总共有 95 个,从空格符开始,分别为: !"#$%&'()*+, -0123456789 : ;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxy z{|}~. 我们先要得到这个字符集的点阵数据,这里我们介绍一个款很好的字符提取软件: PCtoLCD2002 完美版。该软件可以提供各种字符,包括汉字(字体和大小都可以自己设置)阵 提取,且取模方式可以设置好几种,常用的取模方式,该软件都支持。该软件还支持图形模式, 也就是用户可以自己定义图片的大小,然后画图,根据所画的图形再生成点阵数据,这功能在 制作图标或图片的时候很有用。 该软件的界面如图 17.3.1 所示: 图 17.3.1 PCtoLCD2002 软件界面 然后我们选择设置,在设置里面设置取模方式如图 17.3.2 所示: 图 17.3.2 设置取模方式 上图设置的取模方式,在右上角的取模说明里面有,即:从第一列开始向下每取 8 个点作 www.openedv.com 239 ALIENTEK 战舰STM32开发板 为一个字节,如果最后不足 8 个点就补满 8 位。取模顺序是从高到低,即第一个点作为最高位。 如*-------取为 10000000。其实就是按如图 17.3.3 所示的这种方式: 图 17.3.3 取模方式图解 从上到下,从左到右,高位在前。我们按这样的取模方式,然后把 ASCII 字符集按 12*6 大小和 16*0 大小取模出来(对应汉字大小为 12*12 和 16*16,字符的只有汉字的一半大!),保 存在 oledfont.h 里面,每个 12*6 的字符占用 12 个字节,每个 16*8 的字符占用 16 个字节。具 体见 oledfont.h 部分代码(该部分我们不再这里列出来了,请大家参考光盘里面的代码)。 在知道了取模方式之后,我们就可以根据取模的方式来编写显示字符的代码了,这里我们 针对以上取模方式的显示字符代码如下: void OLED_ShowChar(u8 x,u8 y,u8 chr,u8 size,u8 mode) { u8 temp,t,t1; u8 y0=y; chr=chr-' '; //得到偏移后的值 for(t=0;t'~')t=' '; OLED_ShowNum(103,48,t,3,16);//显示 ASCII 字符的码值 delay_ms(300); LED0=!LED0; } } 该部分代码用于在 OLED 上显示一些字符,然后从空格键开始不停的循环显示 ASCII 字符 集,并显示该字符的 ASCII 值。注意在 main.c 文件里面包含 oled.h 头文件,同时把 oled.c 文件 加入到 HARDWARE 组下,然后我们编译此工程,直到编译成功为止。 17.4 下载验证 将代码下载到战舰 STM32 后,可以看到 DS0 不停的闪烁,提示程序已经在运行了。同时 可以看到 OLED 模块显示如图 17.4.1 所示: www.openedv.com 242 ALIENTEK 战舰STM32开发板 图 17.4.1 OLED 显示效果 最后一行不停的显示 ASCII 字符以及其码值。通过这一章的学习,我们学会了 ALIENTEK OLED 模块的使用,在调试代码的时候,又多了一种显示信息的途径,在以后的程序编写中, 大家可以好好利用。 www.openedv.com 243 ALIENTEK 战舰STM32开发板 第十八章 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 下载验证 www.openedv.com 244 ALIENTEK 战舰STM32开发板 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’3 种大小的屏幕可选。 2,320×240 的分辨率(3.5’分辨率为:320*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 所示: www.openedv.com 245 ALIENTEK 战舰STM32开发板 图 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 位双向数据线。 www.openedv.com 246 ALIENTEK 战舰STM32开发板 RST:硬复位 TFTLCD。 RS:命令/数据标志(0,读写命令;1,读写数据)。 80 并口在上一节我们已经有详细的介绍了,这里我们就不再介绍,需要说明的是,TFTLCD 模块的 RST 信号线是直接接到 STM32 的复位脚上,并不由软件控制,这样可以省下来一个 IO 口。另外我们还需要一个背光控制线来控制 TFTLCD 的背光。所以,我们总共需要的 IO 口数 目为 21 个。这里还需要注意,我们标注的 DB1~DB8,DB10~DB17,是相对于 LCD 控制 IC 标 注的,实际上大家可以把他们就等同于 D0~D15,这样理解起来就比较简单一点。 ALIENTEK 提供的 2.8 寸 TFTLCD 模块,其驱动芯片有很多种类型,比如有:ILI9320/ILI9325 /ILI9328/ILI9341/SSD1289/LGDP4531/LGDP4535/R61505/ SPFD5408/ RM68021 等(具体的型号, 大家可以通过下载本章实验代码,通过串口或者 LCD 显示查看),这里我们仅以 ILI9320 控制 器为例进行介绍,其他的控制基本都类似,我们就不详细阐述了。 ILI9320 液晶控制器自带显存,其显存总大小为 172820(240*320*18/8),即 18 位模式(26 万色)下的显存量。模块的 16 位数据线与显寸的对应关系为 565 方式,如图 18.1.1.4 所示: 图 18.1.1.4 16 位数据与显存对应关系图 最低 5 位代表蓝色,中间 6 位为绿色,最高 5 位为红色。数值越大,表示该颜色越深。 接下来,我们介绍一下 ILI9320 的几个重要命令,因为 ILI9320 的命令很多,我们这里不 可能一一介绍,有兴趣的大家可以找到 ILI9320 的 datasheet 看看。里面对这些命令有详细的介 绍。这里我们要介绍的命令列表如表 18.1.1.1 所示: 表 18.1.1.1 ILI9320 常用命令表 R0,这个命令,有两个功能,如果对它写,则最低位为 OSC,用于开启或关闭振荡器。而 如果对它读操作,则返回的是控制器的型号。这个命令最大的功能就是通过读它可以得到控制 器的型号,而我们代码在知道了控制器的型号之后,可以针对不同型号的控制器,进行不同的 初始化。因为 93xx 系列的初始化,其实都比较类似,我们完全可以用一个代码兼容好几个控制 器。 R3,入口模式命令。我们重点关注的是 I/D0、I/D1、AM 这 3 个位,因为这 3 个位控制了 屏幕的显示方向。 AM:控制 GRAM 更新方向。当 AM=0 的时候,地址以行方向更新。当 AM=1 的时候,地 址以列方向更新。 I/D[1:0]:当更新了一个数据之后,根据这两个位的设置来控制地址计数器自动增加/减少 1, 其关系如图 18.1.1.5 所示: www.openedv.com 247 ALIENTEK 战舰STM32开发板 图 18.1.1.5 GRAM 显示方向设置图 通过这几个位的设置,我们就可以控制屏幕的显示方向了,这种方法虽然简单,但是不是 很通用,比如不同的液晶,可能这里差别就比较大,有的甚至无法通用!比如 9341 和 9320 就 完全不通用。 R7,显示控制命令。该命令 CL 位用来控制是 8 位彩色,还是 26 万色。为 0 时 26 万色, 为 1 时八位色。D1、D0、BASEE 这三个位用来控制显示开关与否的。当全部设置为 1 的时候 开启显示,全 0 是关闭。我们一般通过该命令的设置来开启或关闭显示器,以降低功耗。 R32,R33,设置 GRAM 的行地址和列地址。R32 用于设置列地址(X 坐标,0~239),R33 用于设置行地址(Y 坐标,0~319)。当我们要在某个指定点写入一个颜色的时候,先通过这两 个命令设置到改点,然后写入颜色值就可以了。 R34,写数据到 GRAM 命令,当写入了这个命令之后,地址计数器才会自动的增加和减少。 该命令是我们要介绍的这一组命令里面唯一的单个操作的命令,只需要写入该值就可以了,其 他的都是要先写入命令编号,然后写入操作数。 R80~R83,行列 GRAM 地址位置设置。这几个命令用于设定你显示区域的大小,我们整个 屏的大小为 240*320,但是有时候我们只需要在其中的一部分区域写入数据,如果用先写坐标, 后写数据这样的方式来实现,则速度大打折扣。此时我们就可以通过这几个命令,在其中开辟 一个区域,然后不停的丢数据,地址计数器就会根据 R3 的设置自动增加/减少,这样就不需要 频繁的写地址了,大大提高了刷新的速度。 命令部分,我们就为大家介绍到这里,我们接下来看看要如何才能驱动 ALIENTEK TFTLCD 模块,这里 TFTLCD 模块的初始化和我们前面介绍的 OLED 模块的初始化框图是一样 的,只是初始化代码部分不同。接下来我们也是将该模块用来来显示字符和数字。通过以上介 绍,我们可以得出 TFTLCD 显示需要的相关设置步骤如下: 1)设置 STM32 与 TFTLCD 模块相连接的 IO。 这一步,先将我们与 TFTLCD 模块相连的 IO 口进行初始化,以便驱动 LCD。这里我们用 到的是 FSMC,FSMC 将在 18.1.2 节向大家详细介绍。 2)初始化 TFTLCD 模块。 其实这里就是上和上面 OLED 模块的初始化过程差不多。通过向 TFTLCD 写入一系列的设 置,来启动 TFTLCD 的显示。为后续显示字符和数字做准备。 3)通过函数将字符和数字显示到 TFTLCD 模块上。 www.openedv.com 248 ALIENTEK 战舰STM32开发板 这里就是通过我们设计的程序,将要显示的字符送到 TFTLCD 模块就可以了,这些函数将 在软件设计部分向大家介绍。通过以上三步,我们就可以使用 ALIENTEK TFTLCD 模块来显示 字符和数字了, 并且可以显示各种颜色的背景。 18.1.2 FSMC 简介 大容量,且引脚数在 100 脚以上的 STM32F103 芯片都带有 FSMC 接口,ALIENTEK 战舰 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 www.openedv.com 249 ALIENTEK 战舰STM32开发板 的连接,外部 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, 对 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 根地址线 www.openedv.com 250 ALIENTEK 战舰STM32开发板 (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区 FSMC_NE1 0X6000,0000~63FF,FFFF 00 第2区 第3区 FSMC_NE2 FSMC_NE3 0X6400,0000~67FF,FFFF 0X6800,0000~6BFF,FFFF 01 FSMC_A[25:0] 10 第4区 FSMC_NE4 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 所列: www.openedv.com 251 ALIENTEK 战舰STM32开发板 时序模型 简单描述 时间参数 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 同步突发 根据同步时钟 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 所示: www.openedv.com 252 ALIENTEK 战舰STM32开发板 图 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 所示: www.openedv.com 253 ALIENTEK 战舰STM32开发板 图 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 www.openedv.com 254 ALIENTEK 战舰STM32开发板 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 个参数是用 www.openedv.com 255 ALIENTEK 战舰STM32开发板 来配置片选控制寄存器 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 寄存器的时候都有提到,大家可以翻过去看看。 www.openedv.com 256 ALIENTEK 战舰STM32开发板 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 模块插 www.openedv.com 257 ALIENTEK 战舰STM32开发板 上去就好了。实物连接如图 18.2.2 所示: 图 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 { u16 LCD_REG; u16 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 的偏移量。我们将这个地址强制转换为 www.openedv.com 258 ALIENTEK 战舰STM32开发板 LCD_TypeDef 结构体地址,那么可以得到 LCD->LCD_REG 的地址就是 0X6C00,07FE,对应 A10 的状态为 0(即 RS=0),而 LCD-> LCD_RAM 的地址就是 0X6C00,0800(结构体地址自增), 对应 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; //LCD 宽度 u16 height; //LCD 高度 u16 id; //LCD ID u8 dir; //横屏还是竖屏控制:0,竖屏;1,横屏。 u8 wramcmd; //开始写 gram 指令 u8 setxcmd; //设置 x 坐标指令 u8 setycmd; //设置 y 坐标指令 }_lcd_dev; //LCD 参数 extern _lcd_dev lcddev; //管理 LCD 重要参数 该结构体用于保存一些 LCD 重要参数信息,比如 LCD 的长宽、LCD ID(驱动 IC 型号)、 LCD 横竖屏状态等,这个结构体虽然占用了 10 个字节的内存,但是却可以让我们的驱动函数 支持不同尺寸的 LCD,同时可以实现 LCD 横竖屏切换等重要功能,所以还是利大于弊的。有 了以上了解,下面我们开始介绍 lcd.c 里面的一些重要函数。 先看 6 个简单,但是很重要的函数: //写寄存器函数 //regval:寄存器值 void LCD_WR_REG(u16 regval) { LCD->LCD_REG=regval; //写入要写的寄存器序号 } //写 LCD 数据 //data:要写入的值 void LCD_WR_DATA(u16 data) { LCD->LCD_RAM=data; } //读 LCD 数据 //返回值:读到的值 www.openedv.com 259 ALIENTEK 战舰STM32开发板 u16 LCD_RD_DATA(void) { return LCD->LCD_RAM; } //写寄存器 //LCD_Reg:寄存器地址 //LCD_RegValue:要写入的数据 void LCD_WriteReg(u8 LCD_Reg, u16 LCD_RegValue) { LCD->LCD_REG = LCD_Reg; //写入要写的寄存器序号 LCD->LCD_RAM = LCD_RegValue; //写入数据 } //读寄存器 //LCD_Reg:寄存器地址 //返回值:读到的数据 u16 LCD_ReadReg(u8 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; } 因为 FSMC 自动控制了 WR/RD/CS 等这些信号,所以这 6 个函数实现起来都非常简单,我 们就不多说,实现功能见函数前面的备注,通过这几个简单函数的组合,我们就可以对 LCD 进行各种操作了。 第七个要介绍的函数是坐标设置函数,该函数代码如下: //设置光标位置 //Xpos:横坐标 //Ypos:纵坐标 void LCD_SetCursor(u16 Xpos, u16 Ypos) { if(lcddev.id==0X9341) { 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 { www.openedv.com 260 ALIENTEK 战舰STM32开发板 if(lcddev.dir==1)Xpos=lcddev.width-1-Xpos; //横屏其实就是调转 x,y 坐标 LCD_WriteReg(lcddev.setxcmd, Xpos); LCD_WriteReg(lcddev.setycmd, Ypos); } } 该函数实现将 LCD 的当前操作点设置到指定坐标(x,y)。因为 9341 的设置同其他屏有些不 太一样,所以单独对 9341 进行了设置。 接下来我们介绍第八个函数:画点函数。该函数实现代码如下: //画点 //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) { u16 r=0,g=0,b=0; if(x>=lcddev.width||y>=lcddev.height)return 0; //超过了范围,直接返回 LCD_SetCursor(x,y); if(lcddev.id==0X9341)LCD_WR_REG(0X2E); else LCD_WR_REG(R34); //9341 发送读 GRAM 指令 //其他 IC 发送读 GRAM 指令 if(lcddev.id==0X9320)opt_delay(2); //FOR 9320,延时 2us if(LCD->LCD_RAM)r=0; //dummy Read opt_delay(2); www.openedv.com 261 ALIENTEK 战舰STM32开发板 r=LCD->LCD_RAM; //实际坐标颜色 if(lcddev.id==0X9341)//9341 要分 2 次读出 { opt_delay(2); b=LCD->LCD_RAM; g=r&0XFF; //对于 9341,第一次读取的是 RG 的值,R 在前,G 在后,各占 8 位 g<<=8; } //这几种 IC 直接返回颜色值 if(lcddev.id==0X4535||lcddev.id==0X4531||lcddev.id==0X8989||lcddev.id==0XB505)return r; else if(lcddev.id==0X9341)return (((r>>11)<<11)|((g>>10)<<5)|(b>>11)); //ILI9341 需要公式转换一下 else return LCD_BGR2RGB(r); //其他 IC } 在 LCD_ReadPoint 函数中,因为我们的代码不止支持一种 LCD 驱动器,所以,我们根据 不同的 LCD 驱动器((lcddev.id)型号,执行不同的操作,以实现对各个驱动器兼容,提高函数 的通用性。 第十个要介绍的是字符显示函数 LCD_ShowChar,该函数同前面 OLED 模块的字符显示函 数差不多,但是这里的字符显示函数多了 1 个功能,就是可以以叠加方式显示,或者以非叠加 方式显示。叠加方式显示多用于在显示的图片上再显示字符。非叠加方式一般用于普通的显示。 该函数实现代码如下: //在指定位置显示一个字符 //x,y:起始坐标 //num:要显示的字符:" "--->"~" //size:字体大小 12/16 //mode:叠加方式(1)还是非叠加方式(0) void LCD_ShowChar(u16 x,u16 y,u8 num,u8 size,u8 mode) { u8 temp,t1,t; u16 y0=y; u16 colortemp=POINT_COLOR; //设置窗口 num=num-' ';//得到偏移后的值 if(!mode) //非叠加方式 { for(t=0;t=lcddev.height)return; //超区域了 if((y-y0)==size) { y=y0; x++; if(x>=lcddev.width)return; //超区域了 break; } } } }else//叠加方式 { for(t=0;t=lcddev.height)return; //超区域了 if((y-y0)==size) { y=y0; x++; if(x>=lcddev.width)return; //超区域了 break; } } } } POINT_COLOR=colortemp; } 在 LCD_ShowChar 函数里面,我们采用画点函数来显示字符,虽然速度不如开辟窗口的方 式,但是这样写可以有更好的兼容性,方便在不同 LCD 之间移植。该代码中我们用到了两个字 符集点阵数据数组 asc2_1206 和 asc2_1608,这两个字符集的点阵数据的提取方式,同十七章介 绍的提取方法是一模一样的。详细请参考第十七章。 最后,我们再介绍一下 TFTLCD 模块的初始化函数 LCD_Init,该函数先初始化 STM32 与 TFTLCD 连接的 IO 口,并配置 FSMC 控制器,然后读取 LCD 控制器的型号,根据控制 IC 的 www.openedv.com 263 ALIENTEK 战舰STM32开发板 型号执行不同的初始化代码,其简化代码如下: //初始化 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; 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 www.openedv.com 264 ALIENTEK 战舰STM32开发板 readWriteTiming.FSMC_BusTurnAroundDuration = 0x00; readWriteTiming.FSMC_CLKDivision = 0x00; readWriteTiming.FSMC_DataLatency = 0x00; readWriteTiming.FSMC_AccessMode = FSMC_AccessMode_A; //模式 A writeTiming.FSMC_AddressSetupTime = 0x00; writeTiming.FSMC_AddressHoldTime = 0x00; writeTiming.FSMC_DataSetupTime = 0x03; //地址建立时间为 1 个 HCLK //地址保持时间(A //数据保存时间为 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= 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); delay_ms(50); // delay 50 ms LCD_WriteReg(0x0000,0x0001); delay_ms(50); // delay 50 ms lcddev.id = LCD_ReadReg(0x0000); if(lcddev.id==0||lcddev.id==0XFFFF)//读到 ID 不正确 ⑤ // ④使能 BANK1 www.openedv.com 265 ALIENTEK 战舰STM32开发板 { //尝试 9341 的 ID 读取 LCD_WR_REG(0XD3); LCD_RD_DATA(); //dummy read LCD_RD_DATA(); //读到 0X00 lcddev.id=LCD_RD_DATA(); //读取 93 lcddev.id<<=8; lcddev.id|=LCD_RD_DATA(); //读取 41 } printf(" LCD ID:%x\r\n",lcddev.id); //打印 LCD ID if(lcddev.id==0X9341) //9341 初始化 { ……//9341 初始化代码 }else if(lcddev.id==0x9325) //9325 { ……//9325 初始化代码 }else if(lcddev.id==0x9328) //ILI9328 OK { ……//9328 初始化代码 }else if(lcddev.id==0x9320||lcddev.id==0x9300)//未测试. { ……//9300 初始化代码 }else if(lcddev.id==0X9331) { ……//9331 初始化代码 }else if(lcddev.id==0x5408) { ……//5408 初始化代码 } else if(lcddev.id==0x1505)//OK { ……//1505 初始化代码 }else if(lcddev.id==0xB505) { ……//B505 初始化代码 }else if(lcddev.id==0xC505) { ……//C505 初始化代码 }else if(lcddev.id==0x8989) { ……//8989 初始化代码 }else if(lcddev.id==0x4531) { www.openedv.com 266 ALIENTEK 战舰STM32开发板 ……//4531 初始化代码 }else if(lcddev.id==0x4535) { ……//4535 初始化代码 } LCD_Display_Dir(0); LCD_LED=1; //默认为竖屏显示 //点亮背光 LCD_Clear(WHITE); } 从初始化代码可以看出,LCD 初始化步骤为①~⑤在代码中标注: ①GPIO,FSMC,AFIO 时钟使能。 ②GPIO 初始化:GPIO_Init()函数。 ③FSMC 初始化:FSMC_NORSRAMInit()函数。 ④FSMC 使能:FSMC_NORSRAMCmd()函数。 ⑤不同的 LCD 驱动器的初始化代码。 该函数先对 FSMC 相关 IO 进行初始化,然后是 FSMC 的初始化,这个我们在前面都有介 绍,最后根据读到的 LCD ID,对不同的驱动器执行不同的初始化代码,从上面的代码可以看 出,这个初始化函数可以针对 13 款不同的驱动 IC 执行初始化操作,这样大大提高了整个程序 的通用性。大家在以后的学习中应该多使用这样的方式,以提高程序的通用性、兼容性。 特别注意:本函数使用了 printf 来打印 LCD ID,所以,如果你在主函数里面没有初始化串 口,那么将导致程序死在 printf 里面!!如果不想用 printf,那么请注释掉它。 大家可以打开 lcd.h 和 lcd.c 文件看里面的代码,了解各个函数功能。接下来,我们打开 main.c 内容如下: int main(void) { u8 x=0; u8 lcd_id[12]; delay_init(); NVIC_Configuration(); uart_init(9600); LED_Init(); //存放 LCD ID 字符串 //延时函数初始化 //设置 NVIC 中断分组 2:2 位抢占优先级,2 位响应优先级 //串口初始化波特率为 9600 //LED 端口初始化 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; www.openedv.com 267 ALIENTEK 战舰STM32开发板 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,50,200,16,16,"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 LCD_ShowString(30,130,200,16,16,"2012/9/5"); x++; if(x==12)x=0; LED0=!LED0; delay_ms(1000); } } 该部分代码将显示一些固定的字符,同时显示 LCD 驱动 IC 的型号,然后不停的切换背景 颜色,每 1s 切换一次。而 LED0 也会不停的闪烁,指示程序已经在运行了。其中我们用到一个 sprintf 的函数,该函数用法同 printf,只是 sprintf 把打印内容输出到指定的内存区间上,sprintf 的详细用法,请百度。 在编译通过之后,我们开始下载验证代码。 18.4 下载验证 将程序下载到战舰 STM32 后,可以看到 DS0 不停的闪烁,提示程序已经在运行了。同时 可以看到 TFTLCD 模块的显示如图 18.4.1 所示: www.openedv.com 268 ALIENTEK 战舰STM32开发板 图 18.4.1 TFTLCD 显示效果图 我们可以看到屏幕的背景是不停切换的,同时 DS0 不停的闪烁,证明我们的代码被正确的 执行了,达到了我们预期的目的。 www.openedv.com 269 ALIENTEK 战舰STM32开发板 第十九章 USMART 调试组件实验 本章,我们将向大家介绍一个十分重要的辅助调试工具:USMART 调试组件。该组件由 ALIENTEK 开发提供,功能类似 linux 的 shell(RTT 的 finsh 也属于此类)。USMART 最主要 的功能就是通过串口调用单片机里面的函数,并执行,对我们调试代码是很有帮助的。本章分 为如下几个部分: 19.1 USMART 调试组件简介 19.2 硬件设计 19.3 软件设计 19.4 下载验证 www.openedv.com 270 ALIENTEK 战舰STM32开发板 19.1 USMART 调试组件简介 USMART 是由 ALIENTEK 开发的一个灵巧的串口调试互交组件,通过它你可以通过串口 助手调用程序里面的任何函数,并执行。因此,你可以随意更改函数的输入参数(支持数字(10/16 进制)、字符串、函数入口地址等作为参数),单个函数最多支持 10 个输入参数,并支持函数返 回值显示,目前最新版本为 V2.8。 USMART 的特点如下: 1, 可以调用绝大部分用户直接编写的函数。 2, 资源占用极少(最少情况:FLASH:4K;SRAM:72B)。 3, 支持参数类型多(数字(包含 10/16 进制)、字符串、函数指针等)。 4, 支持函数返回值显示。 5, 支持参数及返回值格式设置。 6, 使用方便。 有了 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 所示: www.openedv.com 图 19.1.1 USMART 组件代码 271 ALIENTEK 战舰STM32开发板 其中 redeme.txt 是一个说明文件,不参与编译。其他五个文件,usmart.c 负责与外部互交等。 usmat_str.c 主要负责命令和参数解析。usmart_config.c 主要由用户添加需要由 usmart 管理的函 数。 usmart.h 和 usmart_str.h 是两个头文件,其中 usmart.h 里面含有几个用户配置宏定义,可以 用来配置 usmart 的功能及总参数长度(直接和 SRAM 占用挂钩)、是否使能定时器扫描、是否使 用读写函数等。 USMART 的移植,只需要实现 3 个函数。其中两个函数都在 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 Timer2_Init(1000,(u32)sysclk*100-1);//分频,时钟为 10K ,100ms 中断一次 #endif usmart_dev.sptype=1; //十六进制显示参数 } 该函数有一个参数 sysclk,就是用于定时器初始化。这里需要说明一下,为了让我们的库 函数和寄存器实现函数一致,我们这里不直接通过 SystemCoreClock 来获取系统时钟,直接通 过在外面设置的方式,当然你也可以去掉 sysclk 这个参数,这样函数体里面的 Timer2_Init 函数 就可修改为 Timer2_Init(1000,(u32) SystemCoreClock/10000-1)。另外 USMART_ENTIMX_SCAN 是在 usmart.h 里面定义的一个是否使能定时器中断扫描的宏定义。如果为 1,就通过定时器初 始 化 函数 Timer2_Init 初 始 化 定 时器 2 中断 ,每 100ms 中 断 一次 , 并在 中 断 服务 程 序 TIM2_IRQHandler 里面调用 usmart_scan 函数进行扫描,这里我们就不列出代码,因为之前的 实验对这方面讲解较多。如果为 0,那么需要用户需要自行间隔一定时间(100ms 左右为宜) 调用一次 usmart_scan 函数,以实现串口数据处理。 最后一个是 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)//串口接收完成? www.openedv.com 272 ALIENTEK 战舰STM32开发板 { 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) { 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 的使用。 www.openedv.com 273 ALIENTEK 战舰STM32开发板 19.2 硬件设计 本实验用到的硬件资源有: 1) 指示灯 DS0 和 DS1 2) 串口 3) TFTLCD 模块 这三个硬件在前面章节均有介绍,本章不再介绍。 19.3 软件设计 这里我们在上一章的实验的基础上通过添加文件的方式讲解 USMART 的引入,当然大家 也可以直接打开我们光盘的实例工程。打开上一章的工程,复制 USMART 文件夹(该文件夹 可以在光盘的本章实验例程里面找到)到本工程文件夹下面,如图 19.3.1 所示: 图 19.3.1 复制 USMART 文件夹到工程文件夹下 接着,我们打开工程,并新建 USMART 组,添加 USMART 组件代码,同时把 USMART 文件夹添加到头文件包含路径,在主函数里面加入 include“usmart.h”如图 19.3.2 所示: www.openedv.com 274 ALIENTEK 战舰STM32开发板 图 19.3.2 添加 USMART 组件代码 由于 USMART 默认提供了 STM32 的 TIM2 中断初始化设置代码,我们只需要在 usmart.h 里面设置 USMART_ENTIMX_SCAN 为 1,即可完成 TIM2 的设置,通过 TIM2 的中断服务函 数,调用 usmart_dev.scan()(就是 usmart_scan 函数),实现 usmart 的扫描。此部分代码我们就 不列出来了,请参考 usmart.c。 此时,我们就可以使用 USMART 了,不过在主程序里面还得执行 usmart 的初始化,另外 还需要针对你自己想要被 USMART 调用的函数在 usmart_config.c 里面进行添加。下面先介绍 如何添加自己想要被 USMART 调用的函数,打开 usmart_config.c,如图 19.3.3 所示: www.openedv.com 275 ALIENTEK 战舰STM32开发板 图 19.3.3 添加需要被 USMART 调用的函数 这里的添加函数很简单,只要把函数所在头文件添加进来,并把函数名按上图所示的方式 增加即可,默认我们添加了两个函数:delay_ms 和 delay_us。另外,read_addr 和 write_addr 属 于 usmart 自带的函数,用于读写指定地址的数据,通过配置 USMART_USE_WRFUNS,可以 使能或者禁止这两个函数。 这里我们根据自己的需要按上图的格式添加其他函数,添加完之后如图 19.3.4 所示: www.openedv.com 276 ALIENTEK 战舰STM32开发板 图 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_Configuration(); //设置 NVIC 中断分组 2:2 位抢占优先级,2 位响应优先级 uart_init(9600); //串口初始化波特率为 9600 LED_Init(); //LED 端口初始化 www.openedv.com 277 ALIENTEK 战舰STM32开发板 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,"2011/6/18"); while(1) { LED0=!LED0; delay_ms(500); } } 此代码显示简单的信息后,就是在死循环等待串口数据。 至此,整个 usmart 的移植就完成了。编译成功后,就可以下载程序到开发板,开始 USMART 的体验。 19.4 下载验证 将程序下载到战舰 STM32 后,可以看到 DS0 不停的闪烁,提示程序已经在运行了。同时, 屏幕上显示了一些字符(就是主函数里面要显示的字符)。 我们打开串口调试助手,选择正确的串口号,并选择发送新行(即发送回车键)选项。如 下图所示(点击扩展->隐藏,显示扩展界面): www.openedv.com 图 19.4.1 驱动串口调试助手 278 ALIENTEK 战舰STM32开发板 上图中 list、id、?、help、hex 和 dec 都属于 usmart 自带的系统命令。下面我们简单介绍 下这几个命令: list,该命令用于打印所有 usmart 可调用函数。发送该命令后,串口将受到所有能被 usmart 调用得到函数如图 19.4.2 所示: 图 19.4.2 list 指令执行结果 id,该指令用于获取各个函数的入口地址。比如前面写的 test_fun 函数,就有一个函数参数, 我们需要先通过 id 指令,获取 ledset 函数的 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 进制打印出来。 大家可以亲自体验下这几个系统指令,不过要注意,所有的指令都是大小写敏感的,不要 写错哦。 接下来,我们将介绍如何调用 list 所打印的这些函数,先来看一个简单的 delay_ms 的调用, 我们分别输入 delay_ms(1000)和 delay_ms(0x3E8),如图 19.4.3 所示: www.openedv.com 279 ALIENTEK 战舰STM32开发板 图 19.4.3 串口 delay_ms 函数 从上图可以看出,delay_ms(1000)和 delay_ms(0x3E8)的调用结果是一样的,都是延时 1000ms,因为 usmart 默认设置的是 hex 显示,所以看到串口打印的参数都是 16 进制格式的, 大家可以通过发送 dec 指令切换为十进制显示。另外,由于 usmart 对调用函数的参数大小写不 敏感,所以参数写成:0X3E8 或者 0x3e8 都是正确的。 我们再看另外一个函数,LCD_ShowString 函数,该函数用于显示字符串,我们通过串口输 入:LCD_ShowString(20,200,200,100,16,"This is a test for usmart!!"),如图 19.4.4 所示: 图 19.4.4 串口调用 LCD_ShowString 函数 www.openedv.com 280 ALIENTEK 战舰STM32开发板 该函数用于在指定区域,显示指定字符串,发送给开发板后,我们可以看到 LCD 在我们指 定的地方显示了:This is a test for usmart!! 这个字符串。 其他函数的调用,也都是一样的方法,这里我们就不多介绍了,最后说一下带有函数参数 的函数的调用。我们将 led_set 函数作为 test_fun 的参数,通过在 test_fun 里面调用 led_set 函数, 实现对 DS1(LED1)的控制。前面说过,我们要调用带有函数参数的函数,就必须先得到函数参 数的入口地址(id),通过输入 id 指令,我们可以得到 led_set 的函数入口地址是:0X08000281, 所以,我们在串口输入:test_fun(0X08000281,0),就可以控制 DS1 亮了。如图 19.4.5 所示: 图 19.4.5 串口调用 test_fun 函数 在开发板上,我们可以看到,收到串口发送的 test_fun(0X08000281,0)后,开发板的 DS1 亮了,然后大家可以通过发送 test_fun(0X08000281,1),来关闭 DS1。说明我们成功的通过 test_fun 函数调用 led_set,实现了对 DS1 的控制。也就验证了 USMART 对函数参数的支持。 USMART 调试组件的使用,就为大家介绍到这里。USMART 是一个非常不错的调试组件, 希望大家能学会使用,可以达到事半功倍的效果。 www.openedv.com 281 ALIENTEK 战舰STM32开发板 第二十章 RTC 实时时钟实验 前面我们介绍了两款液晶模块,这一章我们将介绍 STM32 的内部实时时钟(RTC)。在本 章中,我们将利用 ALIENTEK 2.8 寸 TFTLCD 模块来显示日期和时间,实现一个简单的时钟。 另外,本章将顺带向大家介绍 BKP 的使用。本章分为如下几个部分: 20.1 STM32 RTC 时钟简介 20.2 硬件设计 20.3 软件设计 20.4 下载验证 www.openedv.com 282 ALIENTEK 战舰STM32开发板 20.1 STM32 RTC 时钟简介 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 年左右,作为一般应用,这已经是足够了的。 www.openedv.com 283 ALIENTEK 战舰STM32开发板 RTC 还有一个闹钟寄存器 RTC_ALR,用于产生闹钟。系统时间按 TR_CLK 周期累加并与 存储在 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 所示: www.openedv.com 284 ALIENTEK 战舰STM32开发板 图 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 所示: www.openedv.com 285 ALIENTEK 战舰STM32开发板 图 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, www.openedv.com 286 ALIENTEK 战舰STM32开发板 不过这个 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 文件中。 www.openedv.com 287 ALIENTEK 战舰STM32开发板 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 秒中断 www.openedv.com 288 ALIENTEK 战舰STM32开发板 下一步便是设置时间了,设置时间实际上就是设置 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,其代码如下: //实时时钟配置 www.openedv.com 289 ALIENTEK 战舰STM32开发板 //初始化 RTC 时钟,同时检测时钟是否工作正常 //BKP->DR1 用于保存是否第一次配置的设置 //返回 0:正常 //其他:错误代码 u8 RTC_Init(void) { u8 temp=0; //检查是不是第一次配置时钟 if (BKP_ReadBackupRegister(BKP_DR1) != 0x5050) //从指定的后备寄存器中 //读出数据:读出了与写入的指定数据不相乎 { RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); //使能 PWR 和 BKP 外设时钟 PWR_BackupAccessCmd(ENABLE); BKP_DeInit(); //使能后备寄存器访问 //③复位备份区域 RCC_LSEConfig(RCC_LSE_ON); //设置外部低速晶振(LSE) while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) //检查指定的 //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(2009,12,2,10,0,55); //设置时间 RTC_ExitConfigMode(); //退出配置模式 BKP_WriteBackupRegister(BKP_DR1, 0X5050); //向指定的后备寄存器中 //写入用户程序数据 0x5050 } else//系统继续计时 { RTC_WaitForSynchro(); //等待最近一次对 RTC 寄存器的写操作完成 RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能 RTC 秒中断 RTC_WaitForLastTask(); //等待最近一次对 RTC 寄存器的写操作完成 www.openedv.com 290 ALIENTEK 战舰STM32开发板 } RTC_NVIC_Config(); //RCT 中断分组设置 RTC_Get(); //更新时间 return 0; //ok } 该函数用来初始化 RTC 时钟,但是只在第一次的时候设置时间,以后如果重新上电/复位 都不会再进行时间设置了(前提是备份电池有电),在第一次配置的时候,我们是按照上面介绍 的 RTC 初始化步骤来做的,这里就不在多说了,这里我们设置时间是通过时间设置函数 RTC_Set(2012,9,7,13,16,55);来实现的,这里我们默认将时间设置为 2012 年 9 月 7 日 13 点 16 分 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 {temp1++;break;} } else temp-=365; //平年 temp1++; } calendar.w_year=temp1; //得到年份 temp1=0; while(temp>=28) //超过了一个月 { if(Is_Leap_Year(calendar.w_year)&&temp1==1)//当年是不是闰年/2 月份 www.openedv.com 292 ALIENTEK 战舰STM32开发板 { 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_ClearITPendingBit(RTC_IT_SEC|RTC_IT_OW); //清闹钟中断 RTC_WaitForLastTask(); } 此部分代码比较简单,我们通过 RTC_GetITStatus 来判断发生的是何种中断,如果是秒钟 www.openedv.com 293 ALIENTEK 战舰STM32开发板 中断,则执行一次时间的计算,获得最新时间。从而,我们可以在 calendar 里面读到时间、日 期等信息。 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_Configuration(); //设置 NVIC 中断分组 2 uart_init(9600); //串口初始化波特率为 9600 LED_Init(); //LED 端口初始化 LCD_Init(); //LCD 初始化 usmart_dev.init(72); //初始化 USMART POINT_COLOR=RED; //设置字体为红色 LCD_ShowString(60,50,200,16,16,"WarShip STM32"); LCD_ShowString(60,70,200,16,16,"RTC TEST"); LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(60,110,200,16,16,"2012/9/7"); while(RTC_Init()) //RTC 初始化 ,一定要初始化成功 { LCD_ShowString(60,130,200,16,16,"RTC ERROR! "); delay_ms(800); 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) www.openedv.com 294 ALIENTEK 战舰STM32开发板 { 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 秒钟闪烁一次,用来提示程序已经开始 跑了。 为了方便设置时间,我们在 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)", www.openedv.com 295 ALIENTEK 战舰STM32开发板 #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 来设置一下当前 时间。 www.openedv.com 296 ALIENTEK 战舰STM32开发板 第二十一章 待机唤醒实验 本章我们将向大家介绍 STM32 的待机唤醒功能。在本章中,我们将利用 WK_UP 按键来实 现唤醒和进入待机模式的功能,然后利用 DS0 指示状态。本章将分为如下几个部分: 21.1 STM32 待机模式简介 21.2 硬件设计 21.3 软件设计 21.4 下载验证 www.openedv.com 297 ALIENTEK 战舰STM32开发板 21.1 STM32 待机模式简介 很多单片机都有低功耗模式,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 闹钟事件 www.openedv.com 298 ALIENTEK 战舰STM32开发板 发生时,微控制器从待机模式退出。从待机唤醒后,除了电源控制/状态寄存器(PWR_CSR),所 有寄存器被复位。 从待机模式唤醒后的代码执行等同于复位后的执行(采样启动模式引脚,读取复位向量等)。 电源控制/状态寄存器(PWR_CSR)将会指示内核由待机状态退出。 在进入待机模式后,除了复位引脚以及被设置为防侵入或校准输出时的 TAMPER 引脚和被 使能的唤醒引脚(WK_UP 脚),其他的 IO 引脚都将处于高阻态。 图 21.1.1 已经清楚的说明了进入待机模式的通用步骤,其中涉及到 2 个寄存器,即电源控 制寄存器(PWR_CR)和电源控制/状态寄存器(PWR_CSR)。下面我们介绍一下这两个寄存器: 电源控制寄存器(PWR_CR),该寄存器的各位描述如图 21.1.2 所示: www.openedv.com 299 ALIENTEK 战舰STM32开发板 图 21.1.2 PWR_CR 寄存器各位描述 这里我们通过设置 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 从待机模式唤 www.openedv.com 300 ALIENTEK 战舰STM32开发板 醒。在库函数中,设置使能 WK_UP 用于唤醒 CPU 待机模式的函数是: 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:错误的触发 www.openedv.com 301 ALIENTEK 战舰STM32开发板 u8 Check_WKUP(void) { u8 t=0; u8 tx=0; //记录松开的次数 LED0=0; //亮灯 DS0 while(1) { if(WKUP_KD) //已经按下了 { t++; tx=0; }else { tx++; //超过 300ms 内没有 WKUP 信号 if(tx>3) { LED0=1; return 0; //错误的按键,按下次数不够 } } delay_ms(30); if(t>=100) //按下超过 3 秒钟 { LED0=0; //点亮 DS0 return 1; //按下 3s 以上了 } } } //中断,检测到 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 和复用功能时钟 www.openedv.com 302 ALIENTEK 战舰STM32开发板 GPIO_InitStructure.GPIO_Pin =GPIO_Pin_0; //PA.0 GPIO_InitStructure.GPIO_Mode =GPIO_Mode_IPD; //上拉输入 GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 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_Configuration(); uart_init(9600); LED_Init(); WKUP_Init(); LCD_Init(); //延时函数初始化 //设置 NVIC 中断分组 2 //串口初始化波特率为 9600 //LED 端口初始化 //待机唤醒初始化 //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,"2012/9/7"); www.openedv.com 303 ALIENTEK 战舰STM32开发板 while(1) { LED0=!LED0; delay_ms(250); } } 这里我们先初始化 LED 和 WK_UP 按键(通过 WKUP_Init()函数初始化),在死循环里 面等待 WK_UP 中断的到来,在得到中断后,在中断函数里面判断 WK_UP 按下的时间长短, 来决定是否进入待机模式。在 WKUP_Init 函数里面,我们有检测 WK_UP 是否按下 3 秒来决定 是否开机,这点在前面已经介绍了。大家在下载完代码的时候要注意一下。 21.4 下载与测试 在代码编译成功之后,下载代码到 ALIENTEK 战舰 STM32 开发板上,此时,看到开发板 DS0 亮了一下(Check_WKUP 函数执行了 LED0=0 的操作),就没有反应了。其实这是正常的, 在程序下载完之后,开发板不能检测到 WK_UP 的持续按下,所以直接进入待机模式,看起来 和没有下载代码一样。此时,我们长按 WK_UP 按键 3 秒钟左右,可以看到 DS0 开始闪烁。然 后再长按 WK_UP,DS0 会灭掉,程序再次进入待机模式。 www.openedv.com 304 ALIENTEK 战舰STM32开发板 第二十二章 ADC 实验 本章我们将向大家介绍 STM32 的 ADC 功能。在本章中,我们将利用 STM32 的 ADC1 通 道 0 来采样外部电压值,并在 TFTLCD 模块上显示出来。本章将分为如下几个部分: 22.1 STM32 ADC 简介 22.2 硬件设计 22.3 软件设计 22.4 下载验证 www.openedv.com 305 ALIENTEK 战舰STM32开发板 22.1 STM32 ADC 简介 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 所 示: www.openedv.com 306 ALIENTEK 战舰STM32开发板 图 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 寄存器操作模式 该寄存器我们也只针对性的介绍一些位:ADCON 位用于开关 AD 转换器。而 CONT 位用 于设置是否进行连续转换,我们使用单次转换,所以 CONT 位必须为 0。CAL 和 RSTCAL 用 于 AD 校准。ALIGN 用于设置数据对齐,我们使用右对齐,该位设置为 0。 EXTSEL[2:0]用于选择启动规则转换组转换的外部事件,详细的设置关系如图 22.1.4 所示: www.openedv.com 307 ALIENTEK 战舰STM32开发板 图 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 所示: www.openedv.com 308 ALIENTEK 战舰STM32开发板 图 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)。另外两个规则序列寄存器 www.openedv.com 309 ALIENTEK 战舰STM32开发板 同 ADC_SQR1 大同小异,我们这里就不再介绍了,要说明一点的是:我们选择的是单次转换, 所以只有一个通道在规则序列里面,这个序列就是 SQ1,通过 ADC_SQR3 的最低 5 位(也就 是 SQ1)设置。 第四个要介绍的是 ADC 规则数据寄存器(ADC_DR)。规则序列中的 AD 转化结果都将被存 在这个寄存器里面,而注入通道的转换结果被保存在 ADC_JDRx 里面。ADC_DR 的各位描述 如图 22.1.8: 图 22.1.8 ADC_ JDRx 寄存器各位描述 这里要提醒一点的是,该寄存器的数据可以通过 ADC_CR2 的 ALIGN 位设置左对齐还是 右对齐。在读取数据的时候要注意。 最后一个要介绍的 ADC 寄存器为 ADC 状态寄存器(ADC_SR),该寄存器保存了 ADC 转 换时的各种状态。该寄存器的各位描述如图 22.1.9 所示: www.openedv.com 310 ALIENTEK 战舰STM32开发板 图 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 对应表: www.openedv.com 311 ALIENTEK 战舰STM32开发板 图 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; www.openedv.com 312 ALIENTEK 战舰STM32开发板 参数 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 转换的方法是: www.openedv.com 313 ALIENTEK 战舰STM32开发板 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; www.openedv.com 314 ALIENTEK 战舰STM32开发板 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | 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 通道的数目 ADC_Init(ADC1, &ADC_InitStructure); //根据指定的参数初始化外设 ADCx ADC_Cmd(ADC1, ENABLE); //使能指定的 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); //使能指定的 ADC1 的软件转换功能 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;t200)dacval-=200; www.openedv.com 331 ALIENTEK 战舰STM32开发板 else dacval=0; DAC_SetChannel1Data(DAC_Align_12b_R, dacval); //设置 DAC 值 } if(t==10||key==2||key==4) //WKUP/KEY1 按下了,或者定时时间到了 { adcx=DAC_GetDataOutputValue(DAC_Channel_1); //读取前面设置 DAC 的值 LCD_ShowxNum(124,150,adcx,4,16,0); //显示 DAC 寄存器值 temp=(float)adcx*(3.3/4096); //得到 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); //得到 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); //显示电压值的小数部分 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 可控 范围内)。 24.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK 战舰 STM32 开发板上,可以看到 LCD 显示如图 24.4.1 所示: www.openedv.com 332 ALIENTEK 战舰STM32开发板 图 24.4.1 ADC 实验实际测试图 同时伴随 DS0 的不停闪烁,提示程序在运行。此时,我们通过按 WK_UP 按键,可以看到 输出电压增大,按 KEY1 则变小。 大家可以试试在 USMART 调用 Dac1_Set_Vol 函数,来设置 DAC 通道 1 的输出电压,如图 24.4.2 所示: 图 24.4.2 通过 usmart 设置 DAC 通道 1 的电压输出 www.openedv.com 333 ALIENTEK 战舰STM32开发板 第二十五章 PWM DAC 实验 上一章,我们介绍了 STM32 自带 DAC 模块的使用,但不是每个 STM32 都有 DAC 模块的, 对于那些没有 DAC 模块的芯片,我们可以通过 PWM+RC 滤波来实一个 PWM DAC。本章我们 将向大家介绍如何利用 STM32 的 PWM 来设计一个 DAC。我们将利用按键(或 USMART)控 制 STM32 的 PWM 输出,从而控制 PWM DAC 的输出电压,通过 ADC1 的通道 1 采集 PWM DAC 的输出电压,并在 LCD 模块上面显示 ADC 获取到的电压值以及 PWM DAC 的设定输出电压值 等信息。本章将分为如下几个部分: 25.1 PWM DAC 简介 25.2 硬件设计 25.3 软件设计 25.4 下载验证 www.openedv.com 334 ALIENTEK 战舰STM32开发板 25.1 PWM DAC 简介 虽然大容量的 STM32F103 具有内部 DAC,但是更多的型号是没有 DAC 的,不过 STM32 所有的芯片都有 PWM 输出,因此,我们可以用 PWM+简单的 RC 滤波来实现 DAC 输出, 从而节省成本。 PWM 本质上其实就是是一种周期一定,而高低电平占空比可调的方波。实际电路的典 型 PWM 波形,如图 25.1.1 所示: 图 25.1.1 实际电路典型 PWM 波形 图 25.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 位。 www.openedv.com 335 ALIENTEK 战舰STM32开发板 在 8 位分辨条件下,我们一般要求 1 次谐波对输出电压的影响不要超过 1 个位的精度, 也就是 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 滤波,该部分原理图如图 25.1.2 所 示: 图 25.1.2 PWM DAC 二阶 RC 滤波原理图 二阶 RC 滤波截止频率计算公式为: f=1/2πRC 以上公式要求 R1=R2=R,C2=C2=C。根据这个公式,我们计算出图 25.1.2 的截止频率 为:33.8Khz 超过了 22.34Khz,这个和我们前面提到的要求有点出入,原因是该电路我们还 需要用作 PWM DAC 音频输出,而音频信号带宽是 22.05Khz,为了让音频信号能够通过该 低通滤波,同时为了标准化参数选取,所以确定了这样的参数。实测精度在 0.5LSB 以内。 PWM DAC 的原理部分,就为大家介绍到这里。 25.2 硬件设计 本章用到的硬件资源有: 1) 指示灯 DS0 2) WK_UP 和 KEY1 按键 3) 串口 4) TFTLCD 模块 5) ADC 6) PWM DAC 本章,我们使用 STM32 的 TIM4_CH1(PB6)输出 PWM,经过二阶 RC 滤波后,转换为 直流输出,实现 PWM DAC。同上一章一样,我们通过 ADC1 的通道 1(PA1)读取 PWM DAC 的输出,并在 LCD 模块上显示相关数值,通过按键和 USMART 控制 PWM DAC 的输出值。 我们需要用到 ADC 采集 DAC 的输出电压,所以需要在硬件上将 PWM DAC 和 ADC 短接 起来,PWM DAC 部分原理图如图 25.2.1 所示: www.openedv.com 336 ALIENTEK 战舰STM32开发板 图 25.2.1 PWM DAC 原理图 在硬件上,我们还需要用跳线帽短接多功能端口的 PDC 和 ADC,如图 25.2.2 所示: 图 25.2.2 硬件连接示意图 25.3 软件设计 找到 PWM DAC 实验工程,打开 timer.c 文件,可以看到我们在文件的最后添加了一个 新的函数:TIM4_PWM_Init,该函数代码如下: //TIM4 CH1 PWM 输出设置 //PWM 输出初始化 //arr:自动重装值 //psc:时钟预分频数 void TIM4_PWM_Init(u16 arr,u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; www.openedv.com 337 ALIENTEK 战舰STM32开发板 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); //使能 TIMx 外设 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);//使能 PB 时钟 //设置该引脚为复用输出功能,输出 TIM4 CH1 的 PWM 脉冲波形 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; //TIM_CH1 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用功能输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &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; //T 向上计数 TIM_TimeBaseInit(TIM4, &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(TIM4, &TIM_OCInitStructure); //根据指定的参数初始化外设 TIMx TIM_OC1PreloadConfig(TIM4, TIM_OCPreload_Enable); //CH1 预装载使能 TIM_ARRPreloadConfig(TIM4, ENABLE); //使能 TIMx 在 ARR 上的预装载寄存器 TIM_Cmd(TIM4, ENABLE); //使能 TIMx } 该函数用来初始化 TIM4_CH1 的 PWM 输出(PB6),其原理同之前介绍的 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(TIM4,temp); } int main(void) { u16 adcx; float temp; www.openedv.com 338 ALIENTEK 战舰STM32开发板 u8 t=0; u16 pwmval=0; u8 key; delay_init(); NVIC_Configuration(); uart_init(9600); KEY_Init(); LED_Init(); LCD_Init(); Adc_Init(); TIM4_PWM_Init(256,0); TIM_SetCompare1(TIM4,100); //延时函数初始化 //设置 NVIC 中断分组 2 //串口初始化波特率为 9600 //KEY 初始化 //LED 端口初始化 //LCD 初始化 //ADC 初始化 //TIM4 PWM 初始化, Fpwm=72M/256=281.25Khz. //初始值为 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,"2012/8/3"); LCD_ShowString(60,130,200,16,16,"WKUP:+ 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(TIM4,pwmval);//初始值 while(1) { t++; key=KEY_Scan(0); if(key==4) { if(pwmval<250)pwmval+=10; TIM_SetCompare1(TIM4,pwmval); //输出 }else if(key==2) { if(pwmval>10)pwmval-=10; else pwmval=0; TIM_SetCompare1(TIM4,pwmval); //输出 } if(t==10||key==2||key==4) //WKUP/KEY1 按下了,或者定时时间到了 { adcx=TIM_GetCapture1(TIM4); LCD_ShowxNum(124,150,adcx,4,16,0); //显示 DAC 寄存器值 temp=(float)adcx*(3.3/256); //得到 DAC 电压值 adcx=temp; LCD_ShowxNum(124,170,temp,1,16,0); //显示电压值整数部分 www.openedv.com 339 ALIENTEK 战舰STM32开发板 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 上面显示 TIM4_CCR1 寄存器的值、PWM DAC 设计输出电压以及 ADC 采集到的实 际输出电压。同时 DS0 闪烁,提示程序运行状况。 不过此部分代码还有一个 PWM_DAC_Set 函数,用于 USMART 调用,从而通过串口控 制 PWM DAC 的输出,所以还需要将 PWM_DAC_Set 函数加入 USMART 控制,方法前面 已经有详细的介绍了,大家这里自行添加,或者直接查看我们光盘的源码。 25.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK 战舰 STM32 开发板上,可以看 到 LCD 显示如图 25.4.1 所示: www.openedv.com 340 ALIENTEK 战舰STM32开发板 图 25.4.1 ADC 实验实际测试图 同时伴随 DS0 的不停闪烁,提示程序在运行。此时,我们通过按 WK_UP 按键,可以 看到输出电压增大,按 KEY1 则变小。 大家可以试试在 USMART 调用 PWM_DAC_Set 函数,来设置 PWM DAC 的输出电压, 如图 25.4.2 所示: www.openedv.com 341 ALIENTEK 战舰STM32开发板 图 25.4.2 通过 usmart 设置 PWM DAC 的电压输出 www.openedv.com 342 ALIENTEK 战舰STM32开发板 第二十六章 DMA 实验 本章我们将向大家介绍 STM32 的 DMA。在本章中,我们将利用 STM32 的 DMA 来实 现串口数据传送,并在 TFTLCD 模块上显示当前的传送进度。本章分为如下几个部分: 26.1 STM32 DMA 简介 26.2 硬件设计 26.3 软件设计 26.4 下载验证 www.openedv.com 343 ALIENTEK 战舰STM32开发板 26.1 STM32 DMA 简介 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 请求,可以通过设置相应的 外设寄存器中的控制位,被独立地开启或关闭。 表 26.1.1 是 DMA1 各通道一览表: www.openedv.com 344 ALIENTEK 战舰STM32开发板 表 26.1.1 DMA1 个通道一览表 这里解释一下上面说的逻辑或,例如通道 1 的几个 DMA1 请求(ADC1、TIM2_CH3、TIM4_CH1), 这几个是通过逻辑或到通道 1 的,这样我们在同一时间,就只能使用其中的一个。其他通道也 是类似的。 这里我们要使用的是串口 1 的 DMA 传送,也就是要用到通道 4。接下来,我们介绍一下 DMA 设置相关的几个寄存器。 第一个是 DMA 中断状态寄存器(DMA_ISR)。该寄存器的各位描述如图 26.1.1 所示: 图 26.1.1 DMA_ISR 寄存器各位描述 我们如果开启了 DMA_ISR 中这些中断,在达到条件后就会跳到中断服务函数里面去,即使 没开启,我们也可以通过查询这些位来获得当前 DMA 传输的状态。这里我们常用的是 TCIFx, 即通道 DMA 传输完成与否的标志。注意此寄存器为只读寄存器,所以在这些位被置位之后,只 能通过其他的操作来清除。 第二个是 DMA 中断标志清除寄存器(DMA_IFCR)。该寄存器的各位描述如图 26.1.2 所示: www.openedv.com 345 ALIENTEK 战舰STM32开发板 图 26.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 (表 26.1.1),接下来我们就介绍库函数下 DMA1 通道 4 的配置步骤: 1)使能 DMA 时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能 DMA 时钟 2)初始化 DMA 通道 4 参数 www.openedv.com 346 ALIENTEK 战舰STM32开发板 前面讲解过,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。 www.openedv.com 347 ALIENTEK 战舰STM32开发板 第九个参数 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 相关的库函数我们就讲解到这里,大家可以查看固件库中文手册详细了解。 www.openedv.com 348 ALIENTEK 战舰STM32开发板 26.2 硬件设计 所以本章用到的硬件资源有: 1) 指示灯 DS0 2) KEY0 按键 3) 串口 4) TFTLCD 模块 5) DMA 本章我们将利用外部按键 KEY0 来控制 DMA 的传送,每按一次 KEY0,DMA 就传送一次数据到 USART1,然后在 TFTLCD 模块上显示进度等信息。DS0 还是用来做为程序运行的指示灯。 本章实验需要注意 P6 口的 RXD 和 TXD 是否和 PA9 和 PA10 连接上,如果没有,请先连接。 26.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_InitStructure.DMA_MemoryBaseAddr = cmar; //DMA 外设 ADC 基地址 //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 通道拥有中优先级 www.openedv.com 349 ALIENTEK 战舰STM32开发板 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 函数如下: u8 SendBuff[5200]; const u8 TEXT_TO_SEND[]={"ALIENTEK Warship STM32 DMA 串口实验""}; int main(void) { u16 i; u8 t=0; u8 j,mask=0; float pro=0; delay_init(); NVIC_Configuration(); uart_init(9600); LED_Init(); LCD_Init(); KEY_Init(); //进度 //延时函数初始化 //设置 NVIC 中断分组 2 //串口初始化波特率为 9600 //LED 端口初始化 //初始化 LCD //按键初始化 MYDMA_Config(DMA1_Channel4,(u32)&USART1->DR,(u32)SendBuff,5168); //DMA1 通道 4,外设为串口 1,存储器为 SendBuff,长度 5168. POINT_COLOR=RED; //设置字体为红色 LCD_ShowString(60,50,200,16,16,"warship STM32"); LCD_ShowString(60,70,200,16,16,"DMA TEST"); LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(60,110,200,16,16,"2012/8/3"); LCD_ShowString(60,130,200,16,16,"KEY0:Start"); //显示提示信息 j=sizeof(TEXT_TO_SEND); for(i=0;i<5168;i++) { if(t>=j) //填充 ASCII 字符集数据 //加入换行符 { www.openedv.com 350 ALIENTEK 战舰STM32开发板 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==KEY_RIGHT) //KEY0 按下 { LCD_ShowString(60,150,200,16,16,"Start Transimit...."); LCD_ShowString(60,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(DMA2_FLAG_TC4)!=RESET) //判断传输完成 { DMA_ClearFlag(DMA2_FLAG_TC4); //清除通道 4 传输完成标志 break; } pro=DMA_GetCurrDataCounter(DMA1_Channel4);//得到当前剩余数据量 pro=1-pro/5168; //得到百分比 pro*=100; //扩大 100 倍 LCD_ShowNum(60,170,pro,3,16); } LCD_ShowNum(60,170,100,3,16); //显示 100% LCD_ShowString(60,150,200,16,16,"Transimit Finished!");//传送完成 } i++; delay_ms(10); if(i==20) www.openedv.com 351 ALIENTEK 战舰STM32开发板 { 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 串口传输的软件设计就完成了。 26.4 下载验证 在代码编译成功之后,我们通过串口下载代码到 ALIENTEK 战舰 STM32 开发板上,可以 看到 LCD 显示如图 26.4.1 所示: 图 26.4.1 DMA 串口实验实物测试图 伴随 DS0 的不停闪烁,提示程序在运行。我们打开串口调试助手,然后按 KEY0,可以看 到串口显示如图 26.4.2 所示的内容: www.openedv.com 352 ALIENTEK 战舰STM32开发板 图 26.4.2 串口收到的数据内容 上图中,接收到的数据,并没有进行换行,而实际上,我们的串口是发送了换行符的,这 是 SSCOM3.3 串口调试助手的又一个小 bug。大家可以换一个别的串口调试助手,一般都是 可以收到整齐的数据的。 同时可以看到 TFTLCD 上显示了进度等信息,如图 26.4.3 所示: www.openedv.com 353 ALIENTEK 战舰STM32开发板 26.4.3 DMA 串口数据传输中 至此,我们整个 DMA 串口实验就结束了,希望大家通过本章的学习,掌握 STM32 的 DMA 使用。DMA 是个非常好的功能,它不但能减轻 CPU 负担,还能提高数据传输速度,合理的应 用 DMA,往往能让你的程序设计变得简单。 www.openedv.com 354 ALIENTEK 战舰STM32开发板 第二十七章 IIC 实验 本章我们将向大家介绍如何利用 STM32 的普通 IO 口模拟 IIC 时序,并实现和 24C02 之间 的双向通信。在本章中,我们将利用 STM32 的普通 IO 口模拟 IIC 时序,来实现 24C02 的读写, 并将结果显示在 TFTLCD 模块上。本章分为如下几个部分: 27.1 IIC 简介 27.2 硬件设计 27.3 软件设计 27.4 下载验证 www.openedv.com 355 ALIENTEK 战舰STM32开发板 27.1 IIC 简介 IIC(Inter-Integrated Circuit)总线是一种由 PHILIPS 公司开发的两线式串行总线,用于连接 微控制器及其外围设备。它是由数据线 SDA 和时钟 SCL 构成的串行总线,可发送和接收数据。 在 CPU 与被控 IC 之间、IC 与 IC 之间进行双向传送,高速 IIC 总线一般可达 400kbps 以上。 I2C 总线在传送数据过程中共有三种类型信号, 它们分别是:开始信号、结束信号和应答 信号。 开始信号:SCL 为高电平时,SDA 由高电平向低电平跳变,开始传送数据。 结束信号:SCL 为高电平时,SDA 由低电平向高电平跳变,结束传送数据。 应答信号:接收数据的 IC 在接收到 8bit 数据后,向发送数据的 IC 发出特定的低电平脉冲, 表示已收到数据。CPU 向受控单元发出一个信号后,等待受控单元发出一个应答信号,CPU 接 收到应答信号后,根据实际情况作出是否继续传递信号的判断。若未收到应答信号,由判断为 受控单元出现故障。 这些信号中,起始信号是必需的,结束信号和应答信号,都可以不要。IIC 总线时序图如 图 27.1..1 所示: 图 27.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 提示程序正在运行。 27.2 硬件设计 本章需要用到的硬件资源有: 1) 指示灯 DS0 2) WK_UP 和 KEY1 按键 3) 串口(USMART 使用) 4) TFTLCD 模块 5) 24C02 www.openedv.com 356 ALIENTEK 战舰STM32开发板 前面 4 部分的资源,我们前面已经介绍了,请大家参考相关章节。这里只介绍 24C02 与 STM32 的连接,24C02 的 SCL 和 SDA 分别连在 STM32 的 PB10 和 PB11 上的,连接关系如图 27.2.1 所示: 图 27.2.1 STM32 与 24C02 连接图 27.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_10|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_10|GPIO_Pin_11); //PB10,PB11 输出高 } //产生 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); www.openedv.com 357 ALIENTEK 战舰STM32开发板 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 应答 www.openedv.com 358 ALIENTEK 战舰STM32开发板 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 www.openedv.com 359 ALIENTEK 战舰STM32开发板 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)); 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 :写入数据的目的地址 //发送写命令 //发送高地址 //发送器件地址 0XA0,写数据 //发送低地址 //进入接收模式 //产生一个停止条件 www.openedv.com 360 ALIENTEK 战舰STM32开发板 //DataToWrite:要写入的数据 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 的 www.openedv.com 372 ALIENTEK 战舰STM32开发板 波 特率设 置成了 最低 ( 36Mhz, 256 分频为 140.625KHz )。在外 部函数 里面 ,我们 通过 SPI2_SetSpeed 来设置 SPI2 的速度,而我们的数据发送和接收则是通过 SPI2_ReadWriteByte 函 数来实现的。SPI2_SetSpeed 函数我们是通过寄存器设置方式来实现的,因为固件库并没有提供 单独的设置分频系数的函数 ,当然,我们也可以勉强的调用 SPI_Init 初始化函数来实现分频系 数修改。要读懂这段代码,可以直接查找中文参考手册中 SPI 章节的寄存器 CR1 的描述即可。 下面我们打开 flash.c,里面编写的是与 W25Q64 操作相关的代码,由于篇幅所限,详细 代码,这里就不贴出了。我们仅介绍几个重要的函数,首先是 SPI_Flash_Read 函数,该函数用 于从 W25Q64 的指定地址读出指定长度的数据。其代码如下: //读取 SPI FLASH //在指定地址开始读取指定长度的数据 //pBuffer:数据存储区 //ReadAddr:开始读取的地址(24bit) //NumByteToRead:要读取的字节数(最大 65535) void SPI_Flash_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; //下一个扇区还是写不完 //下一个扇区可以写完了 } }; } 该函数可以在 W25Q64 的任意地址开始写入任意长度(必须不超过 W25Q64 的容量)的数 据。我们这里简单介绍一下思路:先获得首地址(WriteAddr)所在的扇区,并计算在扇区内的 偏移,然后判断要写入的数据长度是否超过本扇区所剩下的长度,如果不超过,再先看看是否 要擦除,如果不要,则直接写入数据即可,如果要则读出整个扇区,在偏移处开始写入指定长 度的数据,然后擦除这个扇区,再一次性写入。当所需要写入的数据长度超过一个扇区的长度 www.openedv.com 374 ALIENTEK 战舰STM32开发板 的时候,我们先按照前面的步骤把扇区剩余部分写完,再在新扇区内执行同样的操作,如此循 环,直到写入结束。 其他的代码就比较简单了,我们这里不介绍了。接着打开 flahs.h 文件可以看到,这里面就 定义了一些与 W25Q64 操作相关的命令(部分省略了),这些命令在 W25Q64 的数据手册上都 有详细的介绍,感兴趣的读者可以参考该数据手册,其他的就没啥好说的了。。最后,我们看看 main.c 里面代码如下: const u8 TEXT_Buffer[]={"Warship STM32 SPI TEST"}; #define SIZE sizeof(TEXT_Buffer) int main(void) { u8 key; u16 i=0; u8 datatemp[SIZE]; u32 FLASH_SIZE; delay_init(); //延时函数初始化 NVIC_Configuration(); //设置 NVIC 中断分组 2:2 位抢占优先级,2 位响应优先级 uart_init(9600); //串口初始化波特率为 9600 LED_Init(); //LED 端口初始化 LCD_Init(); //初始化 LCD KEY_Init(); //初始化 KEY SPI_Flash_Init(); //SPI FLASH 初始化 POINT_COLOR=RED; //设置字体为红色 LCD_ShowString(60,50,200,16,16,"warship STM32"); LCD_ShowString(60,70,200,16,16,"SPI TEST"); LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(60,110,200,16,16,"2012/8/5"); LCD_ShowString(60,130,200,16,16,"WKUP:Write KEY1:Read"); //显示提示信息 while(SPI_Flash_ReadID()!=W25Q64) //检测不到 W25Q64 { LCD_ShowString(60,150,200,16,16,"25Q64 Check Failed!"); delay_ms(500); LCD_ShowString(60,150,200,16,16,"Please Check! "); delay_ms(500); LED0=!LED0;//DS0 闪烁 } LCD_ShowString(60,150,200,16,16,"25Q64 Ready!"); FLASH_SIZE=8*1024*1024;//FLASH 大小为 8M 字节 POINT_COLOR=BLUE;//设置字体为蓝色 while(1) { key=KEY_Scan(0); if(key==KEY_UP) //KEY_UP 按下,写入 W25Q64 www.openedv.com 375 ALIENTEK 战舰STM32开发板 { LCD_Fill(0,170,239,319,WHITE); //清除半屏 LCD_ShowString(60,170,200,16,16,"Start Write W25Q64...."); SPI_Flash_Write((u8*)TEXT_Buffer,FLASH_SIZE-100,SIZE);//从倒数第 100 //个地址处开始,写入 SIZE 长度的数据 LCD_ShowString(60,170,200,16,16,"W25Q64 Write Finished!");//提示传送完成 } if(key==KEY_DOWN) //KEY_DOWN 按下,读取字符串并显示 { LCD_ShowString(60,170,200,16,16,"Start Read W25Q64.... "); SPI_Flash_Read(datatemp,FLASH_SIZE-100,SIZE);//从倒数第 100 个地址处 //开始,读出 SIZE 个字节 LCD_ShowString(60,170,200,16,16,"The Data Readed Is: ");//提示传送完成 LCD_ShowString(60,190,200,16,16,datatemp);//显示读到的字符串 } i++; delay_ms(10); if(i==20) { LED0=!LED0; //提示系统正在运行 i=0; } } } 这部分代码和 IIC 实验那部分代码大同小异,我们就不多说了,实现的功能就和 IIC 差不 多,不过此次写入和读出的是 SPI FLASH,而不是 EEPROM。 28.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK 战舰 STM32 开发板上,通过先按 WK_UP 按键写入数据,然后按 KEY1 读取数据,得到如图 28.4.1 所示: www.openedv.com 376 ALIENTEK 战舰STM32开发板 图 28.4.1 程序运行效果图 伴随 DS0 的不停闪烁,提示程序在运行。程序在开机的时候会检测 W25Q64 是否存在,如 果不存在则会在 TFTLCD 模块上显示错误信息,同时 DS0 慢闪。大家可以通过跳线帽把 PB12 和 PB13 短接就可以看到报错了。 www.openedv.com 377 ALIENTEK 战舰STM32开发板 第二十九章 485 实验 本章我们将向大家介绍如何利用 STM32 的串口实现 485 通信(半双工)。在本章中,我们 将利用 STM32 的串口 2 来实现两块开发板之间的 485 通信,并将结果显示在 TFTLCD 模块上。 本章分为如下几个部分: 29.1 485 简介 29.2 硬件设计 29.3 软件设计 29.4 下载验证 www.openedv.com 378 ALIENTEK 战舰STM32开发板 29.1 485 简介 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 推荐 的连接方式如图 29.1.2 所示: 图 29.1.2 RS485 连接 在上面的连接中,如果需要添加匹配电阻,我们一般在总线的起止端加入,也就是主机和 设备 4 上面各加一个 120Ω的匹配电阻。 由于 RS485 具有传输距离远、传输速度快、支持节点多和抗干扰能力更强等特点,所以 RS485 有很广泛的应用。 战舰 STM32 开发板采用 SP3485 作为收发器,该芯片支持 3.3V 供电,最大传输速度可达 10Mbps,支持多达 32 个节点,并且有输出短路保护。该芯片的框图如图 29.1.2 所示: 图 29.1.2 SP3485 框图 图中 A、B 总线接口,用于连接 485 总线。RO 是接收输出端,DI 是发送数据收入端,RE 是接收使能信号(低电平有效),DE 是发送使能信号(高电平有效)。 www.openedv.com 379 ALIENTEK 战舰STM32开发板 本章,我们通过该芯片连接 STM32 的串口 2,实现两个开发板之间的 485 通信。本章将实 现这样的功能:通过连接两个战舰 STM32 开发板的 RS485 接口,然后由 KEY0 控制发送,当 按下一个开发板的 KEY0 的时候,就发送 5 个数据给另外一个开发板,并在两个开发板上分别 显示发送的值和接收到的值。 本章,我们只需要配置好串口 2,就可以实现正常的 485 通信了,串口 2 的配置和串口 1 基本类似,只是串口的时钟来自 APB1,最大频率为 36Mhz。 29.2 硬件设计 本章要用到的硬件资源如下: 1) 指示灯 DS0 2) KEY0 按键 3) TFTLCD 模块 4) 串口 2 5) RS485 收发芯片 前面 3 个都有详细介绍,这里我们介绍 RS485 和串口 2 的连接关系,如图 29.2.1 所示: 图 29.2.1 STM32 与 SP3485 连接电路图 从上图可以看出:STM32 的串口 2 通过 P9 端口设置,连接到 SP3485,通过 STM32 的 PG9 控制 SP3485 的收发,当 PG9=0 的时候,为接收模式;当 PG9=1 的时候,为发送模式。这里注 意,我们要设置好开发板上 P9 排针的连接,通过跳线帽将 PA2 和 PA3 分别连接到 485T 和 485R 上面,如图 29.2.2 所示: www.openedv.com 380 ALIENTEK 战舰STM32开发板 图 29.2.2 硬件连接示意图 最后,我们用 2 根导线将两个开发板 RS485 端子的 A 和 A,B 和 B 连接起来。这里注意不 要接反了(A 接 B),接反了会导致通讯异常!! 29.3 软件设计 打开我们的 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 时钟 www.openedv.com 381 ALIENTEK 战舰STM32开发板 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 端口配置 //推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOA, &GPIO_InitStructure); //PA2 //复用推挽 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; //一般设置为 9600; 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; www.openedv.com 382 ALIENTEK 战舰STM32开发板 RS485_TX_EN=1; for(t=0;t5)key=5; //最大是 5 个数据. for(i=0;i=0xfff)return 1; return 0; www.openedv.com 410 ALIENTEK 战舰STM32开发板 } //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 通信的波特率和工作模式等,在该函数中,我们就是按 30.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) { 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_Configuration(); //设置 NVIC 中断分组 2:2 位抢占优先级,2 位响应优先级 uart_init(9600); //串口初始化波特率为 9600 LED_Init(); //初始化与 LED 连接的硬件接口 LCD_Init(); //初始化 LCD www.openedv.com 411 ALIENTEK 战舰STM32开发板 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(60,110,200,16,16,"2012/9/11"); 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==KEY_RIGHT) //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); //发送 8 个字节 if(res)LCD_ShowString(60+80,190,200,16,16,"Failed"); //提示发送失败 else LCD_ShowString(60+80,190,200,16,16,"OK "); //提示发送成功 }else if(key==KEY_UP) //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"); } www.openedv.com 412 ALIENTEK 战舰STM32开发板 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)//不合格 www.openedv.com 420 ALIENTEK 战舰STM32开发板 { 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; }//正确了 //对角线相等 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 的距离 www.openedv.com 421 ALIENTEK 战舰STM32开发板 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; CMD_RDY=0XD0; }else //X,Y 方向与屏幕相同 { CMD_RDX=0XD0; CMD_RDY=0X90; } continue; } POINT_COLOR=BLUE; LCD_Clear(WHITE);//清屏 www.openedv.com 422 ALIENTEK 战舰STM32开发板 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; 其中(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*480 和 320*240 的屏都可以兼容)。其他的函数我们这里就不多介绍了,保存 touch.c 文件,并把该 文件加入到 HARDWARE 组下。接下来打开 touch.h 文件,在该文件里面输入如下代码: www.openedv.com 423 ALIENTEK 战舰STM32开发板 #ifndef __TOUCH_H__ #define __TOUCH_H__ #include "sys.h" #define TP_PRES_DOWN 0x80 //触屏被按下 #define TP_CATH_PRES 0x40 //有按键按下了 //触摸屏控制器 typedef struct { u8 (*init)(void); u8 (*scan)(u8); void (*adjust)(void); //初始化触摸屏控制器 //扫描触摸屏.0,屏幕扫描;1,物理坐标; //触摸屏校准 u16 x0; //原始坐标(第一次按下时的坐标) 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 里面定义 #define PEN PFin(10) //PF10 INT #define DOUT PFin(8) //PF8 MISO #define TDIN PFout(9) //PF9 MOSI #define TCLK PBout(1) //PB1 SCLK #define TCS PBout(2) //PB2 CS void TP_Write_Byte(u8 num); u16 TP_Read_AD(u8 CMD); u16 TP_Read_XOY(u8 xy); u8 TP_Read_XY(u16 *x,u16 *y); u8 TP_Read_XY2(u16 *x,u16 *y); //向控制芯片写入一个数据 //读取 AD 转换值 //带滤波的坐标读取(X/Y) //双方向读取(X+Y) //带加强滤波的双方向坐标读取 void TP_Drow_Touch_Point(u16 x,u16 y,u16 color); //画一个坐标校准点 void TP_Draw_Big_Point(u16 x,u16 y,u16 color); //画一个大点 u8 TP_Scan(u8 tp); //扫描 www.openedv.com 424 ALIENTEK 战舰STM32开发板 void TP_Save_Adjdata(void); u8 TP_Get_Adjdata(void); //保存校准参数 //读取校准参数 void TP_Adjust(void); u8 TP_Init(void); //触摸屏校准 //初始化 void TP_Adj_Info_Show(u16 x0,u16 y0,u16 x1,u16 y1,u16 x2,u16 y2,u16 x3,u16 y3,u16 fac); //显示校准信息 #endif 上述代码,我们定义了_m_tp_dev 结构体,用于管理和记录触摸屏相关信息,在外部调用 的时候,我们一般直接调用 tp_dev 的相关成员函数/变量屏即可达到需要的效果。这样种设计简 化了接口,另外管理和维护也比较方便,大家可以效仿一下。其他部分我们不做多的介绍,最 后我们打开 main.c,修改代码如下: //清屏,重新装载对话界面 void Load_Drow_Dialog(void) { LCD_Clear(WHITE);//清屏 POINT_COLOR=BLUE;//设置字体为蓝色 LCD_ShowString(lcddev.width-24,0,200,16,16,"RST");//显示清屏区域 POINT_COLOR=RED;//设置画笔蓝色 } int main(void) { u8 key; u8 i=0; delay_init(); //延时函数初始化 NVIC_Configuration(); //设置 NVIC 中断分组 2:2 位抢占优先级,2 位响应优先级 uart_init(9600); //串口初始化波特率为 9600 LED_Init(); //LED 端口初始化 LCD_Init(); //LCD 初始化 KEY_Init(); //按键初始化 POINT_COLOR=RED;//设置字体为红色 LCD_ShowString(60,50,200,16,16,"WarShip STM32"); LCD_ShowString(60,70,200,16,16,"TOUCH TEST"); LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(60,110,200,16,16,"2012/9/11"); LCD_ShowString(60,130,200,16,16,"Press KEY0 to Adjust"); tp_dev.init(); delay_ms(1500); Load_Drow_Dialog(); while(1) { key=KEY_Scan(0); tp_dev.scan(0); if(tp_dev.sta&TP_PRES_DOWN) //触摸屏被按下 www.openedv.com 425 ALIENTEK 战舰STM32开发板 { if(tp_dev.x(lcddev.width-24)&&tp_dev.y<16)Load_Drow_Dialog();//清除 else TP_Draw_Big_Point(tp_dev.x,tp_dev.y,RED); // 画 图 } }else delay_ms(10); if(key==KEY_RIGHT) //没有按键按下的时候 //KEY_RIGHT 按下,则执行校准程序 { LCD_Clear(WHITE);//清屏 TP_Adjust(); //屏幕校准 TP_Save_Adjdata(); Load_Drow_Dialog(); } i++; if(i==20) { i=0; LED0=!LED0; } } } 此函数就实现了我们上面介绍的本章所要实现的功能。当然这里还用到我们之前写的 24CXX 的代码,用来保存和调用触摸屏的校准信息(在触摸屏校准函数和初始化函数里面)。 31.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK 战舰 STM32 开发板上,得到如图 31.4.1 所示: www.openedv.com 426 ALIENTEK 战舰STM32开发板 图 31.4.1 程序运行效果 如果已经校准过了,则在等待 1.5s 之后进入手写界面,同时 DS0 开始闪烁,界面如图 31.4.2 所示: 图 31.4.2 手写界面 此时,我们就可以在该界面下用笔或者手指输入信息了。如果没有校准过,则会自动进入 校准程序(当你发现精度不行的时候,也可以通过按 KEY0 进入校准程序),如图 31.4.3 所示, www.openedv.com 427 ALIENTEK 战舰STM32开发板 在校准完成之后自动进入手写界面。 图 31.4.3 校准界面 www.openedv.com 428 ALIENTEK 战舰STM32开发板 第三十二章 红外遥控实验 本章,我们将向大家介绍如何通过 STM32 来解码红外遥控器的信号。ALIENTK 战舰 STM32 开发板标配了红外接收头和一个很小巧的红外遥控器。在本章中,我们将利用 STM32 的输入 捕获功能,解码开发板标配的这个红外遥控器的编码信号,并将解码后的键值 TFTLCD 模块上 显示出来。本章分为如下几个部分: 32.1 红外遥控简介 32.2 硬件设计 32.3 软件设计 32.4 下载验证 www.openedv.com 429 ALIENTEK 战舰STM32开发板 32.1 红外遥控简介 红外遥控是一种无线、非接触控制技术,具有抗干扰能力强,信息传输可靠,功耗低,成 本低,易实现等显著优点,被诸多电子设备特别是家用电器广泛采用,并越来越多的应用到计 算机系统中。 由于红外线遥控不具有像无线电遥控那样穿过障碍物去控制被控对象的能力,所以,在设 计红外线遥控器时,不必要像无线电遥控器那样,每套(发射器和接收器)要有不同的遥控频率 或编码(否则,就会隔墙控制或干扰邻居的家用电器),所以同类产品的红外线遥控器,可以有 相同的遥控频率或编码,而不会出现遥控信号“串门”的情况。这对于大批量生产以及在家用 电器上普及红外线遥控提供了极大的方面。由于红外线为不可见光,因此对环境影响很小,再 由红外光波动波长远小于无线电波的波长,所以红外线遥控不会影响其他家用电器,也不会影 响临近的无线电设备。 红外遥控的编码目前广泛使用的是: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 位数据格式。按照低位在前,高位在后的顺序发送。采用反码是为了增加传输的可靠性(可 用于校验)。 我们遥控器的按键 2 按下时,从红外接收头端收到的波形如图 32.1.1 所示: 图 32.1.1 按键 2 所对应的红外波形 从图 32.1.1 中可以看到,其地址码为 0,控制码为 168。可以看到在 100ms 之后,我们还 收到了几个脉冲,这是 NEC 码规定的连发码(由 9ms 低电平+2.5m 高电平+0.56ms 低电平 +97.94ms 高电平组成),如果在一帧数据发送完毕之后,按键仍然没有放开,则发射重复码, 即连发码,可以通过统计连发码的次数来标记按键按下的长短/次数。 第十五章我们曾经介绍过利用输入捕获来测量高电平的脉宽,本章解码红外遥控信号,刚 好可以利用输入捕获的这个功能来实现遥控解码。关于输入捕获的介绍,请参考第十五章的内 容。 www.openedv.com 430 ALIENTEK 战舰STM32开发板 32.2 硬件设计 本实验采用定时器的输入捕获功能实现红外解码,本章实验功能简介:开机在 LCD 上显示 一些信息之后,即进入等待红外触发,如过接收到正确的红外信号,则解码,并在 LCD 上显示 键值和所代表的意义,以及按键次数等信息。同样我们也是用 LED0 来指示程序正在运行。 所要用到的硬件资源如下: 1) 指示灯 DS0 2) TFTLCD 模块(带触摸屏) 3) 红外接收头 4) 红外遥控器 前两个,在之前的实例已经介绍过了,遥控器属于外部器件,遥控接收头在板子上,与 MCU 的连接原理图如 32.2.1 所示: 图 32.2.1 红外遥控接收头与 STM32 的连接电路图 红外遥控接收头连接在 STM32 的 PB9(TIM4_CH4)上。硬件上不需要变动,只要程序将 TIM4_CH4 设计为输入捕获,然后将收到的脉冲信号解码就可以了。开发板配套的红外遥控器 外观如图 32.2.2 所示: www.openedv.com 图 32.2.2 红外遥控器 431 ALIENTEK 战舰STM32开发板 32.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 www.openedv.com 432 ALIENTEK 战舰STM32开发板 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 //下降沿捕获 { www.openedv.com 433 ALIENTEK 战舰STM32开发板 Dval=TIM_GetCapture4(TIM4); //读取 CCR1 也可以清 CC1IF 标志位 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_ClearFlag(TIM4,TIM_IT_Update|TIM_IT_CC4); } //处理红外键盘 //返回值: // 0,没有任何按键按下 //其他,按下的按键键值. u8 Remote_Scan(void) { u8 sta=0; u8 t1,t2; if(RmtSta&(1<<6))//得到一个按键的所有信息了 www.openedv.com 434 ALIENTEK 战舰STM32开发板 { 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_Configuration(); //设置 NVIC 中断分组 2:2 位抢占优先级,2 位响应优先级 uart_init(9600); //串口初始化波特率为 9600 LED_Init(); //LED 端口初始化 LCD_Init(); //LCD 初始化 KEY_Init(); //按键端口初始化 Remote_Init(); //红外接收初始化 POINT_COLOR=RED;//设置字体为红色 LCD_ShowString(60,50,200,16,16,"WarShip STM32"); LCD_ShowString(60,70,200,16,16,"REMOTE TEST"); www.openedv.com 435 ALIENTEK 战舰STM32开发板 LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(60,110,200,16,16,"2012/9/12"); LCD_ShowString(60,130,200,16,16,"KEYVAL:"); LCD_ShowString(60,150,200,16,16,"KEYCNT:"); LCD_ShowString(60,170,200,16,16,"SYMBOL:"); while(1) { key=Remote_Scan(); if(key) { LCD_ShowNum(116,130,key,3,16); //显示键值 LCD_ShowNum(116,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(116,170,116+8*8,170+16,WHITE); //清楚之前的显示 LCD_ShowString(116,170,200,16,16,str); //显示 SYMBOL }else delay_ms(10); t++; if(t==20) { t=0; LED0=!LED0; } www.openedv.com 436 ALIENTEK 战舰STM32开发板 } } 主函数 main 代码比较简单,进行一系列初始化后,根据扫描到的按键值来显示。至此,我 们的软件设计部分就结束了。 32.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK 战舰 STM32 开发板上,可以看到 LCD 显示如图 32.4.1 所示的内容: 图 32.4.1 程序运行时 LCD 显示内容 此时我们通过遥控器按下不同的按键,则可以看到 LCD 上显示了不同按键的键值以及按键 次数和对应的遥控器上的符号。如图 32.4.2 所示: www.openedv.com 437 ALIENTEK 战舰STM32开发板 图 32.4.2 解码成功 www.openedv.com 438 ALIENTEK 战舰STM32开发板 第三十三章 游戏手柄实验 相信 80 后小时候都有玩过 FC 游戏机(又称:红白机/小霸王游戏机),那是一代经典,给 我们的童年带了了无限乐趣。本章,我们将向大家介绍如何通过 STM32 来驱动 FC 游戏机手柄, 将 FC 游戏机的手柄作为战舰 STM32 开发板的输入设备(综合实验可以直接通过这个手柄来玩 FC 游戏)。 在本章中,我们将使用 STM32 驱动 FC 手柄,将手柄的按键键值等信息通过 TFTLCD 模 块显示出来。本章分为如下几个部分: 33.1 游戏手柄简介 33.2 硬件设计 33.3 软件设计 33.4 下载验证 www.openedv.com 439 ALIENTEK 战舰STM32开发板 33.1 游戏手柄简介 FC 游戏机曾今是一统天下(现在也还是很多人玩),红极一时,那时任天堂单是 FC 机的 主机的发售收入就超过全美国的电视台的收入的总和。本章,我们将使用 STM32 来驱动 FC 手柄,实现手柄控制信号的读取,我们先来了解一下 FC 手柄。 FC 手柄,大致可分为两种:一种手柄插口是 11 针的,一种是 9 针的。但 11 针的现在市 面上很少了(因为 11 针手柄是早期 FC 组装兼容机最主要的周边),现在几乎都是 9 针 FC 组 装手柄的天下,所以我们本章使用的是 9 针 FC 手柄,该手柄还有一个特点,就是可以直接和 DR9 的串口头对插!这样同开发板的连接就简单了。FC 手柄的外观如图 33.1.1 所示: 图 33.1.1 FC 手柄外观图 这种手柄一般有 10 个按键(实际是 8 个键值):上、下、左、右、Start、Select、A、B、A 连发、B 连发。这里的 A 和 A 连发是一个键值,而 B 和 B 连发也是一个键值,只是连发按键 当你一直按下的时候,会不停的发送(方便快速按键,比如发炮弹之类的功能)。 FC 手柄的控制电路,由 1 个 8 位并入串出的移位寄存器(CD4021),外加一个时基集成 电路(NE555,用于连发)构成。不过现在的手柄,为了节约成本,直接就在 PCB 上做绑定 了,所以你拆开手柄,一般是看不到里面有四四方方的 IC,而只有一个黑色的小点,所有电路 都集成到这个里面了,但是他们的控制和读取方法还是一样的。 9 针手柄的读取时序和接线图如图 33.1.2 所示: www.openedv.com 440 ALIENTEK 战舰STM32开发板 图 33.1.2 FC 手柄读取时序和接线图 从上图可看出,读取手柄按键值的信息十分简单:先 Latch(锁存键值),然后就得到了第 一个按键值(A),之后在 Clock 的作用下,依次读取其他按键的键值,总共 8 个按键键值。 有了以上了解,我们就可以通过 STM32 的 IO 来驱动 FC 手柄了。 33.2 硬件设计 本实验采用 STM32 的 3 个普通 IO 连接 FC 手柄的 Clock、Data 和 Latch 信号,本章实验功 能简介:在主函数不停的查询手柄输入,一旦检测到输入信号,则在 LCD 模块上面显示键值和 对应的按键符号。同样我们也是用 LED0 来指示程序正在运行。 所要用到的硬件资源如下: 1) 指示灯 DS0 2) TFTLCD 模块 3) FC 手柄接口(JOYPAD) 4) FC 手柄 前两个,在之前的实例已经介绍过了,FC 手柄属于外部器件。战舰 STM32 开发板板载了 一个 FC 手柄接口(就是一个 DR9 接头),该接口与 MCU 的连接原理图如 33.2.1 所示: www.openedv.com 441 ALIENTEK 战舰STM32开发板 图 33.2.1 FC 手柄接头与 STM32 的连接电路图 图中,JOY_PAD 就是用来连接 FC 手柄的,该接头采用标准的 DR9 座,战舰 STM32 开发 板上有 2 个 DR9 座,一个用来接 FC 手柄(有 JOY_PAD 字样,LCD 左上),另外一个用来接 RS232 串口(有 COM 字样,LCD 右上),这两个头千万不要接错!否则可能烧坏手柄或者烧 坏 STM32。 从上图我们知道,手柄的 CLK(Clock)、LAT(Latch)和 DAT(Data)分别连接在 STM32 的 PC12、PC8 和 PC9 上面,这里与 SDIO 部分信号线共用了,所以当使用 SDIO 的时候,就不 能使用 FC 手柄了。因为信号线都是直连的,所以我们在开发板上不需要做配置,只需要将 FC 手柄插入 JOY_PAD 插口即可。 开发板配套的手柄,见图 33.1.1。 www.openedv.com 442 ALIENTEK 战舰STM32开发板 33.3 软件设计 打开我们的游戏手柄实验工程,可以看到我们的工程中添加了 joypad.c 文件以及其头文件 joypad.h 文件。 打开 joypad.c 文件,代码如下: #include "joypad.h" //初始化手柄接口. void JOYPAD_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);//使能 PC 端口时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8|GPIO_Pin_12; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); GPIO_SetBits(GPIOC,GPIO_Pin_8|GPIO_Pin_12); //端口 //推挽输出 //初始化 GPIO //输出高 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入 GPIO_Init(GPIOC, &GPIO_InitStructure); //初始化 GPIO GPIO_SetBits(GPIOC,GPIO_Pin_9); //输出高 } //读取手柄按键值. //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) { u8 temp=0; u8 t; JOYPAD_LAT=1; //锁存当前状态 JOYPAD_LAT=0; for(t=0;t<8;t++) { temp<<=1; if(JOYPAD_DAT)temp|=0x01; //LOAD 之后,就得到第一个数据 JOYPAD_CLK=1; //每给一次脉冲,收到一个数据 www.openedv.com 443 ALIENTEK 战舰STM32开发板 JOYPAD_CLK=0; } return temp; } 该部分代码仅 2 个函数,都比较简单,JOYPAD_Init 函数用于初始化 IO,即把 PC8、PC9 和 PC12 设置为正确的状态,以便同 FC 手柄通信。另外一个函数 JOYPAD_Read 就是按照图 33.1.2 所示的时序读取 FC 手柄,该函数的返回值就是手柄的状态。 接下来打开 joypad.h 可以看到该文件里代码主要是定义位带操作实现 IO 控制,当然,你 也可以跟 LE 试验一样通过库函数设置。 最后我们看看 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_Configuration(); uart_init(9600); LED_Init(); //延时函数初始化 //设置 NVIC 中断分组 2:2 位抢占优先级,2 位响应优先级 //串口初始化波特率为 9600 //LED 端口初始化 LCD_Init(); JOYPAD_Init(); //LCD 初始化 //手柄初始化 POINT_COLOR=RED;//设置字体为红色 LCD_ShowString(60,50,200,16,16,"WarShip STM32"); LCD_ShowString(60,70,200,16,16,"JOYPAD TEST"); LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(60,110,200,16,16,"2012/9/12"); LCD_ShowString(60,130,200,16,16,"KEYVAL:"); LCD_ShowString(60,150,200,16,16,"SYMBOL:"); POINT_COLOR=BLUE;//设置字体为红色 while(1) { key=JOYPAD_Read(); if(key!=0XFF) { LCD_ShowNum(116,130,key,3,16);//显示键值 for(i=0;i<8;i++) { if((key&(1<=200)return 1; else retry=0; while (!DS18B20_DQ_IN&&retry<240) { retry++; 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:要写入的字节 www.openedv.com 464 ALIENTEK 战舰STM32开发板 void DS18B20_Write_Byte(u8 dat) { u8 j; u8 testb; DS18B20_IO_OUT();//SET PA0 OUTPUT; for (j=1;j<=8;j++) { 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 www.openedv.com 465 ALIENTEK 战舰STM32开发板 GPIO_SetBits(GPIOG,GPIO_Pin_11); //输出 1 DS18B20_Rst(); return DS18B20_Check(); } //从 ds18b20 得到温度值 //精度:0.1C //返回值:温度值 (-550~1250) short DS18B20_Get_Temp(void) { 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_Configuration(); uart_init(9600); //延时函数初始化 //设置 NVIC 中断分组 2:2 位抢占优先级,2 位响应优先级 //串口初始化波特率为 9600 www.openedv.com 466 ALIENTEK 战舰STM32开发板 LED_Init(); LCD_Init(); KEY_Init(); //LED 端口初始化 //LCD 初始化 //KEY 初始化 POINT_COLOR=RED;//设置字体为红色 LCD_ShowString(60,50,200,16,16,"WarShip STM32"); LCD_ShowString(60,70,200,16,16,"DS18B20 TEST"); LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(60,110,200,16,16,"2012/9/12"); while(DS18B20_Init()) //DS18B20 初始化 { LCD_ShowString(60,130,200,16,16,"DS18B20 Error"); delay_ms(200); LCD_Fill(60,130,239,130+16,WHITE); delay_ms(200); } LCD_ShowString(60,130,200,16,16,"DS18B20 OK"); POINT_COLOR=BLUE;//设置字体为蓝色 LCD_ShowString(60,150,200,16,16,"Temp: . C"); while(1) { if(t%10==0)//每 100ms 读取一次 { temperature=DS18B20_Get_Temp(); if(temperature<0) { LCD_ShowChar(60+40,150,'-',16,0); temperature=-temperature; }else LCD_ShowChar(60+40,150,' ',16,0); LCD_ShowNum(60+40+8,150,temperature/10,2,16); LCD_ShowNum(60+40+32,150,temperature%10,1,16); //显示负号 //转为正数 //去掉负号 //显示正数部分 //显示小数部分 } delay_ms(10); t++; if(t==20) { t=0; LED0=!LED0; } } } 主函数代码很简单,一系列初始化之后,就是每 100ms 读取一次 18B20 的值,然后转化为 www.openedv.com 467 ALIENTEK 战舰STM32开发板 温度后显示在 LCD 上。至此,我们本章的软件设计就结束了。 35.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK 战舰 STM32 开发板上,可以看到 LCD 显示开始显示当前的温度值(假定 DS18B20 已经接上去了),如图 35.4.1 所示: 图 35.4.1 DS18B20 读取到的温度值 该程序还可以读取并显示负温度值的,只是由于本人在广州,是没办法看到了(除非放到 冰箱),具备条件的读者可以测试一下。 www.openedv.com 468 ALIENTEK 战舰STM32开发板 第三十六章 DHT11 数字温湿度传感器实验 上一章,我们介绍了数字温度传感器 DS18B20 的使用,本章我们将介绍数字温湿度传感器 DHT11 的使用,该传感器不但能测温度,还能测湿度。本章我们将向大家介绍如何使用 STM32 来读取 DHT11 数字温湿度传感器,从而得到环境温度和湿度等信息,并把从温湿度值显示在 TFTLCD 模块上。本章分为如下几个部分: 36.1 DHT11 简介 36.2 硬件设计 36.3 软件设计 36.4 下载验证 www.openedv.com 469 ALIENTEK 战舰STM32开发板 36.1 DHT11 简介 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 的数据 由以上数据就可得到湿度和温度的值,计算方法: www.openedv.com 470 ALIENTEK 战舰STM32开发板 湿度= byte4 . byte3=45.0 (%RH) 温度= 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 所示: www.openedv.com 图 36.1.5 DHT11 数字‘1’时序 471 ALIENTEK 战舰STM32开发板 通过以上了解,我们就可以通过 STM32 来实现对 DHT11 的读取了。DHT11 的介绍就到这 里,更详细的介绍,请参考 DHT11 的数据手册。 36.2 硬件设计 由于开发板上标准配置是没有 DHT11 这个传感器的,只有接口,所以要做本章的实验, 大家必须找一个 DHT11 插在预留的 DHT11 接口上。 本章实验功能简介:开机的时候先检测是否有 DHT11 存在,如果没有,则提示错误。只 有在检测到 DHT11 之后才开始读取温湿度值,并显示在 LCD 上,如果发现了 DHT11,则程 序每隔 100ms 左右读取一次数据,并把温湿度显示在 LCD 上。同样我们也是用 DS0 来指示程 序正在运行。 所要用到的硬件资源如下: 1) 指示灯 DS0 2) TFTLCD 模块 3) DHT11 接口 4) DHT11 温湿度传感器 这些我们都已经介绍过了,DHT11 的接口和 DS18B20 的接口是共用一个的,不过 DHT11 有 4 条腿,需要把 U13 的 4 个接口都用上,将 DHT11 传感器插入到这个上面就可以通过 STM32 来读取温湿度值了。连接示意图如图 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) www.openedv.com 472 ALIENTEK 战舰STM32开发板 { 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 的回应 //返回 1:未检测到 DHT11 的存在 //返回 0:存在 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++; www.openedv.com 473 ALIENTEK 战舰STM32开发板 delay_us(1); } delay_us(40);//等待 40us if(DHT11_DQ_IN)return 1; else return 0; } //从 DHT11 读取一个字节 //返回值:读到的数据 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 的存在 www.openedv.com 474 ALIENTEK 战舰STM32开发板 //返回 1:不存在 //返回 0:存在 u8 DHT11_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOG, ENABLE); //使能 PG 端口时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11; 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); //PG11 端口配置 //推挽输出 //初始化 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_Configuration(); uart_init(9600); LED_Init(); LCD_Init(); //延时函数初始化 //设置 NVIC 中断分组 2:2 位抢占优先级,2 位响应优先级 //串口初始化波特率为 9600 //LED 端口初始化 //初始化 LCD KEY_Init(); //初始化 KEY POINT_COLOR=RED;//设置字体为红色 LCD_ShowString(60,50,200,16,16,"WarShip STM32"); LCD_ShowString(60,70,200,16,16,"DS18B20 TEST"); LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(60,110,200,16,16,"2012/9/12"); while(DHT11_Init()) //DHT11 初始化 { LCD_ShowString(60,130,200,16,16,"DHT11 Error"); delay_ms(200); LCD_Fill(60,130,239,130+16,WHITE); www.openedv.com 475 ALIENTEK 战舰STM32开发板 delay_ms(200); } LCD_ShowString(60,130,200,16,16,"DHT11 OK"); POINT_COLOR=BLUE;//设置字体为蓝色 LCD_ShowString(60,150,200,16,16,"Temp: C"); LCD_ShowString(60,170,200,16,16,"Humi: %"); while(1) { if(t%10==0)//每 100ms 读取一次 { DHT11_Read_Data(&temperature,&humidity); //读取温湿度值 LCD_ShowNum(60+40,150,temperature,2,16); //显示温度 LCD_ShowNum(60+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 所示: www.openedv.com 476 ALIENTEK 战舰STM32开发板 图 36.4.1 DHT11 读取到的温湿度值 至此,本章实验结束。大家可以将本章通过 DHT11 读取到的温度值,和前一章的通过 DS18B20 读取到的温度值对比一下,看看哪个更准确? www.openedv.com 477 ALIENTEK 战舰STM32开发板 第三十七章 无线通信实验 ALIENTKE 战舰 STM32 开发板带有一个 2.4G 无线模块(NRF24L01 模块)通信接口,采 用 8 脚插针方式与开发板连接。本章我们将以 NRF24L01 模块为例向大家介绍如何在 ALIENTEK 战舰 STM32 开发板上实现无线通信。在本章中,我们将使用两块战舰 STM32 开发 板,一块用于发送收据,另外一块用于接收,从而实现无线数据传输。本章分为如下几个部分: 37.1 NRF24L01 无线模块简介 37.2 硬件设计 37.3 软件设计 37.4 下载验证 www.openedv.com 478 ALIENTEK 战舰STM32开发板 37.1 NRF24L01 无线模块简介 NRF24L01 无线模块,采用的芯片是 NRF24L01,该芯片的主要特点如下: 1)2.4G 全球开放的 ISM 频段,免许可证使用。 2)最高工作速率 2Mbps,高校的 GFSK 调制,抗干扰能力强。 3)125 个可选的频道,满足多点通信和调频通信的需要。 4)内置 CRC 检错和点对多点的通信地址控制。 5)低工作电压(1.9~3.6V)。 6)可设置自动应答,确保数据可靠传输。 该芯片通过 SPI 与外部 MCU 通信,最大的 SPI 速度可以达到 10Mhz。本章我们用到的模 块是深圳云佳科技生产的 NRF24L01,该模块已经被很多公司大量使用,成熟度和稳定性都是 相当不错的。该模块的外形和引脚图如图 37.1.1 所示: 图 37.1.1 NRF24L01 无线模块外观引脚图 模块 VCC 脚的电压范围为 1.9~3.6V,建议不要超过 3.6V,否则可能烧坏模块,一般用 3.3V 电压比较合适。除了 VCC 和 GND 脚,其他引脚都可以和 5V 单片机的 IO 口直连,正是因为其 兼容 5V 单片机的 IO,故使用上具有很大优势。 关于 NRF24L01 的详细介绍,请参考 NRF24L01 的技术手册。 37.2 硬件设计 本章实验功能简介:开机的时候先检测 NRF24L01 模块是否存在,在检测到 NRF24L01 模块之后,根据 KEY0 和 KEY1 的设置来决定模块的工作模式,在设定好工作模式之后,就会 不停的发送/接收数据,同样用 DS0 来指示程序正在运行。 所要用到的硬件资源如下: 1) 指示灯 DS0 2) KEY0 和 KEY1 按键 3) TFTLCD 模块 4) NRF24L01 接口 5) NRF24L01 模块 NRF24L01 模块属于外部模块,这里我们仅介绍 NRF24L01 接口和 STM32 的连接情况,他 们的连接关系如图 37.2.1 所示: www.openedv.com 479 ALIENTEK 战舰STM32开发板 图 37.2.1 NRF24L01 模块接口与 STM32 连接原理图 这里NRF24L01也是使用的SPI2,和W25Q64以及SD卡等共用一个SPI接口,所以在使用 的时候,他们分时复用SPI2。本章我们需要把SD卡和W25Q64的片选信号置高,以防止这两个 器件对NRF24L01的通信造成干扰。 由于无线通信实验是双向的,所以至少要有两个模块同时能工作,这里我们使用2套 ALIENTEK战舰STM32开发板来向大家演示。 37.3 软件设计 打开我们的无线通信实验项目工程,可以看到我们加入了 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_GPIOD| RCC_APB2Periph_GPIOG, ENABLE); //使能 PB,D,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);//上拉 www.openedv.com 480 ALIENTEK 战舰STM32开发板 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; //PD2 上拉 禁止 SD 卡的干扰 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 GPIO_SetBits(GPIOD,GPIO_Pin_2); //初始化指定 IO GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7; //PG6 7 推挽 GPIO_Init(GPIOG, &GPIO_InitStructure); //初始化指定 IO GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; GPIO_Init(GPIOG, &GPIO_InitStructure); //PG8 输入 //初始化指定 IO 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 个时钟沿 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; NRF24L01_CSN=1; //使能 24L01 //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); //读出写入的地址 www.openedv.com 481 ALIENTEK 战舰STM32开发板 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; //使能 SPI 传输 status =SPI2_ReadWriteByte(reg); //发送寄存器号 SPI2_ReadWriteByte(value); //写入寄存器的值 NRF24L01_CSN=1; //禁止 SPI 传输 return(status); //返回状态值 } //读取 SPI 寄存器值 //reg:要读的寄存器 u8 NRF24L01_Read_Reg(u8 reg) { u8 reg_val; NRF24L01_CSN = 0; //使能 SPI 传输 SPI2_ReadWriteByte(reg); //发送寄存器号 reg_val=SPI2_ReadWriteByte(0XFF); //读取寄存器内容 NRF24L01_CSN = 1; //禁止 SPI 传输 return(reg_val); //返回状态值 } //在指定位置读出指定长度的数据 //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_ShowString(60,170,239,32,16,"Send Failed "); LCD_Fill(0,188,240,218,WHITE);//清空上面的显示 }; LED0=!LED0; delay_ms(1500); }; www.openedv.com 487 ALIENTEK 战舰STM32开发板 } } 主函数显示进行一系列外设初始化,然后 check 无线模块,检测通过后,通过按键扫描检 测按键来决定是进入发送还是接受模式。至此,我们整个实验的软件设计就完成了。 37.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK 战舰 STM32 开发板上,可以看到 LCD 显示如图 37.4.1 所示的内容(默认 NRF24L01 已经接上了): 图 37.4.1 选择工作模式界面 通过 KEY0 和 KEY1 来选择 NRF24L01 模块所要进入的工作模式,我们两个开发板一个选 择发送,一个选择接收就可以了。 设置好后通信界面如图 37.4.2 所示: www.openedv.com 488 ALIENTEK 战舰STM32开发板 图 37.4.2 通信界面 www.openedv.com 489 ALIENTEK 战舰STM32开发板 第三十八章 PS2 鼠标实验 PS/2 作为电脑的标准输入接口,用于鼠标键盘等设备。PS/2 只需要一个简单的接口(2 个 IO 口),就可以外扩鼠标、键盘等,是单片机理想的输入外扩方式。 ALIENTEK 战舰 STM32 开发板也自带了一个 PS/2 接口,可以用来驱动标准的鼠标、键盘 等外设,也可以用来驱动一些 PS/2 接口的小键盘,条码扫描枪等。在本章中,我们将向大家介 绍,如何在 ALIENTEK 战舰 STM32 开发板上,通过 PS/2 接口来驱动电脑鼠标。本章分为如下 几个部分: 38.1 PS/2 简介 38.2 硬件设计 38.3 软件设计 38.4 下载验证 www.openedv.com 490 ALIENTEK 战舰STM32开发板 38.1 PS/2 简介 PS/2 是电脑上常见的接口之一,用于鼠标、键盘等设备。一般情况下,PS/2 接口的鼠标为 绿色,键盘为紫色。 PS/2 接口是输入装置接口,而不是传输接口。所以 PS2 口根本没有传输速率的概念,只有 扫描速率。在 Windows 环境下,ps/2 鼠标的采样率默认为 60 次/秒,USB 鼠标的采样率为 120 次/秒。较高的采样率理论上可以提高鼠标的移动精度。 物理上的 PS/2 端口可有 2 种,一种是 5 脚的,一种是六脚的。下面给出这两种 PS/2 接口 的引脚定义图,如图 38.1.1 所示: 图 38.1.1 PS/2 引脚定义图 从图 38.1.1 可以看出,不管是 5 脚还是 6 脚的 PS/2 接头,都是有 4 根有用的线连接:时钟 线、数据线、电源线、地线。PS/2 设备的电源是 5V 的,而数据线和时钟线均是集电极开路的, 这两根信号线都需要接一个上拉电阻(开发板上使用的是 10K)。 PS/2 鼠标和键盘遵循一种双向同步串行协议,换句话说每次数据线上发送一位数据并且每 在时钟线上发一个脉冲就被读入。键盘/鼠标可以发送数据到主机,而主机也可以发送数据到设 备,但主机总是在总线上有优先权,它可以在任何时候抑制来自于键盘/鼠标的通讯,只要把时 钟拉低即可。 从设备到主机的数据在时钟信号的下降沿被主机读取,而从主机到设备的数据在时钟信号 的上升沿被设备读取。不论通信方向如何,时钟总是由设备产生的,最大的时钟频率为 33Khz, 大多数设备工作在 10~20Khz。 鼠标键盘,采用的是一种每帧包含 11/12 位的串行协议,这些位的含义如表 38.1.1 所示: 表 38.1.2 鼠标/键盘帧数据格式 表 38.1.2 中校验位的含义是:如果数据位中包含偶数个 1,则校验位为 1;如果数据位中 包含奇数个 1,则校验位为 0。数据位中的 1 的个数加上校验位总为奇数(奇校验),用于数据 侦错。当主机发送数据给键盘/鼠标的时候,设备会发送一个握手信号来应答数据已经被收到了, www.openedv.com 491 ALIENTEK 战舰STM32开发板 该位不会出现在设备到主机的通信中。 设备到主机的通信过程: 正常情况下数据线和时钟线都是高电平,当键盘/鼠标有数据要发送时,它先检测时钟线, 确认时钟线是高电平。如果不是,则是主机抑制了通信,设备必须缓冲任何要发送的数据,直 到重新获得总线的控制权(键盘有 16 字节的缓冲区而鼠标的缓冲区仅存储最后一个要发送的 数据包)。如果时钟线是高电平,设备就可以开始传送数据了。 设备到主机的数据在时钟线的下降沿被主机读入,如图 38.1.2 所示: 图 38.1.2 设备到主机通信时序图 主机可以在设备发送数据的时候拉低时钟线来来放弃当前数据的传送。 主机到设备的通信过程: 主机到设备的通信与设备到主机的通信有点不同,因为 PS/2 的时钟总是由设备产生的,如 果主机要发送数据,则它必须首先把时钟线和数据线设置为请求发送状态。请求发送状态通过 如下过程实现: 1.拉低时钟线至少 100us 以抑制通信。 2.拉低数据线,以应用“请求发送”,然后释放时钟线。 设备在不超过 10ms 的时间内就会检测这个状态,当设备检测到这个状态后,它将开始产生 时钟信号,并且在设备提供的时钟脉冲驱动下输入八个数据位和一个停止位。主机仅当时钟线 为低的时候改变数据线,而数据在时钟脉冲的上升沿被锁存,这与发生在设备到主机通讯的过 程中正好相反。 主机到设备的通信时序图如图 38.1.3 所示: 图 38.1.3 主机到设备通信时序图 以上简单介绍了 PS/2 协议的通信过程,更多的介绍请参考《PS/2 技术参考》一文。本章 我们要驱动一个 PS/2 鼠标,所以接下来简单介绍一下 PS/2 鼠标的相关信息。 标准的 PS/2 鼠标支持下面的输入:X(左右)位移、Y(上下)位移、左键、中键和右键。 但是我们目前用到鼠标大都还有滚轮,有的还有更多的按键,这就是所谓的 Intellimouse。它支 持 5 个鼠标按键和三个位移轴(左右、上下和滚轮)。 标准的鼠标有两个计数器保持位移的跟踪:X 位移计数器和 Y 位移计数器。可存放 9 位 的 2 进制补码,并且每个计数器都有相关的溢出标志。它们的内容连同三个鼠标按钮的状态一 起以三字节移动数据包的形式发送给主机,位移计数器表示从最后一次位移数据包被送往主机 后所发生的位移量。 www.openedv.com 492 ALIENTEK 战舰STM32开发板 标准 PS/2 鼠标发送唯一和按键信息以 3 字节的数据包格式发给主机,三个数据包的意义如 图 38.1.4 所示: 图 38.1.4 标准鼠标位移数据包格式 位移计数器是一个 9 位 2 的补码整数,其最高位作为符号位出现在位移数据包的第一个字 节里。这些计数器在鼠标读取输入发现有位移时被更新。这些值是自从最后一次发送位移数据 包给主机后位移的累计量(即最后一次包发给主机后位移计数器被复位位移计数器可表示的值 的范围是-255 到+255)。如果超过了范围,相应的溢出位就会被置位,并在复位之前,计数器 不会再增减。 而所谓的 Intellimouse,因为多了 2 个按键和一个滚轮,所以 Intellimouse 的一个位移数据 包由 4 个字节组成,如图 38.1.5 所示: 图 38.1.5 Intellimouse 鼠标位移数据包格式 Z0-Z3 是 2 的补码,用于表示从上次数据报告以来滚轮的位移量。有效范围从-8 到+7,第 四键如果按下,则 4th Btn 位被置位,如果没有按下,则 4th Btn 位为 0。第五键也与此类似。 鼠标的介绍我们就简单的介绍到这里,详细的说明请参考光盘《PS/2 技术参考》第三章 PS/2 鼠标接口(第 36 页)。 38.2 硬件设计 本章实验功能简介:开机的时候先检测是否有鼠标接入,如果没有/检测错误,则提示错误 代码。只有在检测到 PS/2 鼠标之后才开始后续操作,当检测到鼠标之后,就在 LCD 上显示鼠 标位移数据包的内容,并转换为坐标值,在 LCD 上显示,如果有按键按下,则会提示按下的是 哪个按键。同样我们也是用 LED0 来指示程序正在运行。 所要用到的硬件资源如下: 1) 指示灯 DS0 2) TFTLCD 模块 3) PS/2 鼠标/键盘接口 4) PS/2 鼠标 本章需要用到一个PS/2接口的鼠标,大家得自备一个。下面我来看一看开发板PS/2接口与 STM32的连接电路,如图38.2.1所示: www.openedv.com 493 ALIENTEK 战舰STM32开发板 图 38.2.1 PS/2 接口与 STM32 的连接电路图 可以看到,PS/2 接口与 STM32 的连接仅仅 2 个 IO 口,其中 PS_CLK 连接在 PC11 上面, 而 PS_DAT 则连接在 PC10 上面,这两个口和 SDIO_D2 和 SDIO_D3 复用了,所以在 SDIO 使 用的时候,就不能使用 PS/2 设备了,这个在使用的时候大家要注意一下。 38.3 软件设计 打开 PS2 鼠标实验的工程,可以看到我们在工程中新建了 ps2.c 和头文件 ps2.h 以及 mouse.c 和 mouse.h 头文件。 打开 ps2.c,代码如下: #include "ps2.h" #include "usart.h" //PS2 产生的时钟频率在 10~20Khz(最大 33K) //高/低电平的持续时间为 25~50us 之间. //PS2_Status 当前状态标志 //[7]:接收到一次数据;[6]:校验错误;[5:4]:当前工作的模式;[3:0]:收到的数据长度; u8 PS2_Status=CMDMODE; //默认为命令模式 u8 PS2_DATA_BUF[16]; //ps2 数据缓存区 //位计数器 u8 BIT_Count=0; //中断 15~10 处理函数 //每 11 个 bit,为接收 1 个字节 //每接收完一个包(11 位)后,设备至少会等待 50ms 再发送下一个包 //只做了鼠标部分,键盘部分暂时未加入 //CHECK OK 2010/5/2 EXTI_InitTypeDef EXTI_InitStructure; void EXTI15_10_IRQHandler(void) { static u8 tempdata=0; static u8 parity=0; if(EXTI_GetITStatus(EXTI_Line11)==SET) //中断 11 产生了相应的中断 www.openedv.com 494 ALIENTEK 战舰STM32开发板 { EXTI_ClearITPendingBit(EXTI_Line11); //清除 LINE11 上的中断标志位 if(BIT_Count==0) { parity=0; tempdata=0; } BIT_Count++; if(BIT_Count>1&&BIT_Count<10)//这里获得数据 { tempdata>>=1; if(PS2_SDA) { tempdata|=0x80; parity++;//记录 1 的个数 } }else if(BIT_Count==10)//得到校验位 { if(PS2_SDA)parity|=0x80;//校验位为 1 } if(BIT_Count==11)//接收到 1 个字节的数据了 { BIT_Count=parity&0x7f;//取得 1 的个数 if(((BIT_Count%2==0)&&(parity&0x80))||((BIT_Count%2==1)&&(parity&0x80)==0)) { //PS2_Status|=1<<7;//标记得到数据 BIT_Count=PS2_Status&0x0f; PS2_DATA_BUF[BIT_Count]=tempdata;//保存数据 if(BIT_Count<15)PS2_Status++; //数据长度加 1 BIT_Count=PS2_Status&0x30; //得到模式 switch(BIT_Count) { case CMDMODE://命令模式下,每收到一个字节都会产生接收完成 PS2_Dis_Data_Report();//禁止数据传输 PS2_Status|=1<<7; //标记得到数据 break; case KEYBOARD: break; case MOUSE: if(MOUSE_ID==0)//标准鼠标,3 个字节 { if((PS2_Status&0x0f)==3) { PS2_Status|=1<<7; //标记得到数据 www.openedv.com 495 ALIENTEK 战舰STM32开发板 PS2_Dis_Data_Report();//禁止数据传输 } }else if(MOUSE_ID==3)//扩展鼠标,4 个字节 { if((PS2_Status&0x0f)==4) { PS2_Status|=1<<7; //标记得到数据 PS2_Dis_Data_Report();//禁止数据传输 } } break; } }else { PS2_Status|=1<<6; //标记校验错误 PS2_Status&=0xf0; //清除接收数据计数器 } BIT_Count=0; } } } //禁止数据传输 //把时钟线拉低,禁止数据传输 //CHECK OK 2010/5/2 void PS2_Dis_Data_Report(void) { PS2_Set_Int(0); //关闭中断 PS2_SET_SCL_OUT(); //设置 SCL 为输出 PS2_SCL_OUT=0; //抑制传输 } //使能数据传输 //释放时钟线 //CHECK OK 2010/5/2 void PS2_En_Data_Report(void) { PS2_SET_SCL_IN(); //设置 SCL 为输入 PS2_SET_SDA_IN(); //SDA IN PS2_SCL_OUT=1; //上拉 PS2_SDA_OUT=1; PS2_Set_Int(1); //开启中断 } //PS2 中断屏蔽设置 www.openedv.com 496 ALIENTEK 战舰STM32开发板 //en:1,开启;0,关闭; //CHECK OK 2010/5/2 void PS2_Set_Int(u8 en) { EXTI_ClearITPendingBit(EXTI_Line11); //清除 EXTI11 线路挂起位 if(en) { EXTI_InitStructure.EXTI_LineCmd = ENABLE; }else{ EXTI_InitStructure.EXTI_LineCmd = DISABLE; } EXTI_Init(&EXTI_InitStructure); //根据指定的参数初始化 EXTI } //等待 PS2 时钟线 sta 状态改变 //sta:1,等待变为 1;0,等待变为 0; //返回值:0,时钟线变成了 sta;1,超时溢出; //CHECK OK 2010/5/2 u8 Wait_PS2_Scl(u8 sta) { u16 t=0; sta=!sta; while(PS2_SCL==sta) { delay_us(1); t++; if(t>16000)return 1;//时间溢出 (设备会在 10ms 内检测这个状态) } return 0;//被拉低了 } //在发送命令/数据之后,等待设备应带,该函数用来获取应答 //返回得到的值 //返回 0,且 PS2_Status.6=1,则产生了错误 //CHECK OK 2010/5/2 u8 PS2_Get_Byte(void) { u16 t=0; u8 temp=0; while(1)//最大等待 55ms { t++; delay_us(10); www.openedv.com 497 ALIENTEK 战舰STM32开发板 if(PS2_Status&0x80) //得到了一次数据 { temp=PS2_DATA_BUF[PS2_Status&0x0f-1]; PS2_Status&=0x70;//清除计数器,接收到数据标记 break; }else if(t>5500||PS2_Status&0x40)break;//超时溢出/接收错误 } PS2_En_Data_Report(); //使能数据传输 return temp; } //发送一个命令到 PS2. //返回值:0,无错误,其他,错误代码 u8 PS2_Send_Cmd(u8 cmd) { u8 i; u8 high=0; //记录 1 的个数 PS2_Set_Int(0); //屏蔽中断 PS2_SET_SCL_OUT(); //设置 SCL 为输出 PS2_SET_SDA_OUT(); //SDA OUT PS2_SCL_OUT=0; //拉低时钟线 delay_us(120); //保持至少 100us PS2_SDA_OUT=0; //拉低数据线 delay_us(10); PS2_SET_SCL_IN(); //释放时钟线,这里 PS2 设备得到第一个位,开始位 PS2_SCL_OUT=1; if(Wait_PS2_Scl(0)==0) //等待时钟拉低 { for(i=0;i<8;i++) { if(cmd&0x01) { PS2_SDA_OUT=1; high++; }else PS2_SDA_OUT=0; cmd>>=1; //这些地方没有检测错误,因为这些地方不会产生死循环 Wait_PS2_Scl(1);//等待时钟拉高 发送 8 个位 Wait_PS2_Scl(0);//等待时钟拉低 } if((high%2)==0)PS2_SDA_OUT=1;//发送校验位 10 else PS2_SDA_OUT=0; Wait_PS2_Scl(1); //等待时钟拉高 10 位 Wait_PS2_Scl(0); //等待时钟拉低 www.openedv.com 498 ALIENTEK 战舰STM32开发板 PS2_SDA_OUT=1; //发送停止位 11 Wait_PS2_Scl(1); //等待时钟拉高 11 位 PS2_SET_SDA_IN(); //SDA in Wait_PS2_Scl(0); //等待时钟拉低 if(PS2_SDA==0)Wait_PS2_Scl(1);//等待时钟拉高 12 位 else { PS2_En_Data_Report(); return 1;//发送失败 } }else { PS2_En_Data_Report(); return 2;//发送失败 } PS2_En_Data_Report(); return 0; //发送成功 } //PS2 初始化 //CHECK OK 2010/5/2 void PS2_Init(void) { NVIC_InitTypeDef NVIC_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); //使能 PC 端口时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10|GPIO_Pin_11; //PC10,11 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); //初始化指定端口 GPIO_EXTILineConfig(GPIO_PortSourceGPIOC,GPIO_PinSource11); //中断线 11 //中断线初始化 EXTI_InitStructure.EXTI_Line=EXTI_Line11; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //中断 EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; //上升沿出发 EXTI_InitStructure.EXTI_LineCmd = ENABLE; //使能中断线 EXTI_Init(&EXTI_InitStructure); //根据指定的参数初始化 EXTI //中断分组初始化 NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;//使能按键所在的中断通道 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //抢占优先级 1 www.openedv.com 499 ALIENTEK 战舰STM32开发板 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; //子优先级 2 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道 NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化外设 NVIC 寄存器 } 该部分为底层的 PS/2 协议驱动程序,采用中断接收 PS/2 设备产生的时钟信号,然后解析。 PS2_Init 函数主要是进行一些 GPIO 初始化,中断线映射以及中断分组设置。这里相信大家已 经很熟悉这段代码了。头文件 ps2.h 中的代码很简单,相信大家都看得懂,这里不拿出来介绍 了。然后打开 mouse.c,代码如下: #include "mouse.h" #include "usart.h" #include "lcd.h" u8 MOUSE_ID;//用来标记鼠标 ID PS2_Mouse MouseX; //处理 MOUSE 的数据 void Mouse_Data_Pro(void) { MouseX.x_pos+=(signed char)PS2_DATA_BUF[1]; MouseX.y_pos+=(signed char)PS2_DATA_BUF[2]; MouseX.z_pos+=(signed char)PS2_DATA_BUF[3]; MouseX.bt_mask=PS2_DATA_BUF[0]&0X07;//取出掩码 } //初始化鼠标 //返回:0,初始化成功 //其他:错误代码 //CHECK OK 2010/5/2 u8 Init_Mouse(void) { u8 t; PS2_Init(); delay_ms(800); //等待上电复位完成 PS2_Status=CMDMODE; //进入命令模式 t=PS2_Send_Cmd(PS_RESET); //复位鼠标 if(t!=0)return 1; t=PS2_Get_Byte(); if(t!=0XFA)return 2; t=0; while((PS2_Status&0x80)==0)//等待复位完毕 { t++; delay_ms(10); if(t>50)return 3; } www.openedv.com 500 ALIENTEK 战舰STM32开发板 PS2_Get_Byte();//得到 0XAA PS2_Get_Byte();//得到 ID 0X00 //进入滚轮模式的特殊初始化序列 PS2_Send_Cmd(SET_SAMPLE_RATE);//进入设置采样率 if(PS2_Get_Byte()!=0XFA)return 4;//传输失败 PS2_Send_Cmd(0XC8);//采样率 200 if(PS2_Get_Byte()!=0XFA)return 5;//传输失败 PS2_Send_Cmd(SET_SAMPLE_RATE);//进入设置采样率 if(PS2_Get_Byte()!=0XFA)return 6;//传输失败 PS2_Send_Cmd(0X64);//采样率 100 if(PS2_Get_Byte()!=0XFA)return 7;//传输失败 PS2_Send_Cmd(SET_SAMPLE_RATE);//进入设置采样率 if(PS2_Get_Byte()!=0XFA)return 8;//传输失败 PS2_Send_Cmd(0X50);//采样率 80 if(PS2_Get_Byte()!=0XFA)return 9;//传输失败 //序列完成 PS2_Send_Cmd(GET_DEVICE_ID); //读取 ID if(PS2_Get_Byte()!=0XFA)return 10;//传输失败 MOUSE_ID=PS2_Get_Byte();//得到 MOUSE ID PS2_Send_Cmd(SET_SAMPLE_RATE);//再次进入设置采样率 if(PS2_Get_Byte()!=0XFA)return 11; //传输失败 PS2_Send_Cmd(0X0A); //采样率 10 if(PS2_Get_Byte()!=0XFA)return 12; //传输失败 PS2_Send_Cmd(GET_DEVICE_ID); //读取 ID if(PS2_Get_Byte()!=0XFA)return 13; //传输失败 MOUSE_ID=PS2_Get_Byte() ; //得到 MOUSE ID PS2_Send_Cmd(SET_RESOLUTION); //设置分辨率 if(PS2_Get_Byte()!=0XFA)return 14; //传输失败 PS2_Send_Cmd(0X03);//8 点/mm if(PS2_Get_Byte()!=0XFA)return 15; //传输失败 PS2_Send_Cmd(SET_SCALING11); //设置缩放比率为 1:1 if(PS2_Get_Byte()!=0XFA)return 16; //传输失败 PS2_Send_Cmd(SET_SAMPLE_RATE); //设置采样率 if(PS2_Get_Byte()!=0XFA)return 17; //传输失败 PS2_Send_Cmd(0X28);//40 if(PS2_Get_Byte()!=0XFA)return 18; //传输失败 PS2_Send_Cmd(EN_DATA_REPORT); //使能数据报告 if(PS2_Get_Byte()!=0XFA)return 19; //传输失败 www.openedv.com 501 ALIENTEK 战舰STM32开发板 PS2_Status=MOUSE; //进入鼠标模式 return 0; //无错误,初始化成功 } 该部分仅 2 个函数,Init_Mouse 用于初始化鼠标,让鼠标进入 Intellimouse 模式,里面的初 始化序列完全按照《PS/2 技术参考》里面介绍的来设计。另外一个函数就是将收到的数据简单 处理一下。 接下来打开 mouse.h 可以看到我们定义了一个鼠标结构体: typedef struct { short x_pos; //横坐标 short y_pos; //纵坐标 short z_pos; //滚轮坐标 u8 bt_mask; //按键标识,bit2 中间键;bit1,右键;bit0,左键 } PS2_Mouse; 该鼠标结构体用于存放鼠标相关的数据,并对鼠标的相关命令进行了宏定义(部分被省略)。 最后,打开 main.c 文件, 代码如下: //显示鼠标的坐标值 //x,y:在 LCD 上显示的坐标位置 //pos:坐标值 void Mouse_Show_Pos(u16 x,u16 y,short pos) { if(pos<0) { LCD_ShowChar(x,y,'-',16,0); //显示负号 pos=-pos; //转为正数 }else LCD_ShowChar(x,y,' ',16,0); //去掉负号 LCD_ShowNum(x+8,y,pos,5,16); //显示值 } int main(void) { u8 t; u8 errcnt=0; delay_init(); //延时函数初始化 NVIC_Configuration(); //设置 NVIC 中断分组 2:2 位抢占优先级,2 位响应优先级 uart_init(9600); //串口初始化波特率为 9600 LED_Init(); //LED 端口初始化 LCD_Init(); //初始化 LCD POINT_COLOR=RED;//设置字体为红色 LCD_ShowString(60,50,200,16,16,"WarShip STM32"); LCD_ShowString(60,70,200,16,16,"Mouse TEST"); LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(60,110,200,16,16,"2012/9/13"); www.openedv.com 502 ALIENTEK 战舰STM32开发板 while(Init_Mouse()) //检查鼠标是否在位. { LCD_ShowString(60,130,200,16,16,"Mouse Error"); delay_ms(400); LCD_Fill(60,130,239,130+16,WHITE); delay_ms(100); } LCD_ShowString(60,130,200,16,16,"Mouse OK"); LCD_ShowString(60,150,200,16,16,"Mouse ID:"); LCD_ShowNum(132,150,MOUSE_ID,3,16);//填充模式 POINT_COLOR=BLUE; LCD_ShowString(30,170,200,16,16,"BUF[0]:"); LCD_ShowString(30,186,200,16,16,"BUF[1]:"); LCD_ShowString(30,202,200,16,16,"BUF[2]:"); if(MOUSE_ID==3)LCD_ShowString(30,218,200,16,16,"BUF[3]:"); LCD_ShowString(90+30,170,200,16,16,"X POS:"); LCD_ShowString(90+30,186,200,16,16,"Y POS:"); LCD_ShowString(90+30,202,200,16,16,"Z POS:"); if(MOUSE_ID==3)LCD_ShowString(90+30,218,200,16,16,"BUTTON:"); t=0; while(1) { if(PS2_Status&0x80)//得到了一次数据 { LCD_ShowNum(56+30,170,PS2_DATA_BUF[0],3,16);//填充模式 LCD_ShowNum(56+30,186,PS2_DATA_BUF[1],3,16);//填充模式 LCD_ShowNum(56+30,202,PS2_DATA_BUF[2],3,16);//填充模式 if(MOUSE_ID==3)LCD_ShowNum(56+30,218,PS2_DATA_BUF[3],3,16); Mouse_Data_Pro();//处理数据 Mouse_Show_Pos(146+30,170,MouseX.x_pos); Mouse_Show_Pos(146+30,186,MouseX.y_pos); //X 坐标 //Y 坐标 if(MOUSE_ID==3)Mouse_Show_Pos(146+30,202,MouseX.z_pos); //滚轮位置 if(MouseX.bt_mask&0x01)LCD_ShowString(146+30,218,200,16,16,"LEFT"); else LCD_ShowString(146+30,218,200,16,16," "); if(MouseX.bt_mask&0x02)LCD_ShowString(146+30,234,200,16,16,"RIGHT"); else LCD_ShowString(146+30,234,200,16,16," "); if(MouseX.bt_mask&0x04)LCD_ShowString(146+30,250,200,16,16,"MIDDLE"); else LCD_ShowString(146+30,250,200,16,16," "); PS2_Status=MOUSE; PS2_En_Data_Report();//使能数据报告 }else if(PS2_Status&0x40) { errcnt++; PS2_Status=MOUSE; www.openedv.com 503 ALIENTEK 战舰STM32开发板 LCD_ShowNum(86+30,234,errcnt,3,16);//填充模式 } t++; delay_ms(1); if(t==200) { t=0; LED0=!LED0; } } } 此部分,除了 main 函数,我们还编写了 Mouse_Show_Pos 函数,用于在指定位置显示鼠标 坐标值,并支持负数显示,通过该函数,可以方便我们显示鼠标坐标数据。至此,PS/2 鼠标实 验的软件设计部分就结束了。 38.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK 战舰 STM32 开发板上,可以看到 LCD 显示如图 38.4.1 所示内容(假定 PS/2 鼠标已经接上,并且初始化成功): 图 38.4.1 PS/2 鼠标实验显示结果 移动鼠标,或者按动按键,就可以看到上面的数据不断变化,证明我们的鼠标已经成功被 驱动了,接下来我们就可以使用鼠标来控制 STM32 了。 www.openedv.com 504 ALIENTEK 战舰STM32开发板 第三十九章 FLASH 模拟 EEPROM 实验 STM32 本身没有自带 EEPROM,但是 STM32 具有 IAP(在应用编程)功能,所以我们可 以把它的 FLASH 当成 EEPROM 来使用。本章,我们将利用 STM32 内部的 FLASH 来实现第二 十八章类似的效果,不过这次我们是将数据直接存放在 STM32 内部,而不是存放在 W25Q64。 本章分为如下几个部分: 39.1 STM32 FLASH 简介 39.2 硬件设计 39.3 软件设计 39.4 下载验证 www.openedv.com 505 ALIENTEK 战舰STM32开发板 39.1 STM32 FLASH 简介 不同型号的 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 要求的指令块,预取指令块仅用 www.openedv.com 506 ALIENTEK 战舰STM32开发板 于在 I-Code 总线上的取指操作,数据常量是通过 D-Code 总线访问的。这两条总线的访问目标 是相同的闪存模块,访问 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 所示: www.openedv.com 507 ALIENTEK 战舰STM32开发板 图 39.1.2 STM32 闪存编程过程 从上图可以得到闪存的编程顺序如下:  检查 FLASH_CR 的 LOCK 是否解锁,如果没有则先解锁  检查 FLASH_SR 寄存器的 BSY 位,以确认没有其他正在进行的编程操作  设置 FLASH_CR 寄存器的 PG 位为’1’  在指定的地址写入要编程的半字  等待 BSY 位变为’0’  读出写入的地址并验证数据 前面提到,我们在 STM32 的 FLASH 编程的时候,要先判断缩写地址是否被擦除了,所以, 我们有必要再绍一下 STM32 的闪存擦除,STM32 的闪存擦除分为两种:页擦除和整片擦除。 页擦除过程如图 39.1.3 所示 www.openedv.com 图 39.1.3 STM32 闪存页擦除过程 508 ALIENTEK 战舰STM32开发板 从上图可以看出,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 所示: www.openedv.com 509 ALIENTEK 战舰STM32开发板 图 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 寄 www.openedv.com 510 ALIENTEK 战舰STM32开发板 存器写入特定的序列(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 位半字写入和 8 位字节写入函数。这里需要说明,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 指定地址的半字的函数固件库并没有给出来,这里我们 自己写的一个函数: u16 STMFLASH_ReadHalfWord(u32 faddr) www.openedv.com 511 ALIENTEK 战舰STM32开发板 { 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;i>8); //发送高字节 IIC_Wait_Ack(); IIC_Send_Byte(val&0XFF); //发送低字节 IIC_Wait_Ack(); IIC_Stop(); //产生一个停止条件 } //读 RDA5820 寄存器 u16 RDA5820_RD_Reg(u8 addr) { u16 res; IIC_Start(); IIC_Send_Byte(RDA5820_WRITE); //发送写命令 IIC_Wait_Ack(); IIC_Send_Byte(addr); //发送地址 IIC_Wait_Ack(); IIC_Start(); IIC_Send_Byte(RDA5820_READ); //发送读命令 IIC_Wait_Ack(); res=IIC_Read_Byte(1); //读高字节,发送 ACK res<<=8; res|=IIC_Read_Byte(0); //读低字节,发送 NACK IIC_Stop(); return res; } //设置 RDA5820 为 RX 模式 void RDA5820_RX_Mode(void) { u16 temp; temp=RDA5820_RD_Reg(0X40); temp&=0xfff0; RDA5820_WR_Reg(0X40,temp) ; } //产生一个停止条件 //返回读到的数据 //读取 0X40 的内容 //RX 模式 //FM RX 模式 www.openedv.com 523 ALIENTEK 战舰STM32开发板 //设置 RDA5820 为 TX 模式 void RDA5820_TX_Mode(void) { u16 temp; temp=RDA5820_RD_Reg(0X40); temp&=0xfff0; temp|=0x0001; RDA5820_WR_Reg(0X40,temp) ; } //得到信号强度 //返回值范围:0~127 u8 RDA5820_Rssi_Get(void) { u16 temp; temp=RDA5820_RD_Reg(0X0B); return temp>>9; } //设置音量 ok //vol:0~15; void RDA5820_Vol_Set(u8 vol) { u16 temp; temp=RDA5820_RD_Reg(0X05); temp&=0XFFF0; temp|=vol&0X0F; RDA5820_WR_Reg(0X05,temp) ; } //静音设置 //mute:0,不静音;1,静音 void RDA5820_Mute_Set(u8 mute) { u16 temp; temp=RDA5820_RD_Reg(0X02); if(mute)temp|=1<<14; else temp&=~(1<<14); RDA5820_WR_Reg(0X02,temp) ; } //设置灵敏度 //rssi:0~127; void RDA5820_Rssi_Set(u8 rssi) { u16 temp; temp=RDA5820_RD_Reg(0X05); //读取 0X40 的内容 //TX 模式 //FM TM 模式 //读取 0X0B 的内容 //返回信号强度 //读取 0X05 的内容 //设置音量 //读取 0X02 的内容 //设置 MUTE //读取 0X05 的内容 www.openedv.com 524 ALIENTEK 战舰STM32开发板 temp&=0X80FF; temp|=(u16)rssi<<8; RDA5820_WR_Reg(0X05,temp) ; //设置 RSSI } //设置 TX 发送功率 //gain:0~63 void RDA5820_TxPAG_Set(u8 gain) { u16 temp; temp=RDA5820_RD_Reg(0X42); //读取 0X42 的内容 temp&=0XFFC0; temp|=gain; //GAIN RDA5820_WR_Reg(0X42,temp) ; //设置 PA 的功率 } //设置 TX 输入信号增益 //gain:0~7 void RDA5820_TxPGA_Set(u8 gain) { u16 temp; temp=RDA5820_RD_Reg(0X42); //读取 0X42 的内容 temp&=0XF8FF; temp|=gain<<8; //GAIN RDA5820_WR_Reg(0X42,temp) ; //设置 PGA } //设置 RDA5820 的工作频段 //band:0,87~108Mhz;1,76~91Mhz;2,76~108Mhz;3,用户自定义(53H~54H) void RDA5820_Band_Set(u8 band) { u16 temp; temp=RDA5820_RD_Reg(0X03); //读取 0X03 的内容 temp&=0XFFF3; temp|=band<<2; RDA5820_WR_Reg(0X03,temp) ; //设置 BAND } //设置 RDA5820 的步进频率 //band:0,100Khz;1,200Khz;3,50Khz;3,保留 void RDA5820_Space_Set(u8 spc) { u16 temp; temp=RDA5820_RD_Reg(0X03); //读取 0X03 的内容 temp&=0XFFFC; temp|=spc; RDA5820_WR_Reg(0X03,temp) ; //设置 BAND www.openedv.com 525 ALIENTEK 战舰STM32开发板 } //设置 RDA5820 的频率 //freq:频率值(单位为 10Khz),比如 10805,表示 108.05Mhz void RDA5820_Freq_Set(u16 freq) { u16 temp; u8 spc=0,band=0; u16 fbtm,chan; temp=RDA5820_RD_Reg(0X03); //读取 0X03 的内容 temp&=0X001F; band=(temp>>2)&0x03; //得到频带 spc=temp&0x03; //得到分辨率 if(spc==0)spc=10; else if(spc==1)spc=20; else spc=5; if(band==0)fbtm=8700; else if(band==1||band==2)fbtm=7600; else { fbtm=RDA5820_RD_Reg(0X53);//得到 bottom 频率 fbtm*=10; } if(freq>6; band=(temp>>2)&0x03; spc=temp&0x03; //读取 0X03 的内容 //得到频带 //得到分辨率 www.openedv.com 526 ALIENTEK 战舰STM32开发板 if(spc==0)spc=10; else if(spc==1)spc=20; else spc=5; if(band==0)fbtm=8700; else if(band==1||band==2)fbtm=7600; else { fbtm=RDA5820_RD_Reg(0X53);//得到 bottom 频率 fbtm*=10; } temp=fbtm+chan*spc; return temp;//返回频率值 } 本部分代码我们就不细说了,都是一些寄存器配置,比较简单。头文件 rda5820.h 的代码 也比较简单,这里不详细说明了。接下来我们看看 audiosel.c 文件代码如下: #include "audiosel.h" //声音初始化 void Audiosel_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB| RCC_APB2Periph_GPIOD, ENABLE); //使能 PB PD 端口时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); // PB.6 推挽输出 //推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; GPIO_Init(GPIOD, &GPIO_InitStructure); ///PD.7 推挽输出 } //设置 4052 的选择通道 //声音通道选择 //0 //MP3 通道 //1 //收音机通道 //2 //PWM 音频通道 //3 //无声 void Audiosel_Set(u8 ch) { AUDIO_SELA=ch&0X01; AUDIO_SELB=(ch>>1)&0X01; www.openedv.com 527 ALIENTEK 战舰STM32开发板 } 此部分代码很简单,用来控制 74HC4052。同样,头文件 audiosel.h 我们这里也不列出来, 大家可以打开看看。 最后,打开 main.c 文件, 代码如下: void RDA5820_Show_Msg(void) { u8 rssi; u16 freq; freq=RDA5820_Freq_Get(); //读取设置到的频率值 LCD_ShowNum(100,210,freq/100,3,16); //显示频率整数部分 LCD_ShowNum(132,210,(freq%100)/10,1,16); //显示频率小数部分 rssi=RDA5820_Rssi_Get(); LCD_ShowNum(100,230,rssi,2,16); //得到信号强度 //显示信号强度 } int main(void) { u8 key,rssi; u16 freqset=8700;//默认为 87Mhz u8 i=0; u8 mode=0; //接收模式 delay_init(); //延时函数初始化 NVIC_Configuration(); //设置 NVIC 中断分组 2:2 位抢占优先级,2 位响应优先级 uart_init(9600); //串口初始化波特率为 9600 KEY_Init(); //按键初始化 LED_Init(); //LED 端口初始化 LCD_Init(); //LCD 初始化 Audiosel_Init(); Audiosel_Set(AUDIO_RADIO); RDA5820_Init(); POINT_COLOR=RED;//设置字体为红色 LCD_ShowString(60,50,200,16,16,"WarShip STM32"); LCD_ShowString(60,70,200,16,16,"RDA5820 TEST"); LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(60,110,200,16,16,"2012/9/14"); LCD_ShowString(60,130,200,16,16,"KEY0:Freq+ KEY2:Freq-"); LCD_ShowString(60,150,200,16,16,"KEY1:Auto Search(RX)"); LCD_ShowString(60,170,200,16,16,"KEY_UP:Mode Set"); POINT_COLOR=BLUE; //显示提示信息 POINT_COLOR=BLUE;//设置字体为蓝色 LCD_ShowString(60,190,200,16,16,"Mode:FM RX"); LCD_ShowString(60,210,200,16,16,"Freq: 93.6Mhz"); www.openedv.com 528 ALIENTEK 战舰STM32开发板 LCD_ShowString(60,230,200,16,16,"Rssi:"); RDA5820_Band_Set(0); //设置频段为 87~108Mhz RDA5820_Space_Set(0); //设置步进为 100Khz RDA5820_TxPGA_Set(3); //信号增益设置为 3 RDA5820_TxPAG_Set(63); //发射功率为最大. RDA5820_RX_Mode(); //设置为接收模式 freqset=9360; //默认为 93.6Mhz RDA5820_Freq_Set(freqset); //设置频率 while(1) { key=KEY_Scan(0);//不支持连按 switch(key) { case 0://无任何按键按下 break; case KEY_UP://切换模式 mode=!mode; if(mode) { Audiosel_Set(AUDIO_PWM); //设置到 PWM 音频通道 RDA5820_TX_Mode(); //发送模式 RDA5820_Freq_Set(freqset); //设置频率 LCD_ShowString(100,190,200,16,16,"FM TX"); }else { Audiosel_Set(AUDIO_RADIO); //设置到收音机声道 RDA5820_RX_Mode(); //接收模式 RDA5820_Freq_Set(freqset); //设置频率 LCD_ShowString(100,190,200,16,16,"FM RX"); } break; case KEY_DOWN://自动搜索下一个电台. if(mode==0)//仅在接收模式有效 { while(1) { if(freqset<10800)freqset+=10; //频率增加 100Khz else freqset=8700; //回到起点 RDA5820_Freq_Set(freqset); //设置频率 delay_ms(10); //等待调频信号稳定 if(RDA5820_RD_Reg(0X0B)&(1<<8))//是一个有效电台. { www.openedv.com 529 ALIENTEK 战舰STM32开发板 RDA5820_Show_Msg(); //显示信息 break; } RDA5820_Show_Msg(); //显示信息 //在搜台期间有按键按下,则跳出搜台. key=KEY_Scan(0);//不支持连按 if(key)break; } } break; case KEY_LEFT://频率减 if(freqset>8700)freqset-=10; //频率减少 100Khz else freqset=10800; //越界处理 RDA5820_Freq_Set(freqset); //设置频率 RDA5820_Show_Msg();//显示信息 break; case KEY_RIGHT://频率增 if(freqset<10800)freqset+=10; //频率增加 100Khz else freqset=8700; //越界处理 RDA5820_Freq_Set(freqset); //设置频率 RDA5820_Show_Msg(); //显示信息 break; } i++; delay_ms(10); if(i==200)//每两秒左右检测一次信号强度等信息. { i=0; rssi=RDA5820_Rssi_Get(); //得到信号强度 LCD_ShowNum(100,230,rssi,2,16); //显示信号强度 } if((i%20)==0)LED0=!LED0;//DS0 闪烁,提示程序运行 } } 此部分代码除了 mian 函数,还有一个 RDA5820_Show_Msg 函数,该函数用于显示当前 FM 频率和信号强度等信息。main 函数开始设置默认频率,然后根据按键来设置 FM 的收发模式以 及频率。本章,我面可以利用 USMART 来设置 RDA5820 的各项参数,方便大家快速掌握。在 usmart_nametab 里面,我面加入如下五个函数: (void*)RDA5820_Rssi_Set,"void RDA5820_Rssi_Set(u8 rssi)", (void*)RDA5820_Band_Set,"void RDA5820_Band_Set(u8 band)", (void*)RDA5820_Freq_Set,"void RDA5820_Freq_Set(u16 freq)", (void*)RDA5820_Vol_Set,"void RDA5820_Vol_Set(u8 vol)", (void*)RDA5820_TxPGA_Set,"void RDA5820_TxPGA_Set(u8 gain)", www.openedv.com 530 ALIENTEK 战舰STM32开发板 这样,我面就可以在串口调用这些函数,从而修改 RDA5820 的配置。至此,我们的软件 设计部分就结束了。 40.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK 战舰 STM32 开发板上,得到如图 40.4.1 所示界面: 图 40.4.1 程序运行效果图 此时,我们就可以在耳机上面听到广播了(注意,将开发板自带的天线拉出来,可以提高 接收能力),通过 KEY0 和 KEY1,调节频率,也可以通过按 KEY1 自动搜索下一个电台(PS: 如果收不到台,说明你住的地方信号不好,跑到窗户边或者室外,一般就可以收到电台了)。 通过 WK_UP 按键,可以切换工作模式,例如:切换到 FM TX 模式,就可以通过单独的收 音机(或者另外一块战舰 STM32 开发板)接收到开发板发出的 FM 信号了,此时在多功能接口 P3 的 AIN 端输入音频信号,就可以在收音机端接收这个音频信号了。 本章还可以通过 USMART 设置 RDA5820 的相关参数,感兴趣的朋友可以动手试试。 www.openedv.com 531 ALIENTEK 战舰STM32开发板 第四十一章 摄像头实验 ALIENTEK 战舰 STM32 开发板板载了一个摄像头接口(P8),该接口可以用来连接 ALIENTEK OV7670 摄像头模块。本章,我们将使用 STM32 驱动 ALIENTEK OV7670 摄像头 模块,实现摄像头功能。本章分为如下几个部分: 41.1 OV7670 简介 41.2 硬件设计 41.3 软件设计 41.4 下载验证 www.openedv.com 532 ALIENTEK 战舰STM32开发板 41.1 OV7670 简介 OV7670 是 OV(OmniVision)公司生产的一颗 1/6 寸的 CMOS VGA 图像传感器。该传感 器体积小、工作电压低,提供单片 VGA 摄像头和影像处理器的所有功能。通过 SCCB 总线控 制,可以输出整帧、子采样、取窗口等方式的各种分辨率 8 位影像数据。该产品 VGA 图像最 高达到 30 帧/秒。用户可以完全控制图像质量、数据格式和传输方式。所有图像处理功能过程 包括伽玛曲线、白平衡、度、色度等都可以通过 SCCB 接口编程。OmmiVision 图像传感器应 用独有的传感器技术,通过减少或消除光学或电子缺陷如固定图案噪声、托尾、浮散等,提高 图像质量,得到清晰的稳定的彩色图像。 OV7670 的特点有:  高灵敏度、低电压适合嵌入式应用  标准的 SCCB 接口,兼容 IIC 接口  支持 RawRGB、RGB(GBR4:2:2,RGB565/RGB555/RGB444),YUV(4:2:2)和 YCbCr (4:2:2)输出格式  支持 VGA、CIF,和从 CIF 到 40*30 的各种尺寸输出  支持自动曝光控制、自动增益控制、自动白平衡、自动消除灯光条纹、自动黑电平校 准等自动控制功能。同时支持色饱和度、色相、伽马、锐度等设置。  支持闪光灯  支持图像缩放 OV7670 的功能框图图如图 41.1.1 所示: 图 41.1.1 OV7670 功能框图 OV7670 传感器包括如下一些功能模块。 1.感光整列(Image Array) OV7670 总共有 656*488 个像素,其中 640*480 个有效(即有效像素为 30W)。 2.时序发生器(Video Timing Generator) 时序发生器具有的功能包括:整列控制和帧率发生(7 种不同格式输出)、内部信号发生器 www.openedv.com 533 ALIENTEK 战舰STM32开发板 和分布、帧率时序、自动曝光控制、输出外部时序(VSYNC、HREF/HSYNC 和 PCLK)。 3.模拟信号处理(Analog Processing) 模拟信号处理所有模拟功能,并包括:自动增益(AGC)和自动白平衡(AWB)。 4.A/D 转换(A/D) 原始的信号经过模拟处理器模块之后 ,分 G 和 BR 两路进入一个 10 位的 A/D 转换器, A/D 转换器工作在 12M 频率,与像素频率完全同步(转换的频率和帧率有关)。 除 A/D 转换器外,该模块还有以下三个功能:  黑电平校正(BLC)  U/V 通道延迟  A/D 范围控制 A/D 范围乘积和 A/D 的范围控制共同设置 A/D 的范围和最大值,允许用户根据应用调整图 片的亮度。 5.测试图案发生器(Test Pattern Generator) 测试图案发生器功能包括:八色彩色条图案、渐变至黑白彩色条图案和输出脚移位“1”。 6.数字处理器(DSP) 这个部分控制由原始信号插值到 RGB 信号的过程,并控制一些图像质量:  边缘锐化(二维高通滤波器)  颜色空间转换( 原始信号到 RGB 或者 YUV/YCbYCr)  RGB 色彩矩阵以消除串扰  色相和饱和度的控制  黑/白点补偿  降噪  镜头补偿  可编程的伽玛  十位到八位数据转换 7.缩放功能(Image Scaler) 这个模块按照预先设置的要求输出数据格式,能将 YUV/RGB 信号从 VGA 缩小到 CIF 以 下的任何尺寸。 8.数字视频接口(Digital Video Port) 通过寄存器 COM2[1:0],调节 IOL/IOH 的驱动电流,以适应用户的负载。 9.SCCB 接口(SCCB Interface) SCCB 接 口 控 制 图 像 传 感 器 芯 片 的 运 行 , 详 细 使 用 方 法 参 照 光 盘 的 《 OmniVision Technologies Seril Camera Control Bus(SCCB) Specification》这个文档 10.LED 和闪光灯的输出控制(LED and Storbe Flash Control Output) OV7670 有闪光灯模式,可以控制外接闪光灯或闪光 LED 的工作。 OV7670 的寄存器通过 SCCB 时序访问并设置,SCCB 时序和 IIC 时序十分类似,在本章我 们不做介绍,请大家参考光盘的相关文档。 接下来我们介绍一下 OV7670 的图像数据输出格式。首先我们简单介绍几个定义: VGA,即分辨率为 640*480 的输出模式; QVGA,即分辨率为 320*240 的输出格式,也就是本章我们需要用到的格式; QQVGA,即分辨率为 160*120 的输出格式; PCLK,即像素时钟,一个 PCLK 时钟,输出一个像素(或半个像素)。 VSYNC,即帧同步信号。 www.openedv.com 534 ALIENTEK 战舰STM32开发板 HREF /HSYNC,即行同步信号。 OV7670 的图像数据输出(通过 D[7:0])就是在 PCLK,VSYNC 和 HREF/ HSYNC 的控制 下进行的。首先看看行输出时序,如图 41.1.2 所示: 图 41.1.2 OV7670 行输出时序 从上图可以看出,图像数据在 HREF 为高的时候输出,当 HREF 变高后,每一个 PCLK 时 钟,输出一个字节数据。比如我们采用 VGA 时序,RGB565 格式输出,每 2 个字节组成一个像 素的颜色(高字节在前,低字节在后),这样每行输出总共有 640*2 个 PCLK 周期,输出 640*2 个字节。 再来看看帧时序(VGA 模式),如图 41.1.3 所示: 图 41.1.3 OV7670 帧时序 上图清楚的表示了 OV7670 在 VGA 模式下的数据输出,注意,图中的 HSYNC 和 HREF 其实是同一个引脚产生的信号,只是在不同场合下面,使用不同的信号方式,我们本章用到的 是 HREF。 因为 OV7670 的像素时钟(PCLK)最高可达 24Mhz,我们用 STM32F103ZET6 的 IO 口直 接抓取,是非常困难的,也十分占耗 CPU(可以通过降低 PCLK 输出频率,来实现 IO 口抓取, www.openedv.com 535 ALIENTEK 战舰STM32开发板 但是不推荐)。所以,本章我们并不是采取直接抓取来自 OV7670 的数据,而是通过 FIFO 读取, ALIENTEK OV7670 摄像头模块自带了一个 FIFO 芯片,用于暂存图像数据,有了这个芯片, 我们就可以很方便的获取图像数据了,而不再需要单片机具有高速 IO,也不会耗费多少 CPU, 可以说,只要是个单片机,都可以通过 ALIENTEK OV7670 摄像头模块实现拍照的功能。 接下来我们介绍一下 ALIENTEK OV7670 摄像头模块。该模块的外观如图 41.1.4: 图 41.1.4 ALIENTEK OV7670 摄像头模块外观图 模块原理图如图 41.1.5 所示: 图 41.1.5 ALIENTEK OV7670 摄像头模块原理图 www.openedv.com 536 ALIENTEK 战舰STM32开发板 从上图可以看出,ALIENTEK OV7670 摄像头模块自带了有源晶振,用于产生 12M 时钟作 为 OV7670 的 XCLK 输入。同时自带了稳压芯片,用于提供 OV7670 稳定的 2.8V 工作电压, 并带有一个 FIFO 芯片(AL422B),该 FIFO 芯片的容量是 384K 字节,足够存储 2 帧 QVGA 的 图像数据。模块通过一个 2*9 的双排排针(P1)与外部通信,与外部的通信信号如表 41.1.1 所 示: 信号 作用描述 信号 作用描述 VCC3.3 模块供电脚,接 3.3V 电源 FIFO_WEN FIFO 写使能 GND 模块地线 FIFO_WRST FIFO 写指针复位 OV_SCL SCCB 通信时钟信号 FIFO_RRST FIFO 读指针复位 OV_SDA SCCB 通信数据信号 FIFO_OE FIFO 输出使能(片选) FIFO_D[7:0] FIFO 输出数据(8 位) OV_VSYNC OV7670 帧同步信号 FIFO_RCLK 读 FIFO 时钟 表 41.1.1 OV7670 模块信号及其作用描述 下面我们来看看如何使用 ALIENTEK OV7670 摄像头模块(以 QVGA 模式,RGB565 格式 为例)。对于该模块,我们只关心两点:1,如何存储图像数据;2,如何读取图像数据。 首先,我们来看如何存储图像数据。 ALIENTEK OV7670 摄像头模块存储图像数据的过程为:等待 OV7670 同步信号FIFO 写 指针复位FIFO 写使能等待第二个 OV7670 同步信号FIFO 写禁止。通过以上 5 个步骤, 我们就完成了 1 帧图像数据的存储。 接下来,我们来看看如何读取图像数据。 在存储完一帧图像以后,我们就可以开始读取图像数据了。读取过程为:FIFO 读指针复位 给 FIFO 读时钟(FIFO_RCLK)读取第一个像素高字节给 FIFO 读时钟读取第一个像 素低字节给 FIFO 读时钟读取第二个像素高字节循环读取剩余像素结束。 可以看出,ALIENTEK OV7670 摄像头模块数据的读取也是十分简单,比如 QVGA 模式, RGB565 格式,我们总共循环读取 320*240*2 次,就可以读取 1 帧图像数据,把这些数据写入 LCD 模块,我们就可以看到摄像头捕捉到的画面了。 OV7670 还可以对输出图像进行各种设置,详见光盘《OV7670 中文数据手册 1.01》和 《OV7670 software application note》这两个文档,对 AL422B 的操作时序,请大家参考 AL422B 的数据手册。 了解了 OV7670 模块的数据存储和读取,我们就可以开始设计代码了,本章,我们用一个 外部中断,来捕捉帧同步信号(VSYNC),然后在中断里面启动 OV7670 模块的图像数据存储, 等待下一次 VSHNC 信号到来,我们就关闭数据存储,然后一帧数据就存储完成了,在主函数 里面就可以慢慢的将这一帧数据读出来,放到 LCD 即可显示了,同时开始第二帧数据的存储, 如此循环,实现摄像头功能。 本章,我们将使用摄像头模块的 QVGA 输出(320*240),刚好和战舰 STM32 开发板使用 的 LCD 模块分辨率一样,一帧输出就是一屏数据,提高速度的同时也不浪费资源。注意: ALIENTEK OV7670 摄像头模块自带的 FIFO 是没办法缓存一帧的 VGA 图像的,如果使用 VGA 输出,那么你必须在 FIFO 写满之前开始读 FIFO 数据,保证数据不被覆盖。 41.2 硬件设计 本章实验功能简介:开机后,初始化摄像头模块(OV7670),如果初始化成功,则在 LCD www.openedv.com 537 ALIENTEK 战舰STM32开发板 模块上面显示摄像头模块所拍摄到的内容。我们可以通过 KEY0 设置光照模式(5 种模式)、 通过 KEY1 设置色饱和度,通过 KEY2 设置亮度,通过 WK_UP 设置对比度,通过 TPAD 设置 特效(总共 7 种特效)。通过串口,我们可以查看当前的帧率(这里是指 LCD 显示的帧率, 而不是指 OV7670 的输出帧率),同时可以借助 USMART 设置 OV7670 的寄存器,方便大家 调试。DS0 指示程序运行状态。 本实验用到的硬件资源有: 1) 指示灯 DS0 2) 5 个按键(包括 TPAD 触摸按键) 3) 串口 4) TFTLCD 模块 5) 摄像头模块接口 6) 摄像头模块 ALIENTEK OV7670 摄像头模块在 41.1 节已经有详细介绍过,这里我们主要介绍该模块与 ALIETEK 战舰 STM32 开发板的连接。 在开发板的左下角的 2*9 的 P8 排座,是摄像头模块/OLED 模块共用接口,在第十七章, 我们曾简单介绍过这个接口。本章,我们只需要将 ALIENTEK OV7670 摄像头模块插入这个接 口即可,该接口与 STM32 的连接关系如图 41.2.1 所示: 图 41.2.1 摄像头模块接口与 STM32 连接图 从上图可以看出,OV7670 摄像头模块的各信号脚与 STM32 的连接关系为: OV_SDA 接 PG13; OV_SCL 接 PD3; FIFO_RCLK 接 PB4; FIFO_WEN 接 PB3; FIFO_WRST 接 PD6; FIFO_RRST 接 PG14; FIFO_OE 接 PG15; OV_VSYNC 接 PA8; OV_D[7:0]接 PC[7:0]; 这些线的连接,战舰 STM32 的内部已经连接好了,我们只需要将 OV7670 摄像头模块插上 www.openedv.com 538 ALIENTEK 战舰STM32开发板 去就好了。实物连接如图 41.2.2 所示: 图 41.2.2 OV7670 摄像头模块与开发板连接实物图 41.3 软件设计 打开我们摄像头实验的工程,可以看到我们的工程中多了 ov7670.c 和 sccb.c 源文件,以及 头文件 ov7670.h、sccb.h 和 ov7670cfg.h 等 5 个文件。 本章总共新增了 5 个文件,代码比较多,我们就不一一列出了,仅挑两个重要的地方进行 讲解。首先,我们来看 ov7670.c 里面的 OV7670_Init 函数,该函数代码如下: u8 OV7670_Init(void) { u8 temp; u16 i=0; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB| RCC_APB2Periph_GPIOC|RCC_APB2Periph_GPIOD| RCC_APB2Periph_GPIOG, ENABLE); //使能相关端口时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; //PA8 输入 上拉 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 GPIOA.8 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3|GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 端口配置 //推挽输出 GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_SetBits(GPIOB,GPIO_Pin_3|GPIO_Pin_4); //初始化 GPIO www.openedv.com 539 ALIENTEK 战舰STM32开发板 GPIO_InitStructure.GPIO_Pin = 0xff; //PC0~7 输入 上拉 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(GPIOC, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOD, &GPIO_InitStructure); GPIO_SetBits(GPIOD,GPIO_Pin_6); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14|GPIO_Pin_15; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOG, &GPIO_InitStructure); GPIO_SetBits(GPIOG,GPIO_Pin_14|GPIO_Pin_15); GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE); //SWD SCCB_Init(); //初始化 SCCB 的 IO 口 if(SCCB_WR_Reg(0x12,0x80))return 1; //复位 SCCB delay_ms(50); //读取产品型号 temp=SCCB_RD_Reg(0x0b); if(temp!=0x73)return 2; temp=SCCB_RD_Reg(0x0a); if(temp!=0x76)return 2; //初始化序列 for(i=0;iIDR&0XFF; //读数据 OV7670_RCK=1; color<<=8; OV7670_RCK=0; color|=GPIOC->IDR&0XFF; //读数据 OV7670_RCK=1; LCD->LCD_RAM=color; } EXTI_ClearITPendingBit(EXTI_Line8); //清除 LINE8 上的中断标志位 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_Configuration(); //设置 NVIC 中断分组 2:2 位抢占优先级,2 位响应优先级 uart_init(9600); //串口初始化波特率为 9600 LED_Init(); //LED 端口初始化 LCD_Init(); //LCD 初始化 KEY_Init(); //KEY 初始化 TPAD_Init(72); //触摸按键初始化 www.openedv.com 544 ALIENTEK 战舰STM32开发板 POINT_COLOR=RED;//设置字体为红色 LCD_ShowString(60,50,200,16,16,"WarShip STM32"); LCD_ShowString(60,70,200,16,16,"OV7670 TEST"); LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(60,110,200,16,16,"2012/9/14"); LCD_ShowString(60,130,200,16,16,"KEY0:Light Mode"); LCD_ShowString(60,150,200,16,16,"KEY1:Saturation"); LCD_ShowString(60,170,200,16,16,"KEY2:Brightness"); LCD_ShowString(60,190,200,16,16,"KEY_UP:Contrast"); LCD_ShowString(60,210,200,16,16,"TPAD:Effects"); LCD_ShowString(60,230,200,16,16,"OV7670 Init..."); while(OV7670_Init())//初始化 OV7670 { LCD_ShowString(60,230,200,16,16,"OV7670 Error!!"); delay_ms(200); LCD_Fill(60,230,239,246,WHITE); delay_ms(200); } LCD_ShowString(60,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(10,174,240,320); //设置窗口 OV7670_CS=0; while(1) { key=KEY_Scan(0);//不支持连按 if(key) { tm=20; switch(key) { case KEY_RIGHT: //灯光模式 Light Mode lightmode++; if(lightmode>4)lightmode=0; OV7670_Light_Mode(lightmode); sprintf((char*)msgbuf,"%s",LMODE_TBL[lightmode]); break; www.openedv.com 545 ALIENTEK 战舰STM32开发板 case KEY_DOWN://饱和度 Saturation saturation++; if(saturation>4)saturation=0; OV7670_Color_Saturation(saturation); sprintf((char*)msgbuf,"Saturation:%d",(signed char)saturation-2); break; case KEY_LEFT: //亮度 Brightness brightness++; if(brightness>4)brightness=0; OV7670_Brightness(brightness); sprintf((char*)msgbuf,"Brightness:%d",(signed char)brightness-2); break; case KEY_UP: //对比度 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(60,60,200,16,16,msgbuf); tm--; } i++; if(i==15)//DS0 闪烁. { i=0; LED0=!LED0; } } } 此部分代码除了 mian 函数,还有一个 camera_refresh 函数,该函数用于将摄像头模块 FIFO www.openedv.com 546 ALIENTEK 战舰STM32开发板 的数据读出,并显示在 LCD 上面。main 函数则比较简单,我们就不细说了。 前面提到,我们要用 USMART 来设置摄像头的参数,我们只需要在 usmart_nametab 里面 添加 SCCB_WR_Reg 和 SCCB_RD_Reg 这两个函数,就可以轻松调试摄像头了。 41.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK 战舰 STM32 开发板上,得到如图 41.4.1 所示界面: 图 41.4.1 程序运行效果图 此时,我们可以按不同的按键(KEY0~KEY2、WK_UP、TPAD 等),来设置摄像头的相关 参数和模式,得到不同的成像效果。同时,你还可以在串口,通过 USMART 调用 SCCB_WR_Reg 函数,来设置 OV7670 的各寄存器,达到调试测试 OV7670 的目的,如图 41.4.2 所示: www.openedv.com 547 ALIENTEK 战舰STM32开发板 图 41.4.2 USMART 调试 OV7670 从上图还可以看出,LCD 显示帧率为 8 帧左右,则可以推断 OV7670 的输出帧率则至少是 3*8=24 帧以上(实际是 30 帧)。 www.openedv.com 548 ALIENTEK 战舰STM32开发板 第四十二章 外部 SRAM 实验 STM32F103ZET6 自带了 64K 字节的 SRAM,对一般应用来说,已经足够了,不过在一些 对内存要求高的场合,STM32 自带的这些内存就不够用了。比如跑算法或者跑 GUI 等,就可 能不太够用,所以战舰 STM32 开发板板载了一颗 1M 字节容量的 SRAM 芯片:IS62WV51216, 满足大内存使用的需求。 本章,我们将使用 STM32 来驱动 IS62WV51216,实现对 IS62WV51216 的访问控制,并测 试其容量。本章分为如下几个部分: 42.1 IS62WV51216 简介 42.2 硬件设计 42.3 软件设计 42.4 下载验证 www.openedv.com 549 ALIENTEK 战舰STM32开发板 42.1 IS62WV51216 简介 IS62WV51216 是 ISSI(Integrated Silicon Solution, Inc)公司生产的一颗 16 位宽 512K (512*16,即 1M 字节)容量的 CMOS 静态内存芯片。该芯片具有如下几个特点:  高速。具有 45ns/55ns 访问速度。  低功耗。  TTL 电平兼容。  全静态操作。不需要刷新和时钟电路。  三态输出。  字节控制功能。支持高/低字节控制。 IS62WV51216 的功能框图如图 42.1.1 所示: 图 42.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 原理图如图 42.1.2 所示: www.openedv.com 550 ALIENTEK 战舰STM32开发板 图 42.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、 www.openedv.com 551 ALIENTEK 战舰STM32开发板 16 位宽,读写共用一个时序寄存器。使用的函数是: 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。 42.2 硬件设计 本章实验功能简介:开机后,显示提示信息,然后按下 KEY1 按键,即测试外部 SRAM 容 量大小并显示在 LCD 上。按下 WK_UP 按键,即显示预存在外部 SRAM 的数据。DS0 指示程 序运行状态。 本实验用到的硬件资源有: 1) 指示灯 DS0 2) KEY1 和 WK_UP 按键 3) 串口 4) TFTLCD 模块 5) IS62WV51216 这些我们都已经介绍过(IS62WV51216 与 STM32 的各 IO 对应关系,请参考光盘原理图), 接下来我们开始软件设计。 42.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 ((u32)(0x68000000)) //初始化外部 SRAM void FSMC_SRAM_Init(void) { FSMC_NORSRAMInitTypeDef FSMC_NSInitStructure; FSMC_NORSRAMTimingInitTypeDef readWriteTiming; GPIO_InitTypeDef GPIO_InitStructure; www.openedv.com 552 ALIENTEK 战舰STM32开发板 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD|RCC_APB2Periph_GPIOE| RCC_APB2Periph_GPIOF|RCC_APB2Periph_GPIOG,ENABLE); 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; readWriteTiming.FSMC_AddressHoldTime = 0x00; readWriteTiming.FSMC_DataSetupTime = 0x03; //地址建立时间为 1 个 HCLK //地址保持时间模式 A 未用到 //数据保持时间为 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 配置 www.openedv.com 553 ALIENTEK 战舰STM32开发板 FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM3, ENABLE); // 使能 BANK3 } //在指定地址开始,连续写入 n 个字节. //pBuffer:字节指针 //WriteAddr:要写入的地址 //n:要写入的字节数 void FSMC_SRAM_WriteBuffer(u8* pBuffer,u32 WriteAddr,u32 n) { for(;n!=0;n--) { *(vu8*)(Bank1_SRAM3_ADDR+WriteAddr)=*pBuffer; WriteAddr+=2;//这里需要加 2,是因为 STM32 的 FSMC //地址右移一位对齐.加 2 相当于加 1. 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+=2;//这里需要加 2,是因为 STM32 的 FSMC 地 //址右移一位对齐.加 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; www.openedv.com 554 ALIENTEK 战舰STM32开发板 u8 temp=0; u8 sval=0; //在地址 0 读到的数据 LCD_ShowString(x,y,239,y+16,16,"Ex Memory Test: 0KB"); //每隔 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_Configuration(); //设置 NVIC 中断分组 2:2 位抢占优先级,2 位响应优先级 uart_init(9600); //串口初始化波特率为 9600 LED_Init(); //LED 端口初始化 LCD_Init(); //LCD 初始化 KEY_Init(); //KEY 初始化 FSMC_SRAM_Init(); //初始化外部 SRAM POINT_COLOR=RED; //设置字体为红色 LCD_ShowString(60,50,200,16,16,"WarShip STM32"); LCD_ShowString(60,70,200,16,16,"SRAM TEST"); LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(60,110,200,16,16,"2012/9/16"); LCD_ShowString(60,130,200,16,16,"KEY1:Test Sram"); LCD_ShowString(60,150,200,16,16,"WK_UP:TEST Data"); POINT_COLOR=BLUE;//设置字体为蓝色 for(ts=0;ts<250000;ts++)testsram[ts]=ts;//预存测试数据 while(1) { key=KEY_Scan(0);//不支持连按 www.openedv.com 555 ALIENTEK 战舰STM32开发板 if(key==KEY_DOWN)fsmc_sram_test(60,170);//测试 SRAM 容量 else if(key==KEY_UP)//打印预存测试数据 { for(ts=0;ts<250000;ts++)LCD_ShowxNum(60,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 的分配,可以 针对性的选择放外部还是放内部,有利于提高程序运行速度,使用起来也比较方便。 42.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK 战舰 STM32 开发板上,得到如图 42.4.1 所示界面: www.openedv.com 556 ALIENTEK 战舰STM32开发板 图 42.4.1 程序运行效果图 此时,我们按下 KEY1,就可以在 LCD 上看到内存测试的画面,同样,按下 WK_UP,就 可以看到 LCD 显示存放在数组 testsram 里面的测试数据,如图 42.4.2 所示: 图 42.4.2 外部 SRAM 测试界面 该实验我们还可以借助 USMART 来测试,只需要在 usmart_nametab 里面添加读写 SRAM 的两个函数,就可以用 USMART 来测试外部 SRAM 了。 www.openedv.com 557 ALIENTEK 战舰STM32开发板 第四十三章 内存管理实验 上一节,我们学会了使用 STM32 驱动外部 SRAM,以扩展 STM32 的内存,加上 STM32 本身自带的 64K 字节内存,我们可供使用的内存还是比较多的。如果我们所用的内存都像上一 节的 testsram 那样,定义一个数组来使用,显然不是一个好办法。 本章,我们将学习内存管理,实现对内存的动态管理。本章分为如下几个部分: 43.1 内存管理简介 43.2 硬件设计 43.3 软件设计 43.4 下载验证 www.openedv.com 558 ALIENTEK 战舰STM32开发板 43.1 内存管理简介 内存管理,是指软件运行时对计算机内存资源的分配和使用的技术。其最主要的目的是如 何高效,快速的分配,并且在适当的时候释放和回收内存资源。内存管理的实现方法有很多种, 他们其实最终都是要实现 2 个函数:malloc 和 free;malloc 函数用于内存申请,free 函数用于 内存释放。 本章,我们介绍一种比较简单的办法来实现:分块式内存管理。下面我们介绍一下该方法 的实现原理,如图 43.1.1 所示: 内存管理 内存块 内存块 内存块 1 2 3 …… 内存块 内存池 n 第1项 第2项 第3项 …… 第n项 内存管理表 分配方向 malloc,free等函数 图 43.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 个内存管理表项目的值都清零,标记释 放,完成一次内存释放。 关于分块式内存管理的原理,我们就介绍到这里。 www.openedv.com 559 ALIENTEK 战舰STM32开发板 43.2 硬件设计 本章实验功能简介:开机后,显示提示信息,等待外部输入。KEY0 用于申请内存,每次 申请 2K 字节内存。KEY1 用于写数据到申请到的内存里面。KEY2 用于释放内存。WK_UP 用 于切换操作内存区(内部内存/外部内存)。DS0 用于指示程序运行状态。本章我们还可以通过 USMART 调试,测试内存管理函数。 本实验用到的硬件资源有: 1) 指示灯 DS0 2) 四个按键 3) 串口 4) TFTLCD 模块 5) IS62WV51216 这些我们都已经介绍过,接下来我们开始软件设计。 43.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, //内存管理状态表 www.openedv.com 560 ALIENTEK 战舰STM32开发板 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;i74 个时钟,这是因为 SD 卡内部有个供电电压上升时间,大概为 64 个 CLK,剩 下的 10 个 CLK 用于 SD 卡同步,之后才能开始 CMD0 的操作,在卡初始化的时候,CLK 时钟 最大不能超过 400Khz!。 接着我们看看 SD 卡的初始化,SD 卡的典型初始化过程如下: 1、初始化与 SD 卡连接的硬件条件(MCU 的 SPI 配置,IO 口配置); 2、上电延时(>74 个 CLK); 3、复位卡(CMD0),进入 IDLE 状态; 4、发送 CMD8,检查是否支持 2.0 协议; 5、根据不同协议检查 SD 卡(命令包括:CMD55、CMD41、CMD58 和 CMD1 等); 6、取消片选,发多 8 个 CLK,结束初始化 这样我们就完成了对 SD 卡的初始化,注意末尾发送的 8 个 CLK 是提供 SD 卡额外的时钟, 完成某些操作。通过 SD 卡初始化,我们可以知道 SD 卡的类型(V1、V2、V2HC 或者 MMC), 在完成了初始化之后,就可以开始读写数据了。 SD 卡读取数据,这里通过 CMD17 来实现,具体过程如下: 1、发送 CMD17; 2、接收卡响应 R1; 3、接收数据起始令牌 0XFE; 4、接收数据; 5、接收 2 个字节的 CRC,如果不使用 CRC,这两个字节在读取后可以丢掉。 6、禁止片选之后,发多 8 个 CLK; 以上就是一个典型的读取 SD 卡数据过程,SD 卡的写于读数据差不多,写数据通过 CMD24 来实现,具体过程如下: www.openedv.com 572 ALIENTEK 战舰STM32开发板 1、发送 CMD24; 2、接收卡响应 R1; 3、发送写数据起始令牌 0XFE; 4、发送数据; 5、发送 2 字节的伪 CRC; 6、禁止片选之后,发多 8 个 CLK; 以上就是一个典型的写 SD 卡过程。关于 SD 卡的介绍,我们就介绍到这里,更详细的介 绍请参考光盘 SD 卡的参考资料(SD 卡 2.0 协议)。 44.2 硬件设计 本章实验功能简介:开机的时候先初始化 SD 卡,如果 SD 卡初始化完成,则提示 LCD 初 始化成功。按下 KEY0,读取 SD 卡扇区 0 的数据,然后通过串口发送到电脑。如果没初始化 通过,则在 LCD 上提示初始化失败。 同样用 DS0 来指示程序正在运行。 本实验用到的硬件资源有: 1) 指示灯 DS0 2) KEY0 按键 3) 串口 4) TFTLCD 模块 5) SD 卡接口 6) SD 卡 前面四部分,在之前的实例已经介绍过了,这里我们介绍一下 SD 卡接口和 STM32 的连接 关系,如图 44.2.1 所示: 图44.2.1 SD卡接口与STM32连接原理图 www.openedv.com 573 ALIENTEK 战舰STM32开发板 我们用跳线帽将P10的SD_DT3、SD_CMD、SD_SCK、SD_DT0分别同P12的SD_CS、 SPI2_MOSI、SPI2_SCK、SPI2_MISO连接起来,即实现SD卡的SPI模式连接。硬件连接示意 图如图44.2.2所示: 图44.2.2 SD卡SPI方式硬件连接示意图 将图中所示的4处,用跳线帽短接,接口实现SD卡与STM32的SPI连接。最后,你还得自 备一个SD卡,将其插入板子下面的SD卡接口。 44.3 软件设计 打开 SD 卡实验工程,可以看到我们新建了 MMC_SD.C 文件和 MMC_SD.h,所有 SD 卡相 关的驱动代码和定义都在这两个文件中。。 打开 MMC_SD.C 文件,在该文件里面,我们输入与 SD 卡相关的操作代码,这里由于篇 幅限制,我们不贴出所有代码,仅介绍两个最重要的函数,第一个是 SD_Initialize 函数,该函 数源码如下: //初始化 SD 卡 u8 SD_Initialize(void) { u8 r1; // 存放 SD 卡的返回值 u16 retry; // 用来进行超时计数 u8 buf[4]; u16 i; SD_SPI_Init(); //初始化 IO SD_SPI_SpeedLow(); //设置到低速模式 for(i=0;i<10;i++)SD_SPI_ReadWriteByte(0XFF);//发送最少 74 个脉冲 retry=20; do { r1=SD_SendCmd(CMD0,0,0x95);//进入 IDLE 状态 }while((r1!=0X01) && retry--); www.openedv.com 574 ALIENTEK 战舰STM32开发板 SD_Type=0;//默认无卡 if(r1==0X01) { if(SD_SendCmd(CMD8,0x1AA,0x87)==1)//SD V2.0 { for(i=0;i<4;i++)buf[i]=SD_SPI_ReadWriteByte(0XFF); //Get trailing return value of R7 resp if(buf[2]==0X01&&buf[3]==0XAA)//卡是否支持 2.7~3.6V { retry=0XFFFE; do { SD_SendCmd(CMD55,0,0X01); //发送 CMD55 r1=SD_SendCmd(CMD41,0x40000000,0X01);//发送 CMD41 }while(r1&&retry--); if(retry&&SD_SendCmd(CMD58,0,0X01)==0)//鉴别 SD2.0 卡版本开始 { for(i=0;i<4;i++)buf[i]=SD_SPI_ReadWriteByte(0XFF);//得到 OCR 值 if(buf[0]&0x40)SD_Type=SD_TYPE_V2HC; //检查 CCS else SD_Type=SD_TYPE_V2; } } }else//SD V1.x/ MMC V3 { SD_SendCmd(CMD55,0,0X01); //发送 CMD55 r1=SD_SendCmd(CMD41,0,0X01); //发送 CMD41 if(r1<=1) { SD_Type=SD_TYPE_V1; retry=0XFFFE; do //等待退出 IDLE 模式 { SD_SendCmd(CMD55,0,0X01); //发送 CMD55 r1=SD_SendCmd(CMD41,0,0X01);//发送 CMD41 }while(r1&&retry--); }else { SD_Type=SD_TYPE_MMC;//MMC V3 retry=0XFFFE; do //等待退出 IDLE 模式 { r1=SD_SendCmd(CMD1,0,0X01);//发送 CMD1 }while(r1&&retry--); www.openedv.com 575 ALIENTEK 战舰STM32开发板 } if(retry==0||SD_SendCmd(CMD16,512,0X01)!=0)SD_Type=SD_TYPE_ERR; //错误的卡 } } SD_DisSelect(); //取消片选 SD_SPI_SpeedHigh(); //高速 if(SD_Type)return 0; else if(r1)return r1; return 0xaa; //其他错误 } 该函数先设置与 SD 相关的 IO 口及 SPI 初始化,然后发送 CMD0,进入 IDLE 状态,并设置 SD 卡为 SPI 模式通信,然后判断 SD 卡类型,完成 SD 卡的初始化,注意该函数调用的 SD_SPI_Init 等函数,实际是对 SPI2 的相关函数进行了一层封装,方便移植。另外一个要介绍的函数是 SD_ReadDisk,该函数用于从 SD 卡读取一个扇区的数据(这里一般为 512 字节),该函数代码如 下: //读 SD 卡 //buf:数据缓存区 //sector:扇区 //cnt:扇区数 //返回值:0,ok;其他,失败. u8 SD_ReadDisk(u8*buf,u32 sector,u8 cnt) { u8 r1; if(SD_Type!=SD_TYPE_V2HC)sector <<= 9; //转换为字节地址 if(cnt==1) { r1=SD_SendCmd(CMD17,sector,0X01); //读命令 if(r1==0) //指令发送成功 { r1=SD_RecvData(buf,512); //接收 512 个字节 } }else { r1=SD_SendCmd(CMD18,sector,0X01); //连续读命令 do { r1=SD_RecvData(buf,512); //接收 512 个字节 buf+=512; }while(--cnt && r1==0); SD_SendCmd(CMD12,0,0X01); //发送停止命令 } SD_DisSelect(); //取消片选 www.openedv.com 576 ALIENTEK 战舰STM32开发板 return r1; } 此函数先发送 CMD17 命令,然后读取一个扇区的数据,详细见代码,这里我们就不多介绍 了。然后打开 MMC_SD.H, 该文件主要是一些命令的宏定义以及函数声明,在这里我们设定 了 SD 卡的 CS 管脚为 PD2。接下开我们看看主函数里面编写的应用代码,打开 main.c 文件, 代码如下: int main(void) { u8 key; u32 sd_size; u8 t=0; u8 *buf; delay_init(); NVIC_Configuration(); uart_init(9600); LED_Init(); LCD_Init(); KEY_Init(); FSMC_SRAM_Init(); mem_init(SRAMIN); //延时函数初始化 //设置 NVIC 中断分组 2:2 位抢占优先级,2 位响应优先级 //串口初始化波特率为 9600 //LED 端口初始化 //初始化 LCD //初始化按键 //初始化外部 SRAM //初始化内部内存池 POINT_COLOR=RED;//设置字体为红色 LCD_ShowString(60,50,200,16,16,"WarShip STM32"); LCD_ShowString(60,70,200,16,16,"MALLOC TEST"); LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(60,110,200,16,16,"2012/9/17"); LCD_ShowString(60,130,200,16,16,"KEY0:Read Sector 0"); while(SD_Initialize())//检测不到 SD 卡 { LCD_ShowString(60,150,200,16,16,"SD Card Error!"); delay_ms(500); LCD_ShowString(60,150,200,16,16,"Please Check! "); delay_ms(500); LED0=!LED0;//DS0 闪烁 } POINT_COLOR=BLUE;//设置字体为蓝色 //检测 SD 卡成功 LCD_ShowString(60,150,200,16,16,"SD Card OK "); LCD_ShowString(60,170,200,16,16,"SD Card Size: MB"); sd_size=SD_GetSectorCount();//得到扇区数 LCD_ShowNum(164,170,sd_size>>11,5,16);//显示 SD 卡容量 while(1) { www.openedv.com 577 ALIENTEK 战舰STM32开发板 key=KEY_Scan(0); if(key==KEY_RIGHT)//KEY0 按下了 { buf=mymalloc(0,512); //申请内存 if(SD_ReadDisk(buf,0,1)==0) //读取 0 扇区的内容 { LCD_ShowString(60,190,200,16,16,"USART1 Sending Data..."); printf("SECTOR 0 DATA:\r\n"); for(sd_size=0;sd_size<512;sd_size++)printf("%x ",buf[sd_size]);//打印 0 扇区数据 printf("\r\nDATA ENDED\r\n"); LCD_ShowString(60,190,200,16,16,"USART1 Send Data Over!"); } myfree(0,buf);//释放内存 } t++; delay_ms(10); if(t==20) { LED0=!LED0; t=0; } } } 这里我们通过 SD_GetSectorCount 函数来得到 SD 卡的扇区数,间接得到 SD 卡容量,然后 在液晶上显示出来,接着我们通过按键 KEY0 控制读取 SD 卡的扇区 0,然后把读到的数据通过 串口打印出来。这里,我们对上一章学过的内存管理小试牛刀,稍微用了下,以后我们会尽量 使用内存管理来设计。 44.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK 战舰 STM32 开发板上,可以看到 LCD 显示如图 44.4.1 所示的内容(默认 SD 卡已经接上了): www.openedv.com 578 ALIENTEK 战舰STM32开发板 图 44.4.1 程序运行效果图 打开串口调试助手,按下 KEY0 就可以看到从开发板发回来的数据了,如图 44.4.2 所示: 图 44.4.2 串口收到的 SD 卡扇区 0 内容 这里请大家注意,不同的 SD 卡,读出来的扇区 0 是不尽相同的,所以不要因为你读出来 的数据和图 44.4.2 不同而感到惊讶。 www.openedv.com 579 ALIENTEK 战舰STM32开发板 第四十五章 FATFS 实验 上一章,我们学习了 SD 卡的使用,不过仅仅是简单的实现读扇区而已,真正要好好应用 SD 卡,必须使用文件系统管理,本章,我们将使用 FATFS 来管理 SD 卡,实现 SD 卡文件的读 写等基本功能。本章分为如下几个部分: 45.1 FATFS 简介 45.2 硬件设计 45.3 软件设计 45.4 下载验证 www.openedv.com 580 ALIENTEK 战舰STM32开发板 45.1 FATFS 简介 FATFS 是一个完全免费开源的 FAT 文件系统模块,专门为小型的嵌入式系统而设计。它完 全用标准 C 语言编写,所以具有良好的硬件平台独立性,可以移植到 8051、PIC、AVR、SH、 Z80、H8、ARM 等系列单片机上而只需做简单的修改。它支持 FATl2、FATl6 和 FAT32,支持 多个存储媒介;有独立的缓冲区,可以对多个文件进行读/写,并特别对 8 位单片机和 16 位 单片机做了优化。 FATFS 的特点有:  Windows 兼容的 FAT 文件系统(支持 FAT12/FAT16/FAT32)  与平台无关,移植简单  代码量少、效率高  多种配置选项  支持多卷(物理驱动器或分区,最多 10 个卷)  多个 ANSI/OEM 代码页包括 DBCS  支持长文件名、ANSI/OEM 或 Unicode  支持 RTOS  支持多种扇区大小  只读、最小化的 API 和 I/O 缓冲区等 FATFS 的这些特点,加上免费、开源的原则,使得 FATFS 应用非常广泛。FATFS 模块的层 次结构如图 45.1.1 所示: 应用层 FATFS模块 底层存储媒介接口 (SD卡/ATA/USB/NAND) RTC 图 45.1.1 FATFS 层次结构图 最顶层是应用层,使用者无需理会 FATFS 的内部结构和复杂的 FAT 协议,只需要调用 FATFS 模块提供给用户的一系列应用接口函数,如 f_open,f_read,f_write 和 f_close 等,就可 以像在 PC 上读/写文件那样简单。 中间层 FATFS 模块,实现了 FAT 文件读/写协议。FATFS 模块提供的是 ff.c 和 ff.h。除非 有必要,使用者一般不用修改,使用时将头文件直接包含进去即可。 需要我们编写移植代码的是 FATFS 模块提供的底层接口,它包括存储媒介读/写接口(disk I/O)和供给文件创建修改时间的实时时钟。 FATFS 的源码,大家可以在:http://elm-chan.org/fsw/ff/00index_e.html 这个网站下载到,目 前最新版本为 R0.09a。本章我们就使用最新版本的的 FATFS 作为介绍,下载最新版本的 FATFS 软件包,解压后可以得到两个文件夹:doc 和 src。doc 里面主要是对 FATFS 的介绍,而 src 里 www.openedv.com 581 ALIENTEK 战舰STM32开发板 面才是我们需要的源码。 其中,与平台无关的是: ffconf.h FATFS 模块配置文件 ff.h FATFS 和应用模块公用的包含文件 ff.c FATFS 模块 diskio.h FATFS 和 disk I/O 模块公用的包含文件 interger.h 数据类型定义 option 可选的外部功能(比如支持中文等) 与平台相关的代码(需要用户提供)是: diskio.c FATFS 和 disk I/O 模块接口层文件 FATFS 模块在移植的时候,我们一般只需要修改 2 个文件,即 ffconf.h 和 diskio.c。FATFS 模块的所有配置项都是存放在 ffconf.h 里面,我们可以通过配置里面的一些选项,来满足自己 的需求。接下来我们介绍几个重要的配置选项。 1)_FS_TINY。这个选项在 R0.07 版本中开始出现,之前的版本都是以独立的 C 文件出现 (FATFS 和 Tiny FATFS),有了这个选项之后,两者整合在一起了,使用起来更方便。我们使 用 FATFS,所以把这个选项定义为 0 即可。 2)_FS_READONLY。这个用来配置是不是只读,本章我们需要读写都用,所以这里设置 为 0 即可。 3)_USE_STRFUNC。这个用来设置是否支持字符串类操作,比如 f_putc,f_puts 等,本章 我们需要用到,故设置这里为 1。 4)_USE_MKFS。这个用来定时是否使能格式化,本章需要用到,所以设置这里为 1。 5)_USE_FASTSEEK。这个用来使能快速定位,我们设置为 1,使能快速定位。 6)_CODE_PAGE。这个用于设置语言类型,包括很多选项(见 FATFS 官网说明),我们 这里设置为 936,即简体中文(GBK 码,需要 c936.c 文件支持,该文件在 option 文件夹)。 7)_USE_LFN。该选项用于设置是否支持长文件名(还需要_CODE_PAGE 支持),取值范 围为 0~3。0,表示不支持长文件名,1~3 是支持长文件名,但是存储地方不一样,我们选择使 用 3,通过 ff_memalloc 函数来动态分配长文件名的存储区域。 8)_VOLUMES。用于设置 FATFS 支持的逻辑设备数目,我们设置为 2,即支持 2 个设备。 9)_MAX_SS。扇区缓冲的最大值,一般设置为 512。 其他配置项,我们这里就不一一介绍了,FATFS 的说明文档里面有很详细的介绍,大家自 己阅读即可。下面我们来讲讲 FATFS 的移植,FATFS 的移植主要分为 3 步: ① 数据类型:在 integer.h 里面去定义好数据的类型。这里需要了解你用的编译器的数 据类型,并根据编译器定义好数据类型。 ② 配置:通过 ffconf.h 配置 FATFS 的相关功能,以满足你的需要。 ③ 函数编写:打开 diskio.c,进行底层驱动编写,一般需要编写 6 个接口函数,如 图 45.1.2 所示: www.openedv.com 图 45.1.2 diskio 需要实现的函数 582 ALIENTEK 战舰STM32开发板 通过以上三步,我们即可完成对 FATFS 的移植。 第一步,我们使用的是 MDK3.80a 编译器,器数据类型和 integer.h 里面定义的一致,所以 此步,我们不需要做任何改动。 第二步,关于 ffconf.h 里面的相关配置,我们在前面已经有介绍(之前介绍的 9 个配置), 我们将对应配置修改为我们介绍时候的值即可,其他的配置用默认配置。 第三步,因为 FATFS 模块完全与磁盘 I/O 层分开,因此需要下面的函数来实现底层物理磁 盘的读写与获取当前时间。底层磁盘 I/O 模块并不是 FATFS 的一部分,并且必须由用户提供。 这些函数一般有 6 个,在 diskio.c 里面。 首先是 disk_initialize 函数,该函数介绍如图 45.1.3 所示: 图 45.1.3 disk_initialize 函数介绍 第二个函数是 disk_status 函数,该函数介绍如图 45.1.4 所示: 图 45.1.4 disk_status 函数介绍 第三个函数是 disk_read 函数,该函数介绍如图 45.1.5 所示: www.openedv.com 583 ALIENTEK 战舰STM32开发板 图 45.1.5 disk_read 函数介绍 第四个函数是 disk_write 函数,该函数介绍如图 45.1.6 所示: 图 45.1.6 disk_write 函数介绍 第五个函数是 disk_ioctl 函数,该函数介绍如图 45.1.7 所示: www.openedv.com 584 ALIENTEK 战舰STM32开发板 图 45.1.7 disk_ioctl 函数介绍 最后一个函数是 get_fattime 函数,该函数介绍如图 45.1.8 所示: 图 45.1.8 get_fattime 函数介绍 以上六个函数,我们将在软件设计部分一一实现。通过以上 3 个步骤,我们就完成了对 FATFS 的移植,就可以在我们的代码里面使用 FATFS 了。 FATFS 提供了很多 API 函数,这些函数 FATFS 的自带介绍文件里面都有详细的介绍(包括 参考代码),我们这里就不多说了。这里需要注意的是,在使用 FATFS 的时候,必须先通过 f_mount 函数注册一个工作区,才能开始后续 API 的使用,关于 FATFS 的介绍,我们就介绍到这里。大 家可以通过 FATFS 自带的介绍文件进一步了解和熟悉 FATFS 的使用。 www.openedv.com 585 ALIENTEK 战舰STM32开发板 45.2 硬件设计 本章实验功能简介:开机的时候先初始化 SD 卡,初始化成功之后,注册两个工作区(一 个给 SD 卡用,一个给 SPI FLASH 用),然后获取 SD 卡的容量和剩余空间,并显示在 LCD 模块上,最后等待 USMART 输入指令进行各项测试。本实验通过 DS0 指示程序运行状态。 本实验用到的硬件资源有: 1) 指示灯 DS0 2) 串口 3) TFTLCD 模块 4) SD 卡接口 5) SD 卡 6) SPI FLASH 这些,我们在之前都已经介绍过,如有不清楚,请参考之前内容。 45.3 软件设计 打开我们的 FATFS 实验工程可以看到,我们将 FATFS 部分单独做一个分组,在工程目录 下新建一个 FATFS 的文件夹,然后将 FATFS R0.09a 程序包解压到该文件夹下。同时,我们在 FATFS 文件夹里面新建一个 exfuns 的文件夹,用于存放我们针对 FATFS 做的一些扩展代码。 设计完如图 45.3.1 所示: 图 45.3.1 FATFS 文件夹子目录 下面我们将对工程中 FATFS 部分的代码进行讲解。 打开 diskio.c,代码如下: #include "mmc_sd.h" #include "diskio.h" #include "flash.h" www.openedv.com 586 ALIENTEK 战舰STM32开发板 #include "malloc.h" #define SD_CARD 0 //SD 卡,卷标为 0 #define EX_FLASH 1 //外部 flash,卷标为 1 #define FLASH_SECTOR_SIZE 512 //对于 W25Q64 //前 6M 字节给 fatfs 用,6M 字节后~6M+500K 给用户用,6M+500K 以后,用于存放字库, //字库占用 1.5M. u16 FLASH_SECTOR_COUNT=2048*6; //6M 字节,默认为 W25Q64 #define FLASH_BLOCK_SIZE 8 //每个 BLOCK 有 8 个扇区 //初始化磁盘 DSTATUS disk_initialize ( BYTE drv /* Physical drive nmuber (0..) */ ) { u8 res=0; switch(drv) { case SD_CARD://SD 卡 res = SD_Initialize();//SD_Initialize() if(res)//STM32 SPI 的 bug,在 sd 卡操作失败的时候如果不执行下面的语句, { //可能导致 SPI 读写异常 SD_SPI_SpeedLow(); SD_SPI_ReadWriteByte(0xff);//提供额外的 8 个时钟 SD_SPI_SpeedHigh(); } break; case EX_FLASH://外部 flash SPI_Flash_Init(); if(SPI_FLASH_TYPE==W25Q64)FLASH_SECTOR_COUNT=2048*6; else FLASH_SECTOR_COUNT=2048*2; //其他 break; default: res=1; } if(res)return STA_NOINIT; else return 0; //初始化成功 } //获得磁盘状态 DSTATUS disk_status ( BYTE drv /* Physical drive nmuber (0..) */ ) { return 0; www.openedv.com 587 ALIENTEK 战舰STM32开发板 } //读扇区 //drv:磁盘编号 0~9 //*buff:数据接收缓冲首地址 //sector:扇区地址 //count:需要读取的扇区数 DRESULT disk_read ( BYTE drv, /* Physical drive nmuber (0..) */ BYTE *buff, /* Data buffer to store read data */ DWORD sector, /* Sector address (LBA) */ BYTE count /* Number of sectors to read (1..255) */ ) { u8 res=0; if (!count)return RES_PARERR;//count 不能等于 0,否则返回参数错误 switch(drv) { case SD_CARD://SD 卡 res=SD_ReadDisk(buff,sector,count); if(res) //STM32 SPI 的 bug,在 sd 卡操作失败的时候如果不执行下面的语句, { //可能导致 SPI 读写异常 SD_SPI_SpeedLow(); SD_SPI_ReadWriteByte(0xff);//提供额外的 8 个时钟 SD_SPI_SpeedHigh(); } break; case EX_FLASH://外部 flash for(;count>0;count--) { SPI_Flash_Read(buff,sector*FLASH_SECTOR_SIZE,FLASH_SECTOR_SIZE); sector++; buff+=FLASH_SECTOR_SIZE; } res=0; break; default: res=1; } //处理返回值,将 SPI_SD_driver.c 的返回值转成 ff.c 的返回值 if(res==0x00)return RES_OK; else return RES_ERROR; } //写扇区 //drv:磁盘编号 0~9 www.openedv.com 588 ALIENTEK 战舰STM32开发板 //*buff:发送数据首地址 //sector:扇区地址 //count:需要写入的扇区数 #if _READONLY == 0 DRESULT disk_write ( BYTE drv, /* Physical drive nmuber (0..) */ const BYTE *buff, /* Data to be written */ DWORD sector, /* Sector address (LBA) */ BYTE count /* Number of sectors to write (1..255) */ ) { u8 res=0; if (!count)return RES_PARERR;//count 不能等于 0,否则返回参数错误 switch(drv) { case SD_CARD://SD 卡 res=SD_WriteDisk((u8*)buff,sector,count); break; case EX_FLASH://外部 flash for(;count>0;count--) { SPI_Flash_Write((u8*)buff,sector*FLASH_SECTOR_SIZE, FLASH_SECTOR_SIZE); sector++; buff+=FLASH_SECTOR_SIZE; } res=0; break; default: res=1; } //处理返回值,将 SPI_SD_driver.c 的返回值转成 ff.c 的返回值 if(res == 0x00)return RES_OK; else return RES_ERROR; } #endif /* _READONLY */ //其他表参数的获得 //drv:磁盘编号 0~9 //ctrl:控制代码 //*buff:发送/接收缓冲区指针 DRESULT disk_ioctl ( BYTE drv, /* Physical drive nmuber (0..) */ BYTE ctrl, /* Control code */ www.openedv.com 589 ALIENTEK 战舰STM32开发板 void *buff /* Buffer to send/receive control data */ ) { DRESULT res; if(drv==SD_CARD)//SD 卡 { switch(ctrl) { case CTRL_SYNC: SD_CS=0; if(SD_WaitReady()==0)res = RES_OK; else res = RES_ERROR; SD_CS=1; break; case GET_SECTOR_SIZE: *(WORD*)buff = 512; res = RES_OK; break; case GET_BLOCK_SIZE: *(WORD*)buff = 8; res = RES_OK; break; case GET_SECTOR_COUNT: *(DWORD*)buff = SD_GetSectorCount();res = RES_OK; break; default: res = RES_PARERR; break; } }else if(drv==EX_FLASH) //外部 FLASH { switch(ctrl) { case CTRL_SYNC: res = RES_OK; break; case GET_SECTOR_SIZE: *(WORD*)buff = FLASH_SECTOR_SIZE; res = RES_OK; break; case GET_BLOCK_SIZE: *(WORD*)buff = FLASH_BLOCK_SIZE; res = RES_OK; break; case GET_SECTOR_COUNT: www.openedv.com 590 ALIENTEK 战舰STM32开发板 *(DWORD*)buff = FLASH_SECTOR_COUNT; res = RES_OK; break; default: res = RES_PARERR; break; } }else res=RES_ERROR;//其他的不支持 return res; } //获得时间 //User defined function to give a current time to fatfs module */ //31-25: Year(0-127 org.1980), 24-21: Month(1-12), 20-16: Day(1-31) */ //15-11: Hour(0-23), 10-5: Minute(0-59), 4-0: Second(0-29 *2) */ DWORD get_fattime (void) { return 0; } //动态分配内存 void *ff_memalloc (UINT size) { return (void*)mymalloc(SRAMIN,size); } //释放内存 void ff_memfree (void* mf) { myfree(SRAMIN,mf); } 该函数实现了我们 45.1 节提到的 6 个函数,同时因为在 ffconf.h 里面设置对长文件名的 支持为方法 3,所以必须实现 ff_memalloc 和 ff_memfree 这两个函数。本章,我们用 FATFS 管 理了 2 个磁盘:SD 卡和 SPI FLASH。SD 卡比较好说,但是 SPI FLASH,因为其扇区是 4K 字节大 小,我们为了方便设计,强制将其扇区定义为 512 字节,这样带来的好处就是设计使用相对简 单,坏处就是擦除次数大增,所以不要随便往 SPI FLASH 里面写数据,非必要最好别写,如果 频繁写的话,很容易将 SPI FLASH 写坏。 保存 diskio.c,然后打开 ffconf.h,修改相关配置,并保存,此部分就不贴代码了,请大 家参考光盘源码。 前面提到,我们在 FATFS 文件夹下还新建了一个 exfuns 的文件夹,该文件夹用于保存一些 FATFS 一些针对 FATFS 的扩展代码,本章,我们编写了 4 个文件,分别是:exfuns.c、exfuns.h、 fattester.c 和 fattester.h。其中 exfuns.c 主要定义了一些全局变量,方便 FATFS 的使用, 同时实现了磁盘容量获取等函数。而 fattester.c 文件则主要是为了测试 FATFS 用,因为 FATFS 的很多函数无法直接通过 USMART 调用,所以我们在 fattester.c 里面对这些函数进行了一次再 封装,使得可以通过 USMART 调用。这几个文件的代码,我们就不贴出来了,请大家参考光盘源 www.openedv.com 591 ALIENTEK 战舰STM32开发板 码,我们将 exfuns.c 和 fattester.c 加入 FATFS 组下,同时将 exfuns 文件夹加入头文件包含 路径。 然后,我们打开 main.c,修改 main 函数如下: int main(void) { u32 total,free; u8 t=0; delay_init(); NVIC_Configuration(); uart_init(9600); LED_Init(); LCD_Init(); KEY_Init(); exfuns_init(); //延时函数初始化 //设置 NVIC 中断分组 2:2 位抢占优先级,2 位响应优先级 //串口初始化波特率为 9600 //LED 端口初始化 //初始化 LCD //初始化按键 //为 fatfs 相关变量申请内存 usmart_dev.init(72); mem_init(SRAMIN); //初始化内部内存池 POINT_COLOR=RED;//设置字体为红色 LCD_ShowString(60,50,200,16,16,"WarShip STM32"); LCD_ShowString(60,70,200,16,16,"FATFS TEST"); LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(60,110,200,16,16,"Use USMART for test"); LCD_ShowString(60,130,200,16,16,"2012/9/18"); while(SD_Initialize()) //检测 SD 卡 { LCD_ShowString(60,150,200,16,16,"SD Card Error!"); delay_ms(200); LCD_Fill(60,150,240,150+16,WHITE);//清除显示 delay_ms(200); LED0=!LED0;//DS0 闪烁 } exfuns_init(); f_mount(0,fs[0]); f_mount(1,fs[1]); while(exf_getfree("0",&total,&free)) //为 fatfs 相关变量申请内存 //挂载 SD 卡 //挂载 FLASH. //得到 SD 卡的总容量和剩余容量 { LCD_ShowString(60,150,200,16,16,"Fatfs Error!"); delay_ms(200); LCD_Fill(60,150,240,150+16,WHITE);//清除显示 delay_ms(200); LED0=!LED0;//DS0 闪烁 } POINT_COLOR=BLUE;//设置字体为蓝色 LCD_ShowString(60,150,200,16,16,"FATFS OK!"); www.openedv.com 592 ALIENTEK 战舰STM32开发板 LCD_ShowString(60,170,200,16,16,"SD Total Size: MB"); LCD_ShowString(60,190,200,16,16,"SD Free Size: MB"); LCD_ShowNum(172,170,total>>10,5,16); //显示 SD 卡总容量 MB LCD_ShowNum(172,190,free>>10,5,16); //显示 SD 卡剩余容量 MB while(1) { t++; delay_ms(200); LED0=!LED0; } } 在 main 函数里面,我们为 SD 卡和 FLASH 都注册了工作区(挂载),在初始化 SD 卡并显示 其容量信息后,进入死循环,等待 USMART 测试。 最后,我们在 usmart_config.c 里面的 usmart_nametab 数组添加如下内容: (void*)mf_mount,"u8 mf_mount(u8 drv)", (void*)mf_open,"u8 mf_open(u8*path,u8 mode)", (void*)mf_close,"u8 mf_close(void)", (void*)mf_read,"u8 mf_read(u16 len)", (void*)mf_write,"u8 mf_write(u8*dat,u16 len)", (void*)mf_opendir,"u8 mf_opendir(u8* path)", (void*)mf_readdir,"u8 mf_readdir(void)", (void*)mf_scan_files,"u8 mf_scan_files(u8 * path)", (void*)mf_showfree,"u32 mf_showfree(u8 *drv)", (void*)mf_lseek,"u8 mf_lseek(u32 offset)", (void*)mf_tell,"u32 mf_tell(void)", (void*)mf_size,"u32 mf_size(void)", (void*)mf_mkdir,"u8 mf_mkdir(u8*pname)", (void*)mf_fmkfs,"u8 mf_fmkfs(u8 drv,u8 mode,u16 au)", (void*)mf_unlink,"u8 mf_unlink(u8 *pname)", (void*)mf_rename,"u8 mf_rename(u8 *oldname,u8* newname)", (void*)mf_gets,"void mf_gets(u16 size)", (void*)mf_putc,"u8 mf_putc(u8 c)", (void*)mf_puts,"u8 mf_puts(u8*c)", 这些函数均是在 fattester.c 里面实现,通过调用这些函数,即可实现对 FATFS 对应 API 函数的测试。 至此,软件设计部分就结束了。 45.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK 战舰 STM32 开发板上,可以看到 LCD 显示如图 45.4.1 所示的内容(默认 SD 卡已经接上了): www.openedv.com 593 ALIENTEK 战舰STM32开发板 图 45.4.1 程序运行效果图 打开串口调试助手,我们就可以串口调用前面添加的各种 FATFS 测试函数了,比如我们输 入 mf_scan_files("0:"),即可扫描 SD 卡根目录的所有文件,如图 45.4.2 所示: 图 45.4.2 扫描 SD 卡根目录所有文件 其他函数的测试,用类似的办法即可实现。注意这里 0 代表 SD 卡,1 代表 SPI FLASH。 另外,提醒大家,mf_unlink 函数,在删除文件夹的时候,必须保证文件夹是空的,才可以正常 删除,否则不能删除。 www.openedv.com 594 ALIENTEK 战舰STM32开发板 第四十六章 汉字显示实验 汉字显示在很多单片机系统都需要用到,少则几个字,多则整个汉字库的支持,更有甚者 还要支持多国字库,那就更麻烦了。本章,我们将向大家介绍,如何用 STM32 控制 LCD 显示 汉字。在本章中,我们将使用外部 FLASH 来存储字库,并可以通过 SD 卡更新字库。STM32 读取存在 FLASH 里面的字库,然后将汉字显示在 LCD 上面。本章分为如下几个部分: 46.1 汉字显示原理简介 46.2 硬件设计 46.3 软件设计 46.4 下载验证 www.openedv.com 595 ALIENTEK 战舰STM32开发板 46.1 汉字显示原理简介 常用的汉字内码系统有 GB2312,GB13000,GBK,BIG5(繁体)等几种,其中 GB2312 支持的汉字仅有几千个,很多时候不够用,而 GBK 内码不仅完全兼容 GB2312,还支持了繁体 字,总汉字数有 2 万多个,完全能满足我们一般应用的要求。 本实例我们将制作一个 GBK 字库,制作好的字库放在 SD 卡里面,然后通过 SD 卡,将字 库文件复制到外部 FLASH 芯片 W25Q64 里,这样,W25Q64 就相当于一个汉字字库芯片了。 汉字在液晶上的显示原理与前面显示字符的是一样的。汉字在液晶上的显示其实就是一些 点的显示与不显示,这就相当于我们的笔一样,有笔经过的地方就画出来,没经过的地方就不 画。所以要显示汉字,我们首先要知道汉字的点阵数据,这些数据可以由专门的软件来生成。 只要知道了一个汉字点阵的生成方法,那么我们在程序里面就可以把这个点阵数据解析成一个 汉字。 知道显示了一个汉字,就可以推及整个汉字库了。汉字在各种文件里面的存储不是以点阵 数据的形式存储的(否则那占用的空间就太大了),而是以内码的形式存储的,就是 GB2312/GBK/BIG5 等这几种的一种,每个汉字对应着一个内码,在知道了内码之后再去字库 里面查找这个汉字的点阵数据,然后在液晶上显示出来。这个过程我们是看不到,但是计算机 是要去执行的。 单片机要显示汉字也与此类似:汉字内码(GBK/GB2312)查找点阵库解析显示。 所以只要我们有了整个汉字库的点阵,就可以把电脑上的文本信息在单片机上显示出来了。 这里我们要解决的最大问题就是制作一个与汉字内码对的上号的汉字点阵库。而且要方便单片 机的查找。每个 GBK 码由 2 个字节组成,第一个字节为 0X81~0XFE,第二个字节分为两部分, 一是 0X40~0X7E,二是 0X80~0XFE。其中与 GB2312 相同的区域,字完全相同。 我们把第一个字节代表的意义称为区,那么 GBK 里面总共有 126 个区(0XFE-0X81+1), 每个区内有 190 个汉字(0XFE-0X80+0X7E-0X40+2),总共就有 126*190=23940 个汉字。我 们的点阵库只要按照这个编码规则从 0X8140 开始,逐一建立,每个区的点阵大小为每个汉字 所用的字节数*190。这样,我们就可以得到在这个字库里面定位汉字的方法: 当 GBKL<0X7F 时:Hp=((GBKH-0x81)*190+GBKL-0X40)*(size*2); 当 GBKL>0X80 时:Hp=((GBKH-0x81)*190+GBKL-0X41)*(size*2); 其中 GBKH、GBKL 分别代表 GBK 的第一个字节和第二个字节(也就是高位和低位),size 代表汉字字体的大小(比如 16 字体,12 字体等),Hp 则为对应汉字点阵数据在字库里面的起 始地址(假设是从 0 开始存放)。 这样我们只要得到了汉字的 GBK 码,就可以显示这个汉字了。从而实现汉字在液晶上的 显示。 上一章,我们提到要用 cc936.c,以支持长文件名,但是 cc936.c 文件里面的两个数组太大 了(172KB),直接刷在单片机里面,太占用 flash 了,所以我们必须把这两个数组存放在外部 flash。cc936 里面包含的两个数组 oem2uni 和 uni2oem 存放 unicode 和 gbk 的互相转换对照表, 这两个数组很大,这里我们利用 ALIENTEK 提供的一个 C 语言数组转 BIN(二进制)的软件: C2B 转换助手 V1.1.exe,将这两个数组转为 BIN 文件,我们将这两个数组拷贝出来存放为一个 新的文本文件,假设为 UNIGBK.TXT,然后用 C2B 转换助手打开这个文本文件,如图 46.1.1 所示: www.openedv.com 596 ALIENTEK 战舰STM32开发板 图 46.1.1 C2B 转换助手 然后点击转换,就可以在当前目录下(文本文件所在目录下)得到一个 UNIGBK.bin 的文 件。这样就完成将 C 语言数组转换为.bin 文件,然后只需要将 UNIGBK.bin 保存到外部 FLASH 就实现了该数组的转移。 在 cc936.c 里面,主要是通过 ff_convert 调用这两个数组,实现 UNICODE 和 GBK 的互转, 该函数原代码如下: WCHAR ff_convert ( /* Converted code, 0 means conversion error */ WCHAR src, /* Character code to be converted */ UINT dir /* 0: Unicode to OEMCP, 1: OEMCP to Unicode */ ) { const WCHAR *p; WCHAR c; int i, n, li, hi; if (src < 0x80) { /* ASCII */ c = src; } else { if (dir) { /* OEMCP to unicode */ p = oem2uni; hi = sizeof(oem2uni) / 4 - 1; } else { /* Unicode to OEMCP */ p = uni2oem; hi = sizeof(uni2oem) / 4 - 1; } li = 0; for (n = 16; n; n--) { i = li + (hi - li) / 2; if (src == p[i * 2]) break; www.openedv.com 597 ALIENTEK 战舰STM32开发板 if (src > p[i * 2]) li = i; else hi = i; } c = n ? p[i * 2 + 1] : 0; } return c; } 此段代码,通过二分法(16 阶)在数组里面查找 UNICODE(或 GBK)码对应的 GBK(或 UNICODE)码。当我们将数组存放在外部 flash 的时候,将该函数修改为: WCHAR ff_convert ( /* Converted code, 0 means conversion error */ WCHAR src, /* Character code to be converted */ UINT dir /* 0: Unicode to OEMCP, 1: OEMCP to Unicode */ ) { WCHAR t[2]; WCHAR c; u32 i, li, hi; u16 n; u32 gbk2uni_offset=0; if (src < 0x80)c = src;//ASCII,直接不用转换. else { if(dir) gbk2uni_offset=ftinfo.ugbksize/2; //GBK 2 UNICODE else gbk2uni_offset=0; //UNICODE 2 GBK /* Unicode to OEMCP */ hi=ftinfo.ugbksize/2;//对半开. hi =hi / 4 - 1; li = 0; for (n = 16; n; n--) { i = li + (hi - li) / 2; SPI_Flash_Read((u8*)&t,ftinfo.ugbkaddr+i*4+gbk2uni_offset,4);//读出 4 个字节 if (src == t[0]) break; if (src > t[0])li = i; else hi = i; } c = n ? t[1] : 0; } return c; } 代码中的 ftinfo.ugbksize 为我们刚刚生成的 UNIGBK.bin 的大小,而 ftinfo.ugbkaddr 是我们 存放 UNIGBK.bin 文件的首地址。这里同样采用的是二分法查找,关于 cc936.c 的修改,我们就 介绍到这。 www.openedv.com 598 ALIENTEK 战舰STM32开发板 字库的生成,我们要用到一款软件,由易木雨软件工作室设计的点阵字库生成器 V3.8。该 软件可以在 WINDOWS 系统下生成任意点阵大小的 ASCII,GB2312(简体中文)、GBK(简体中 文)、BIG5(繁体中文)、HANGUL(韩文)、SJIS(日文)、Unicode 以及泰文,越南文、俄文、乌克 兰文,拉丁文,8859 系列等共二十几种编码的字库,不但支持生成二进制文件格式的文件,也 可以生成 BDF 文件,还支持生成图片功能,并支持横向,纵向等多种扫描方式,且扫描方式 可以根据用户的需求进行增加。该软件的界面如图 46.1.1 所示: 图 46.1.2 点阵字库生成器默认界面 比如我们要生成 16*16 的 GBK 字库,则选择:936 中文 PRC GBK,字宽和高均选择 16, 字体大小选择 12(比较适合),然后模式选择纵向取模方式二(字节高位在前,低位在后),最 后点击创建,就可以开始生成我们需要的字库了。具体设置如图 46.1.3 所示: 图 46.1.3 生成 GBK16*16 字库的设置方法 这里注意,软件里面的字体大小并不是我们生成点阵的大小,12 字体是 XP 的叫法,我们 字体的大小以宽和高的大小来决定!可以简单的这么认为:XP 的 12 字体,基本上就等于 16*16 大小。该软件还可以生成其他很多字库,字体也可选,详细的介绍请看软件自带的《点阵字库 生成器说明书》。 本章,我们生成两个字库文件 GBK12.DZK 和 GBK16.DZK,并将后缀名改为.fon(方便识 别^_^),备用。关于汉字显示原理,我们就介绍到这。 www.openedv.com 599 ALIENTEK 战舰STM32开发板 46.2 硬件设计 本章实验功能简介:开机的时候先检测 W25Q64 中是否已经存在字库,如果存在,则按次 序显示汉字(两种字体都显示)。如果没有,则检测 SD 卡和文件系统,并查找 SYSTEM 文件夹 下的 FONT 文件夹,在该文件夹内查找 UNIGBK.BIN、GBK12.FON 和 GBK16.FON(这几个 文件的的由来,我们前面已经介绍了)。在检测到这些文件之后,就开始更新字库,更新完毕 才开始显示汉字。同样我们也是用 DS0 来指示程序正在运行。 所要用到的硬件资源如下: 1) 指示灯 DS0 2) 串口 3) TFTLCD 模块 4) SD 卡接口 5) SD 卡 6) SPI FLASH 这几部分分,在之前的实例中都介绍过了,我们在此就不介绍了。 46.3 软件设计 打开汉字显示实验可以看到,我们在 TEXT 文件夹下新建 fontupd.c、fontupd.h、text.c、text.h 这 4 个文件。并将该文件夹加入了头文件包含路径。 打开 fontupd.c,该文件代码如下: #include "fontupd.h" #include "ff.h" #include "flash.h" #include "lcd.h" #include "malloc.h" u32 FONTINFOADDR=(1024*6+500)*1024;//默认是 6M+500K 后开始 //字库信息结构体. //用来保存字库基本信息,地址,大小等 _font_info ftinfo; //在 sd 卡中的路径 const u8 *GBK16_SDPATH="0:/SYSTEM/FONT/GBK16.FON"; //GBK16 存放位置 const u8 *GBK12_SDPATH="0:/SYSTEM/FONT/GBK12.FON"; //GBK12 存放位置 const u8 *UNIGBK_SDPATH="0:/SYSTEM/FONT/UNIGBK.BIN"; //UNIGBK.BIN 存放位置 //在 25Qxx 中的路径 const u8 *GBK16_25QPATH="1:/SYSTEM/FONT/GBK16.FON"; //GBK12 的存放位置 const u8 *GBK12_25QPATH="1:/SYSTEM/FONT/GBK12.FON"; //GBK12 的存放位置 const u8 *UNIGBK_25QPATH="1:/SYSTEM/FONT/UNIGBK.BIN";//UNIGBK.BIN 存放位置 //显示当前字体更新进度 //x,y:坐标 //size:字体大小 //fsize:整个文件大小 //pos:当前文件指针位置 u32 fupd_prog(u16 x,u16 y,u8 size,u32 fsize,u32 pos) www.openedv.com 600 ALIENTEK 战舰STM32开发板 { float prog; u8 t=0XFF; prog=(float)pos/fsize; prog*=100; if(t!=prog) { LCD_ShowString(x+3*size/2,y,240,320,size,"%"); t=prog; if(t>100)t=100; LCD_ShowNum(x,y,t,3,size);//显示数值 } return 0; } //更新某一个 //x,y:坐标 //size:字体大小 //fxpath:路径 //fx:更新的内容 0,ungbk;1,gbk12;2,gbk16; //返回值:0,成功;其他,失败. u8 updata_fontx(u16 x,u16 y,u8 size,u8 *fxpath,u8 fx) { u32 flashaddr=0; FIL * fftemp; u8 *tempbuf; u8 res; u16 bread; u32 offx=0; u8 rval=0; fftemp=(FIL*)mymalloc(SRAMIN,sizeof(FIL)); //分配内存 if(fftemp==NULL)rval=1; tempbuf=mymalloc(SRAMIN,4096); //分配 4096 个字节空间 if(tempbuf==NULL)rval=1; res=f_open(fftemp,(const TCHAR*)fxpath,FA_READ); if(res)rval=2;//打开文件失败 if(rval==0) { if(fx==0) //更新 UNIGBK.BIN { ftinfo.ugbkaddr=FONTINFOADDR+sizeof(ftinfo); //信息头之后,紧跟 UNIGBK 转换码表 ftinfo.ugbksize=fftemp->fsize; //UNIGBK 大小 flashaddr=ftinfo.ugbkaddr; www.openedv.com 601 ALIENTEK 战舰STM32开发板 }else if(fx==1) //GBK12 { ftinfo.f12addr=ftinfo.ugbkaddr+ftinfo.ugbksize; //UNIGBK 后,跟 GBK12 字库 ftinfo.gbk12size=fftemp->fsize; //GBK12 字库大小 flashaddr=ftinfo.f12addr; //GBK12 的起始地址 }else //GBK16 { ftinfo.f16addr=ftinfo.f12addr+ftinfo.gbk12size; //GBK12 后,跟 GBK16 字库 ftinfo.gkb16size=fftemp->fsize; //GBK16 字库大小 flashaddr=ftinfo.f16addr; //GBK16 的起始地址 } while(res==FR_OK)//死循环执行 { res=f_read(fftemp,tempbuf,4096,(UINT *)&bread);//读取数据 if(res!=FR_OK)break; //执行错误 SPI_Flash_Write(tempbuf,offx+flashaddr,4096); //从 0 开始写入 4096 个数据 offx+=bread; fupd_prog(x,y,size,fftemp->fsize,offx); //进度显示 if(bread!=4096)break; //读完了. } f_close(fftemp); } myfree(SRAMIN,fftemp); //释放内存 myfree(SRAMIN,tempbuf); //释放内存 return res; } //更新字体文件,UNIGBK,GBK12,GBK16 一起更新 //x,y:提示信息的显示地址 //size:字体大小 //提示信息字体大小 //src:0,从 SD 卡更新. // 1,从 25QXX 更新 //返回值:0,更新成功; // 其他,错误代码. u8 update_font(u16 x,u16 y,u8 size,u8 src) { u8 *gbk16_path; u8 *gbk12_path; u8 *unigbk_path; u8 res; if(src)//从 25qxx 更新 { unigbk_path=(u8*)UNIGBK_25QPATH; www.openedv.com 602 ALIENTEK 战舰STM32开发板 gbk12_path=(u8*)GBK12_25QPATH; gbk16_path=(u8*)GBK16_25QPATH; }else//从 sd 卡更新 { unigbk_path=(u8*)UNIGBK_SDPATH; gbk12_path=(u8*)GBK12_SDPATH; gbk16_path=(u8*)GBK16_SDPATH; } res=0XFF; ftinfo.fontok=0XFF; SPI_Flash_Write((u8*)&ftinfo,FONTINFOADDR,sizeof(ftinfo)); //清除之前字库成功的标志.防止更新到一半重启,导致的字库部分数据丢失. SPI_Flash_Read((u8*)&ftinfo,FONTINFOADDR,sizeof(ftinfo));//读出 ftinfo 结构体数据 LCD_ShowString(x,y,240,320,size,"Updating UNIGBK.BIN"); res=updata_fontx(x+20*size/2,y,size,unigbk_path,0); //更新 UNIGBK.BIN if(res)return 1; LCD_ShowString(x,y,240,320,size,"Updating GBK12.BIN "); res=updata_fontx(x+20*size/2,y,size,gbk12_path,1); //更新 GBK12.FON if(res)return 2; LCD_ShowString(x,y,240,320,size,"Updating GBK16.BIN "); res=updata_fontx(x+20*size/2,y,size,gbk16_path,2); //更新 GBK16.FON if(res)return 3; //全部更新好了 ftinfo.fontok=0XAA; SPI_Flash_Write((u8*)&ftinfo,FONTINFOADDR,sizeof(ftinfo)); //保存字库信息 return 0;//无错误. } //初始化字体 //返回值:0,字库完好. // 其他,字库丢失 u8 font_init(void) { SPI_Flash_Init(); FONTINFOADDR=(1024*6+500)*1024; //W25Q64,6M 以后 ftinfo.ugbkaddr=FONTINFOADDR+25; //UNICODEGBK 表存放首地址固定地址 SPI_Flash_Read((u8*)&ftinfo,FONTINFOADDR,sizeof(ftinfo));//读出 ftinfo 结构体数据 if(ftinfo.fontok!=0XAA)return 1; //字库错误. return 0; } 此部分代码主要用于字库的更新操作(包含 UNIGBK 的转换码表更新),其中 ftinfo 是我 们在 fontupd.h 里面定义的一个结构体,用于记录字库首地址及字库大小等信息。因为我们将 W25Q64 的前 6M 字节给 FATFS 管理(用做本地磁盘),然后又预留了 500K 字节给用户自己使 用,最后的 1.5M 字节(W25Q64 总共 8M 字节),才是 UNIGBK 码表和字库的存储空间,所以, www.openedv.com 603 ALIENTEK 战舰STM32开发板 我们的存储地址是从(1024*6+500)*1024 处开始的。最开始的 25 个字节给 ftinfo 用,用于保存 ftinfo 结构体数据。之后开始的是 UNIGBK.bin 的存放地址,之后再是 GBK12.fon 的存放地址, 最后存放 GBK16.fon。 接下来我们看看头文件 fontupd.h 的内容: #ifndef __FONTUPD_H__ #define __FONTUPD_H__ #include //前面 6M 被 fatfs 占用了. //6M 以后紧跟的 500K 字节,用户可以随便用. //6M+500K 字节以后的字节,被字库占用了,不能动! //字体信息保存地址,占 25 个字节,第 1 个字节用于标记字库是否存在.后续每 8 个字节一组, //分别保存起始地址和文件大小 extern u32 FONTINFOADDR; //字库信息结构体定义 //用来保存字库基本信息,地址,大小等 __packed typedef struct { u8 fontok; u32 ugbkaddr; u32 ugbksize; //字库存在标志,0XAA,字库正常;其他,字库不存在 //unigbk 的地址 //unigbk 的大小 u32 f12addr; u32 gbk12size; //gbk12 地址 //gbk12 的大小 u32 f16addr; u32 gkb16size; //gbk16 地址 //gbk16 的大小 }_font_info; extern _font_info ftinfo; //字库信息结构体 u32 fupd_prog(u16 x,u16 y,u8 size,u32 fsize,u32 pos);//显示更新进度 u8 updata_fontx(u16 x,u16 y,u8 size,u8 *fxpath,u8 fx);//更新指定字库 u8 update_font(u16 x,u16 y,u8 size,u8 src);//更新全部字库 u8 font_init(void);//初始化字库 #endif 这里,我们可以看到 ftinfo 的结构体定义,总共占用 25 个字节,第一个字节用来标识字库 是否 OK,其他的用来记录地址和文件大小。打开 text.c 文件,该文件代码如下: #include "sys.h" #include "fontupd.h" #include "flash.h" #include "lcd.h" #include "text.h" #include "string.h" //code 字符指针开始 //从字库中查找出字模 //code 字符串的开始地址, GBK 码 //mat 数据存放地址 size*2 bytes 大小 www.openedv.com 604 ALIENTEK 战舰STM32开发板 void Get_HzMat(unsigned char *code,unsigned char *mat,u8 size) { unsigned char qh,ql; unsigned char i; unsigned long foffset; qh=*code; ql=*(++code); if(qh<0x81||ql<0x40||ql==0xff||qh==0xff)//非 常用汉字 { for(i=0;i<(size*2);i++)*mat++=0x00;//填充满格 return; //结束访问 } if(ql<0x7f)ql-=0x40;//注意! else ql-=0x41; qh-=0x81; foffset=((unsigned long)190*qh+ql)*(size*2);//得到字库中的字节偏移量 if(size==16)SPI_Flash_Read(mat,foffset+ftinfo.f16addr,32); else SPI_Flash_Read(mat,foffset+ftinfo.f12addr,24); } //显示一个指定大小的汉字 //x,y :汉字的坐标 //font:汉字 GBK 码 //size:字体大小 //mode:0,正常显示,1,叠加显示 void Show_Font(u16 x,u16 y,u8 *font,u8 size,u8 mode) { u8 temp,t,t1; u16 y0=y; u8 dzk[32]; u16 tempcolor; if(size!=12&&size!=16)return;//不支持的 size Get_HzMat(font,dzk,size);//得到相应大小的点阵数据 if(mode==0)//正常显示 { for(t=0;tlcddev.width)||(y0>lcddev.height))return; LCD_Fill(x0,y0,x0+len-1,y0,color); } //画图初始化,在画图之前,必须先调用此函数 //指定画点/读点 void piclib_init(void) { pic_phy.read_point=LCD_ReadPoint; //读点函数实现 pic_phy.draw_point=LCD_Fast_DrawPoint; pic_phy.fill=LCD_Fill; pic_phy.draw_hline=piclib_draw_hline; //画点函数实现 //填充函数实现 //画线函数实现 picinfo.ImgWidth=0; //初始化宽度为 0 picinfo.ImgHeight=0; //初始化高度为 0 picinfo.Div_Fac=0; //初始化缩放系数为 0 picinfo.S_Height=0; //初始化设定的高度为 0 picinfo.S_Width=0; picinfo.S_XOFF=0; //初始化设定的宽度为 0 //初始化 x 轴的偏移量为 0 picinfo.S_YOFF=0; picinfo.staticx=0; picinfo.staticy=0; //初始化 y 轴的偏移量为 0 //初始化当前显示到的 x 坐标为 0 //初始化当前显示到的 y 坐标为 0 } //快速 ALPHA BLENDING 算法. //src:源颜色 //dst:目标颜色 www.openedv.com 614 ALIENTEK 战舰STM32开发板 //alpha:透明程度(0~32) //返回值:混合后的颜色. u16 piclib_alpha_blend(u16 src,u16 dst,u8 alpha) { u32 src2; u32 dst2; src2=((src<<16)|src)&0x07E0F81F; dst2=((dst<<16)|dst)&0x07E0F81F; dst2=((((dst2-src2)*alpha)>>5)+src2)&0x07E0F81F; return (dst2>>16)|dst2; } //初始化智能画点 //内部调用 void ai_draw_init(void) { float temp,temp1; temp=(float)picinfo.S_Width/picinfo.ImgWidth; temp1=(float)picinfo.S_Height/picinfo.ImgHeight; if(temp1)temp1=1; //使图片处于所给区域的中间 picinfo.S_XOFF+=(picinfo.S_Width-temp1*picinfo.ImgWidth)/2; picinfo.S_YOFF+=(picinfo.S_Height-temp1*picinfo.ImgHeight)/2; temp1*=8192;//扩大 8192 倍 picinfo.Div_Fac=temp1; picinfo.staticx=0xffff; picinfo.staticy=0xffff;//放到一个不可能的值上面 } //判断这个像素是否可以显示 //(x,y) :像素原始坐标 //chg :功能变量. //返回值:0,不需要显示.1,需要显示 u8 is_element_ok(u16 x,u16 y,u8 chg) { if(x!=picinfo.staticx||y!=picinfo.staticy) { if(chg==1){picinfo.staticx=x; picinfo.staticy=y;} return 1; }else return 0; } //智能画图 //FileName:要显示的图片文件 BMP/JPG/JPEG/GIF //x,y,width,height:坐标及显示区域尺寸 www.openedv.com 615 ALIENTEK 战舰STM32开发板 //acolor :alphablend 的颜色(仅对不大于 320*240 的 32 位 bmp 有效!) //abdnum :alphablend 的值(0~32 有效,其余值表示不使用 alphablend, //仅对不大于 320*240 的 32 位 bmp 有效!) //图片在开始和结束的坐标点范围内显示 u8 ai_load_picfile(const u8 *filename,u16 x,u16 y,u16 width,u16 height) { u8 res;//返回值 u8 temp; if((x+width)>lcddev.width)return PIC_WINDOW_ERR; if((y+height)>lcddev.height)return PIC_WINDOW_ERR; //得到显示方框大小 //x 坐标超范围了. //y 坐标超范围了. if(width==0||height==0)return PIC_WINDOW_ERR; //窗口设定错误 picinfo.S_Height=height; picinfo.S_Width=width; //显示区域无效 if(picinfo.S_Height==0||picinfo.S_Width==0) { picinfo.S_Height=lcddev.height; picinfo.S_Width=lcddev.width; return FALSE; } //显示的开始坐标点 picinfo.S_YOFF=y; picinfo.S_XOFF=x; //文件名传递 temp=f_typetell((u8*)filename); //得到文件的类型 switch(temp) { case T_BMP: res=stdbmp_decode(filename); break; //解码 bmp case T_JPG: case T_JPEG: res=jpg_decode(filename); break; //解码 JPG/JPEG case T_GIF: res=gif_decode(filename,x,y,width,height); break;//解码 gif default: res=PIC_FORMAT_ERR;break; //非图片格式!!! } return res; } 此段代码总共 6 个函数,其中,piclib_draw_hline 函数用于快速画横线,由 gif 解码程 序使用。 piclib_init 函数,该函数用于初始化图片解码的相关信息,其中_pic_phy 是我们在 piclib.h 里面定义的一个结构体,用于管理底层 LCD 接口函数,这些函数必须由用户在外部 www.openedv.com 616 ALIENTEK 战舰STM32开发板 实现。_pic_info 则是另外一个结构体,用于图片缩放处理。 piclib_alpha_blend 函数,该函数用于实现半透明效果,在小格式(分辨率小于 240*320) bmp 解码的时候,可能被用到。 ai_draw_init 函数,该函数用于实现图片在显示区域的居中显示初始化,其实就是根据 图片大小选择缩放比例和坐标偏移值。 is_element_ok 函数,该函数用于判断一个点是不是应该显示出来,在图片缩放的时候 该函数是必须用到的。 ai_load_picfile 函数,该函数是整个图片显示的对外接口,外部程序,通过调用该函数, 可以实现 bmp、jpg/jpeg 和 gif 的显示,该函数根据输入文件的后缀名,判断文件格式,然 后交给相应的解码程序(bmp 解码/jpeg 解码/gif 解码),执行解码,完成图片显示。注意, 这里我们用到一个 f_typetell 的函数,来判断文件的后缀名,f_typetell 函数在 exfuns.c 里面 实现,具体请参考光盘源码。 下面我们看看 piclib.h 头文件源码: #ifndef __PICLIB_H #define __PICLIB_H #include "sys.h" #include "lcd.h" #include "malloc.h" #include "ff.h" #include "exfuns.h" #include "bmp.h" #include "jpeg.h" #include "gif.h" #define PIC_FORMAT_ERR 0x27 //格式错误 #define PIC_SIZE_ERR 0x28 //图片尺寸错误 #define PIC_WINDOW_ERR 0x29 //窗口设定错误 #define PIC_MEM_ERR 0x11 //内存错误 //图片显示物理层接口 //在移植的时候,必须由用户自己实现这几个函数 typedef struct { u16(*read_point)(u16,u16);//u16 read_point(u16 x,u16 y) 读点函数 void(*draw_point)(u16,u16,u16); //void draw_point(u16 x,u16 y,u16 color) 画点函数 void(*fill)(u16,u16,u16,u16,u16); //void fill(u16 sx,u16 sy,u16 ex,u16 ey,u16 color) 单色填充函数 void(*draw_hline)(u16,u16,u16,u16); //void draw_hline(u16 x0,u16 y0,u16 len,u16 color) 画水平线函数 }_pic_phy; extern _pic_phy pic_phy; //图像信息 typedef struct { u32 ImgWidth; //图像的实际宽度和高度 www.openedv.com 617 ALIENTEK 战舰STM32开发板 u32 ImgHeight; u32 Div_Fac; //缩放系数 (扩大了 8192 倍的) u32 S_Height; //设定的高度和宽度 u32 S_Width; u32 S_XOFF; //x 轴和 y 轴的偏移量 u32 S_YOFF; u32 staticx; //当前显示到的xy坐标 u32 staticy; }_pic_info; extern _pic_info picinfo;//图像信息 void piclib_init(void); //初始化画图 u16 piclib_alpha_blend(u16 src,u16 dst,u8 alpha); //alphablend 处理 void ai_draw_init(void); //初始化智能画图 u8 is_element_ok(u16 x,u16 y,u8 chg); //判断像素是否有效 u8 ai_load_picfile(const u8 *filename,u16 x,u16 y,u16 width,u16 height);//智能画图 #endif 这里基本就是我们前面提到的两个结构体的定义以及一些函数的申明。最后我们看看 main.c 里面的源码: //得到 path 路径下,目标文件的总个数 //path:路径 //返回值:总有效文件数 u16 pic_get_tnum(u8 *path) { u8 res; u16 rval=0; DIR tdir; //临时目录 FILINFO tfileinfo; //临时文件信息 u8 *fn; res=f_opendir(&tdir,(const TCHAR*)path); //打开目录 tfileinfo.lfsize=_MAX_LFN*2+1; //长文件名最大长度 tfileinfo.lfname=mymalloc(SRAMIN,tfileinfo.lfsize); //为长文件缓存区分配内存 if(res==FR_OK&&tfileinfo.lfname!=NULL) { while(1)//查询总的有效文件数 { res=f_readdir(&tdir,&tfileinfo); //读取目录下的一个文件 if(res!=FR_OK||tfileinfo.fname[0]==0)break; // 错 误 了 / 到 末 尾 了 , 退 出 fn=(u8*)(*tfileinfo.lfname?tfileinfo.lfname:tfileinfo.fname); res=f_typetell(fn); if((res&0XF0)==0X50) rval++;//图片文件?则有效文件加 1 } } return rval; www.openedv.com 618 ALIENTEK 战舰STM32开发板 } int main(void) { u8 res; DIR picdir; //图片目录 FILINFO picfileinfo; //文件信息 u8 *fn; //长文件名 u8 *pname; //带路径的文件名 u16 totpicnum; //图片文件总数 u16 curindex; //图片当前索引 u8 key; //键值 u8 pause=0; //暂停标记 u8 t; u16 temp; u16 *picindextbl; //图片索引表 delay_init(); //延时函数初始化 NVIC_Configuration(); //设置 NVIC 中断分组 2:2 位抢占优先级,2 位响应优先级 uart_init(9600); //串口初始化波特率为 9600 LED_Init(); //LED 端口初始化 LCD_Init(); //LCD 初始化 KEY_Init(); //KEY 初始化 usmart_dev.init(72); //usmart 初始化 mem_init(SRAMIN); //初始化内部内存池 exfuns_init(); //为 fatfs 相关变量申请内存 f_mount(0,fs[0]); //挂载 SD 卡 f_mount(1,fs[1]); //挂载 FLASH. POINT_COLOR=RED; while(font_init()) //检查字库 { LCD_ShowString(60,50,200,16,16,"Font Error!"); delay_ms(200); LCD_Fill(60,50,240,66,WHITE);//清除显示 } Show_Str(60,50,200,16,"战舰 STM32 开发板",16,0); Show_Str(60,70,200,16,"图片显示程序",16,0); Show_Str(60,90,200,16,"KEY0:NEXT KEY1:PREV",16,0); Show_Str(60,110,200,16,"正点原子@ALIENTEK",16,0); Show_Str(60,130,200,16,"2012 年 9 月 19 日",16,0); while(f_opendir(&picdir,"0:/PICTURE"))//打开图片文件夹 { Show_Str(60,150,240,16,"PICTURE 文件夹错误!",16,0); delay_ms(200); www.openedv.com 619 ALIENTEK 战舰STM32开发板 LCD_Fill(60,150,240,146,WHITE);//清除显示 delay_ms(200); } totpicnum=pic_get_tnum("0:/PICTURE"); //得到总有效文件数 while(totpicnum==NULL)//图片文件为 0 { Show_Str(60,150,240,16,"没有图片文件!",16,0); delay_ms(200); LCD_Fill(60,150,240,146,WHITE);//清除显示 delay_ms(200); } picfileinfo.lfsize=_MAX_LFN*2+1; //长文件名最大长度 picfileinfo.lfname=mymalloc(SRAMIN,picfileinfo.lfsize); //为长文件缓存区分配内存 pname=mymalloc(SRAMIN,picfileinfo.lfsize); //为带路径的文件名分配内存 picindextbl=mymalloc(SRAMIN,2*totpicnum); //申请 2*totpicnum 个字节的内存, //用于存放图片索引 while(picfileinfo.lfname==NULL||pname==NULL||picindextbl==NULL)//内存分配出错 { Show_Str(60,150,240,16,"内存分配失败!",16,0); delay_ms(200); LCD_Fill(60,150,240,146,WHITE);//清除显示 delay_ms(200); } //记录索引 res=f_opendir(&picdir,"0:/PICTURE"); //打开目录 if(res==FR_OK) { curindex=0;//当前索引为 0 while(1)//全部查询一遍 { temp=picdir.index; //记录当前 index res=f_readdir(&picdir,&picfileinfo); //读取目录下的一个文件 if(res!=FR_OK||picfileinfo.fname[0]==0)break; //错误了/到末尾了,退出 fn=(u8*)(*picfileinfo.lfname?picfileinfo.lfname:picfileinfo.fname); res=f_typetell(fn); if((res&0XF0)==0X50)//取高四位,看看是不是图片文件 { picindextbl[curindex]=temp;//记录索引 curindex++; } } } Show_Str(60,150,240,16,"开始显示...",16,0); www.openedv.com 620 ALIENTEK 战舰STM32开发板 delay_ms(1500); piclib_init(); //初始化画图 curindex=0; //从 0 开始显示 res=f_opendir(&picdir,(const TCHAR*)"0:/PICTURE"); //打开目录 while(res==FR_OK)//打开成功 { dir_sdi(&picdir,picindextbl[curindex]); //改变当前目录索引 res=f_readdir(&picdir,&picfileinfo); //读取目录下的一个文件 if(res!=FR_OK||picfileinfo.fname[0]==0)break; //错误了/到末尾了,退出 fn=(u8*)(*picfileinfo.lfname?picfileinfo.lfname:picfileinfo.fname); strcpy((char*)pname,"0:/PICTURE/"); //复制路径(目录) strcat((char*)pname,(const char*)fn); //将文件名接在后面 LCD_Clear(BLACK); ai_load_picfile(pname,0,0,lcddev.width,lcddev.height);//显示图片 Show_Str(2,2,240,16,pname,16,1); //显示图片名字 t=0; while(1) { key=KEY_Scan(0); //扫描按键 if(t>250)key=KEY_RIGHT; //模拟一次按下右键 if(key==KEY_LEFT) //上一张 { if(curindex)curindex--; else curindex=totpicnum-1; break; }else if(key==KEY_RIGHT)//下一张 { curindex++; if(curindex>=totpicnum)curindex=0;//到末尾的时候,自动从头开始 break; }else if(key==KEY_UP) { pause=!pause; LED1=!pause; //暂停的时候 LED1 亮. } if(pause==0)t++; delay_ms(10); } res=0; } myfree(SRAMIN,picfileinfo.lfname); //释放内存 myfree(SRAMIN,pname); //释放内存 myfree(SRAMIN,picindextbl); //释放内存 www.openedv.com 621 ALIENTEK 战舰STM32开发板 } 此部分除了 mian 函数,还有一个 pic_get_tnum 的函数,用来得到 path 路径下,所有有 效文件(图片文件)的个数。在 mian 函数里面我们通过索引(图片文件在 PICTURE 文件 夹下的编号),来查找上一个/下一个图片文件,这里我们需要用到 fatfs 自带的一个函数: dir_sdi,来设置当前目录的索引(因为 f_readdir 只能沿着索引一直往下找,不能往上找), 方便定位到任何一个文件。dir_sdi 在 FATFS 下面被定义为 static 函数,所以我们必须在 ff.c 里面将该函数的 static 修饰词去掉,然后在 ff.h 里面添加该函数的申明,以便 main 函数使用。 其他部分就比较简单了,至此,整个图片显示实验的软件设计部分就结束了。该程序将 实现浏览 PICTURE 文件夹下的所有图片,并显示其名字,每隔 3s 左右切换一幅图片。 47.4 下载验证 在代码编译成功之后,我们下载代码到 ALIENTEK 战舰 STM32 开发板上,可以看到 LCD 开始显示图片(假设 SD 卡及文件都准备好了),如图 47.4.1 所示: 图 37.4.1 图片显示实验显示效果 按 KEY0 和 KEY2 可以快速切换到下一张或上一张。同时,由于我们的代码支持 gif 格 式的图片显示(注意尺寸不能超过 LCD 屏幕尺寸),所以可以放一些 gif 图片到 PICTURE 文件夹,来看动画了。 本章,同样可以通过 USMART 来测试该实验,将 ai_load_picfile 函数加入 USMART 控 制(方法前面已经讲了很多次了),就可以通过串口调用该函数,在屏幕上任何区域显示任 何你想要显示的图片了! www.openedv.com 622 ALIENTEK 战舰STM32开发板 第四十八章 照相机实验 上一章,我们学习了图片解码,本章我们将学习 bmp 编码,结合前面的摄像头实验, 实现一个简单的照相机。本章分为如下几个部分: 48.1 BMP 编码简介 48.2 硬件设计 48.3 软件设计 48.4 下载验证 www.openedv.com 623 ALIENTEK 战舰STM32开发板 48.1 BMP 编码简介 上一章,我们学习了各种图片格式的解码。本章,我们介绍最简单的图片编码方法:BMP 图片编码。通过前面的了解,我们知道 BMP 文件是由文件头、位图信息头、颜色信息和图形 数据等四部分组成。我们先来了解下这几个部分。 1、BMP 文件头(14 字节):BMP 文件头数据结构含有 BMP 文件的类型、文件大小和位图起 始位置等信息。 //BMP 文件头 typedef __packed struct { u16 bfType ; u32 bfSize ; u16 bfReserved1 ; u16 bfReserved2 ; u32 bfOffBits ; //文件标志.只对'BM',用来识别 BMP 位图类型 //文件大小,占四个字节 //保留 //保留 //从文件开始到位图数据(bitmap data)开始之间的的偏移量 }BITMAPFILEHEADER ; 2、位图信息头(40 字节):BMP 位图信息头数据用于说明位图的尺寸等信息。 typedef __packed struct { u32 biSize ; //说明 BITMAPINFOHEADER 结构所需要的字数。 long biWidth ; //说明图象的宽度,以象素为单位 long biHeight ; //说明图象的高度,以象素为单位 u16 biPlanes ; //为目标设备说明位面数,其值将总是被设为 1 u16 biBitCount ; //说明比特数/象素,其值为 1、4、8、16、24、或 32 u32 biCompression ; //说明图象数据压缩的类型。其值可以是下述值之一: //BI_RGB:没有压缩; //BI_RLE8:每个象素 8 比特的 RLE 压缩编码,压缩格式由 2 字节组成 //BI_RLE4:每个象素 4 比特的 RLE 压缩编码,压缩格式由 2 字节组成 //BI_BITFIELDS:每个象素的比特由指定的掩码决定。 u32 biSizeImage ;//说明图象的大小,以字节为单位。当用 BI_RGB 格式时,可设置为 0 long biXPelsPerMeter ;//说明水平分辨率,用象素/米表示 long biYPelsPerMeter ;//说明垂直分辨率,用象素/米表示 u32 biClrUsed ; //说明位图实际使用的彩色表中的颜色索引数 u32 biClrImportant ; //说明对图象显示有重要影响的颜色索引的数目, //如果是 0,表示都重要。 }BITMAPINFOHEADER ; 3、颜色表:颜色表用于说明位图中的颜色,它有若干个表项,每一个表项是一个 RGBQUAD 类型的结构,定义一种颜色。 typedef __packed struct { u8 rgbBlue ; u8 rgbGreen ; //指定蓝色强度 //指定绿色强度 www.openedv.com 624 ALIENTEK 战舰STM32开发板 u8 rgbRed ; //指定红色强度 u8 rgbReserved ; //保留,设置为 0 }RGBQUAD ; 颜色表中 RGBQUAD 结构数据的个数由 biBitCount 来确定:当 biBitCount=1、4、8 时,分 别有 2、16、256 个表项;当 biBitCount 大于 8 时,没有颜色表项。 BMP 文件头、位图信息头和颜色表组成位图信息(我们将 BMP 文件头也加进来,方便处 理),BITMAPINFO 结构定义如下: typedef __packed struct { BITMAPFILEHEADER bmfHeader; BITMAPINFOHEADER bmiHeader; RGBQUAD bmiColors[1]; }BITMAPINFO; 4、位图数据:位图数据记录了位图的每一个像素值,记录顺序是在扫描行内是从左到右,扫描 行之间是从下到上。位图的一个像素值所占的字节数: 当 biBitCount=1 时,8 个像素占 1 个字节; 当 biBitCount=4 时,2 个像素占 1 个字节; 当 biBitCount=8 时,1 个像素占 1 个字节; 当 biBitCount=16 时,1 个像素占 2 个字节; 当 biBitCount=24 时,1 个像素占 3 个字节; 当 biBitCount=32 时,1 个像素占 4 个字节; biBitCount=1 表示位图最多有两种颜色,缺省情况下是黑色和白色,你也可以自己定义这 两种颜色。图像信息头装调色板中将有两个调色板项,称为索引 0 和索引 1。图象数据阵列中 的每一位表示一个象素。如果一个位是 0,显示时就使用索引 0 的 RGB 值,如果位是 1,则使 用索引 1 的 RGB 值。 biBitCount=16 表示位图最多有 65536 种颜色。每个色素用 16 位(2 个字节)表示。这种 格式叫作高彩色,或叫增强型 16 位色,或 64K 色。它的情况比较复杂,当 biCompression 成员 的值是 BI_RGB 时,它没有调色板。16 位中,最低的 5 位表示蓝色分量,中间的 5 位表示