野火电子论坛

 找回密码
 注册

QQ登录

只需一步,快速开始

查看: 6186|回复: 13

[STM32入门篇] 第七课 使用寄存器点亮LED 课后作业

[复制链接]
发表于 2022-7-10 12:15:04 | 显示全部楼层 |阅读模式
本帖最后由 GCT 于 2022-7-14 11:59 编辑

《使用寄存机点亮LED》 课后作业

    在入门篇 第七节课最后,火老师留了两道题,如图

   

题目

题目

    1.点亮其他两个LED灯。(绿灯已经在课上演示过了,要实现红、蓝灯点亮)

    2.写一个简单的延时函数,让灯闪烁。

    遇到的问题用红色字体和下划线标出了。


第一题


    依葫芦画瓢,先看LED原理图。

   

LED原理图

LED原理图

    LED使用的GPIOB(General Purpose Input/Output)端口,其中红灯在PB5端口、绿灯在PB0端口、蓝灯在PB1端口。


    要想让一个外设运行,先要让它的心脏——时钟使能,然后再使能端口。

    先打开ST官方的参考手册,找到存储器映像(Memory Map)(英文版p51 中文版p28)

   

存储器映像

存储器映像

    野火论坛202207101747036355..png

    可以看到RCC的基地址为0x40021018,GPIO端口B的基地址为0x40010C00,GPIOB使用APB2总线。


    找到APB2外设时钟使能寄存器RCC_APB2ENR(英文版p70 中文版p112)

    野火论坛202207101752228072..png

    偏移地址为 0x18,和基地址相加可以算出 RCC_APB2ENR 的物理地址为 0x40021018。

    还可以得到一个信息,初始值所有位为0(Reset value)。


    接着往下看

    野火论坛202207101800275153..png

    可知GPIOB的时钟用第三位控制,置1使能。

    置1使用 |= 按位或运算而不是直接赋值,为了不改变原有的位。


    下面演示一下计算过程:(涉及到低四位,高位全部是零,为了方便看就省略了)

    (1<<3) = 1000

    0000 | 1000 = 1000

    第3位成功置1

    代码实现:

  1. *(unsigned int*)0x40021018 |=(1<<3);
复制代码
     

      然后要配置GPIO的输出模式和速度。
      找到端口配置低寄存器GPIOx_CRL(端口配置低寄存器负责控制GPIOx的0-7端口,高位寄存器负责控制GPIOx的8-15端口
      (英文版p171 中文版p113)

      这里中文版和英文版写的不一样,中文版是x=A..E,英文版x=A..G,英文版多了GPIOF、GPIOG。103VE似乎只有GPIOA-GPIOE
       野火论坛202207101811515692..png

      偏移量0x00,GPIOx_CRL基地址就是物理地址0x40010C00。



      值得提一下这里的初始值0x4444 4444,先要说明这里的32位是怎么组成的。
      寄存器可控制0-7共八个端口,每个端口用四位控制。高两位是CNFy,确定输入/输出模式(推挽/开漏);低两位是MODE,用于确定输入还是输出,还有输出速度。
      初始值把每一个端口初始化为4,也就是0100,这个在下图中也有体现(reset state)。


       野火论坛202207101826008225..png
      选择推挽输出模式(?课上说的是推挽输出,可是实际结果似乎是开漏输出?(补充:后来上课讲到需要先清零,这样就没有问题了)),输出速度10MHz即可
      红色LED在PB5,蓝色LED在PB1
      按需要将第20位或第4位 置1即可


      下面是计算过程,同样省略高位只写了低八位:
      蓝LED置1过程:(低八位)
      (1<<(4*1)) = 0001 0000 //4代表一个端口的四位,右边的1代表PB1

      0100 0100 | 0001 0000 = 0101 0100
      置位结果是开漏输出,速度10MHz?
      红LED置1过程同上

      代码实现:(color变量代表对应颜色的端口号,红=5,蓝=1,绿=0)

  1. *(unsigned int*)0x40010C00 |=(1<<(4*Color));
复制代码
      
      最后是配置端口输出数据寄存器GPIOx_ODR(英文版p173 中文版p115)
       野火论坛202207102312324699..png
      偏移量0x0C,加上基地址可得GPIOx_ODR物理地址0x40010C0C。


      初始值为0x0000 0000
      LED是置0点亮,置零&=~ 按位或 取反 运算。
      将LED蓝所在的1位置0,应该执行如下操作:(依然为了方便看,只写了八位)
      1<<1 = 00000010 //右边的1代表PB1
      0000 0000 & ~(0000 0010) = 0000 0000 & 1111 1101 = 0000 0000


      实现代码:
  1. *(unsigned int*)(0x40010C0C) &=~(1<<Color);
复制代码

      

      总结一下,寄存器点灯程序的三部分:时钟使能、配置输出模式、配置输出。
      将不同颜色的端口定义到了宏定义,整合得到如下完整代码:
     (一开始我写的时候只改了ODR寄存器,没改CRL....然后一直不亮....后来仔细又参考手册才发现CRL也需要修改...)

  1. #include "stm32f10x.h"

  2. #define Red 5
  3. #define Green 0
  4. #define Blue 1

  5. int main(void)
  6. {
  7.         int Color=0;
  8.         Color=Red;                                                    //根据需要改变Color的值Red/Blue/Green

  9.         *(unsigned int*)0x40021018 |=(1<<3);            //控制RCC寄存器 打开GPIOB端口的时钟
  10.         *(unsigned int*)0x40010C00 |=(1<<(4*Color));//控制CRL寄存器 配置IO口为输出
  11.         *(unsigned int*)(0x40010C0C) &=~(1<<Color);//控制ODR寄存器
  12. }

  13. //函数体为空,目的是为了骗过编译器不报错
  14. void SystemInit(void)
  15. {}
复制代码

第二题
      

      说到延时先想到了用循环实现延时。但是转念一想程序运行代码都是很快的,以微秒计数的,可能达不到毫秒甚至秒级别的延时,精度也会极差,就没往下想。
      然后就想到了中断和延时,但是翻阅了好久的参考手册以及附带的其他资料,发现和GPIO寄存器操作不太一样,还需要多学习才能搞出来...
      于是求助了网络,找到一篇比较清晰的文章:https://blog.csdn.net/Firefly_cjd/article/details/106709259,最简单的方法居然真的是让程序循环来实现延时。
  1. //微秒级的延时
  2. void delay_us(int delay_us)
  3. {   
  4.      volatile unsigned int num;
  5.      volatile unsigned int t;
  6.   
  7.      for (num = 0; num < delay_us; num++)
  8.      {
  9.          t = 11;
  10.          while (t != 0)
  11.          {
  12.              t--;
  13.          }
  14.      }
  15. }

  16. //毫秒级的延时
  17. void delay_ms(int delay_ms)
  18. {   
  19.      volatile unsigned int num;
  20.      for (num = 0; num < delay_ms; num++)
  21.      {
  22.           delay_us(1000);
  23.      }
  24. }
复制代码
      实现原理是很简单的for循环体和while循环体,但是有一个细节需要注意:关键字volatile。这个关键字以前c语言编程的时候从来没有出现过。
      网上查了一下得到这样的解释:volatile属于C语言的关键字,《C Primer Plus》 是这样解释关键字的:关键字是C语言的词汇,由于编译器不具备真正的智能,所以你必须用编译器能理解的术语表示你的意图。开发者告诉编译器该变量是易变的,无非就是希望编译器去注意该变量的状态,时刻注意该变量是易变的,每次读取该变量的值都重新从内存中读取。
(来自:https://blog.csdn.net/weixin_38815998/article/details/102840096;这一篇也有很详细的解释:https://blog.csdn.net/hgsdfghdfsd/article/details/52129364


      看完概念还是有点一知半解。经过测试,如果去掉关键字volatile,延时的时间会缩短很多,可能加了关键字volatile访问变量值的过程所耗费的时间会变长吧。
      使用这个延时函数,结合上面的点灯代码,我实现了一个颜色的灯闪烁操作 以及 两个颜色的灯延时切换的操作,代码如下:


      红灯闪烁:
  1. #include "stm32f10x.h"

  2. #define Red 5
  3. #define Green 0
  4. #define Blue 1

  5. void delay_us(int delay_us);
  6. void delay_ms(int delay_ms);
  7. void init();

  8. int main(void)
  9. {
  10.     int Color=0;

  11.     *(unsigned int*)0x40021018 |=(1<<3);

  12.     while(1)
  13.     {
  14.         Color=Red;
  15.         *(unsigned int*)0x40010C00 |=(1<<(4*Color));
  16.         *(unsigned int*)(0x40010C0C) &=~(1<<Color);
  17.         delay_ms(50);
  18.         init();
  19.         delay_ms(50);
  20.     }
  21. }

  22. //函数体为空,目的是为了骗过编译器不报错
  23. void SystemInit(void)
  24. {}

  25. //微秒级的延时
  26. void delay_us(int delay_us)
  27. {
  28.     volatile unsigned int num;
  29.     volatile unsigned int t;

  30.     for (num = 0; num < delay_us; num++)
  31.     {
  32.         t = 11;
  33.         while (t != 0)
  34.         {
  35.             t--;
  36.         }
  37.     }
  38. }

  39. //毫秒级的延时
  40. void delay_ms(int delay_ms)
  41. {
  42.     volatile unsigned int num;
  43.     for (num = 0; num < delay_ms; num++)
  44.     {
  45.         delay_us(1000);
  46.     }
  47. }

  48. void init()
  49. {
  50.     *(unsigned int*)0x40010C00 =0x44444444;
  51.     *(unsigned int*)(0x40010C0C) =0x00000000;
  52. }
复制代码



      红蓝交替:

  1. #include "stm32f10x.h"

  2. #define Red 5
  3. #define Green 0
  4. #define Blue 1

  5. void delay_us(int delay_us);
  6. void delay_ms(int delay_ms);
  7. void init();

  8. int main(void)
  9. {
  10.     int Color=0;

  11.     *(unsigned int*)0x40021018 |=(1<<3);

  12.     while(1)
  13.     {
  14.         Color=Red;
  15.         *(unsigned int*)0x40010C00 |=(1<<(4*Color));
  16.         *(unsigned int*)(0x40010C0C) &=~(1<<Color);
  17.         delay_ms(50);
  18.         init();

  19.         Color=Blue;
  20.         *(unsigned int*)0x40010C00 |=(1<<(4*Color));
  21.         *(unsigned int*)(0x40010C0C) &=~(1<<Color);
  22.         delay_ms(50);
  23.         init();
  24.     }
  25. }

  26. //函数体为空,目的是为了骗过编译器不报错
  27. void SystemInit(void)
  28. {}

  29. //微秒级的延时
  30. void delay_us(int delay_us)
  31. {
  32.     volatile unsigned int num;
  33.     volatile unsigned int t;

  34.     for (num = 0; num < delay_us; num++)
  35.     {
  36.         t = 11;
  37.         while (t != 0)
  38.         {
  39.             t--;
  40.         }
  41.     }
  42. }

  43. //毫秒级的延时
  44. void delay_ms(int delay_ms)
  45. {
  46.     volatile unsigned int num;
  47.     for (num = 0; num < delay_ms; num++)
  48.     {
  49.         delay_us(1000);
  50.     }
  51. }

  52. void init()
  53. {
  54.     *(unsigned int*)0x40010C00 =0x44444444;
  55.     *(unsigned int*)(0x40010C0C) =0x00000000;
  56. }
复制代码
      本人是初学者,可能在编写过程中会有一些错误,也有可能在学习中有一些理解错误。有什么不足还请大佬们指点~感谢~
      (顺便吐槽一下发帖的Markdown编辑器有bug啊...这个帖子写了三次...)




评分

参与人数 3火花 +26 收起 理由
1323891265 + 8
qirui + 8 很给力!
lnp + 10

查看全部评分

回复

使用道具 举报

发表于 2022-7-17 14:23:30 | 显示全部楼层
你好,请问把红灯换成紫灯要怎么做啊
回复 支持 反对

使用道具 举报

 楼主| 发表于 2022-7-17 15:37:21 | 显示全部楼层
陆小小 发表于 2022-7-17 14:23
你好,请问把红灯换成紫灯要怎么做啊

目前我还没学到使用PWM方法
但是还是有办法变成紫灯的,红+蓝=紫 只要同时开启红蓝两个灯就可以实现。
也就是同时配置PB5 PB1
回复 支持 反对

使用道具 举报

发表于 2022-7-18 00:06:06 | 显示全部楼层
用voliatile关键字修饰的这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份,也就是说要从它的源地址处取址,是这个意思吗?
回复 支持 反对

使用道具 举报

发表于 2022-7-18 00:42:01 | 显示全部楼层
我用了上述的延时函数,我测试了之后所需的延时时间确实有减少,运行时间长之后就会出现闪烁时间不均的现象。所以我查找资料用以下这个就好用很多。
  1. // 延时函数
  2. // 延时函数
  3. void Delay(unsigned int count)
  4. {
  5.         unsigned int i;
  6.         for(; count!=0; count--)
  7.         {
  8.                 i = 5000;
  9.                 while(i--);
  10.         }
  11. }
复制代码
回复 支持 1 反对 0

使用道具 举报

 楼主| 发表于 2022-7-18 09:58:35 | 显示全部楼层
狂飙的蜗牛 发表于 2022-7-18 00:06
用voliatile关键字修饰的这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备 ...

是这个意思。
野火论坛202207180957578242..png 零死角那本书的110页也写到了

回复 支持 反对

使用道具 举报

发表于 2022-7-19 21:27:28 | 显示全部楼层
GCT 发表于 2022-7-17 15:37
目前我还没学到使用PWM方法
但是还是有办法变成紫灯的,红+蓝=紫 只要同时开启红蓝两个灯就可以实现。
...

好的好的谢谢啦!
回复 支持 反对

使用道具 举报

发表于 2022-7-25 11:45:55 | 显示全部楼层
回复

使用道具 举报

发表于 2022-7-25 11:46:25 | 显示全部楼层
回复

使用道具 举报

发表于 2022-9-27 11:41:02 | 显示全部楼层
真的强
回复

使用道具 举报

发表于 2022-11-10 09:13:03 | 显示全部楼层
感谢
回复

使用道具 举报

发表于 2023-2-27 11:11:28 | 显示全部楼层
  *(unsigned int*) 0x40021018 |= (1<<3);     //打开GPIOB端口的时钟
  *(unsigned int*) 0x40010C00 &= ~((0x0f)<<(4*0)); //将端口配置低寄存器IO口为低电平
  *(unsigned int*) 0x40010C00 |= (1<<(4*0));  //配置IO口为输出        (端口配置低寄存器默认输出高电平)
  *(unsigned int*) 0x40010c0c &=~(1<<0);      //控制ODR寄存器,将P0口设置为低电平  (点亮PB0小灯(绿))

        *(unsigned int*) 0x40021018 |=(1<<3);       //打开GPIOB端口的时钟
        *(unsigned int*) 0x40010C00 &= ~((0x0f)<<(4*1)); //将端口配置低寄存器IO口为低电平
        *(unsigned int*) 0x40010c00 |=(1<<(4*1));   //配置I口为输出(端口配置低寄存器默认输出高电平)
        *(unsigned int*) 0x40010c0c &=~(1<<1);     //控制ODR寄存器,将PB1设置为低电平  (点亮PB1小灯(蓝))
       
        *(unsigned int*) 0x40021018 |=(1<<3);      //打开GPIOB端口的时钟
        *(unsigned int*) 0x40010C00 &= ~((0x0f)<<(4*5)); //将端口配置低寄存器IO口为低电平
        *(unsigned int*) 0x40010c00 |=(1<<(4*5));   //配置IO口为输出(端口配置低寄存器默认输出高电平)
        *(unsigned int*) 0x40010c0c &=~(1<<5);     //控制ODR寄存器,将PB1设置为低电平  (点亮PB5小灯(红))
回复 支持 反对

使用道具 举报

发表于 2023-3-6 08:35:21 | 显示全部楼层
其实根源几乎都是操作寄存器,不管用标准库还是HAL库,里面的函数实现还是用句柄结构体去操作对应的寄存器位。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

联系站长|手机版|野火电子官网|野火淘宝店铺|野火电子论坛 ( 粤ICP备14069197号 ) 大学生ARM嵌入式2群

GMT+8, 2024-5-9 14:42 , Processed in 0.039405 second(s), 27 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表