数码之家

 找回密码
 立即注册

QQ登录

只需一步,快速开始

微信登录

微信扫一扫,快速登录

搜索
查看: 1141|回复: 0

[ARM] 单片机入门杂谈(五):从MCS-51到ARM

[复制链接]
发表于 2024-4-8 19:15:13 | 显示全部楼层 |阅读模式
在部分人看来,现在还学习MCS-51单片机是落伍的,应该学习ARM、ESPxxxx、RISC-V ……这些时髦的单片机。下面以Cortex-M3核心单片机为例简单介绍一下如何从MCS-51单片机转到ARM单片机(如果没有特别说明,下文所说的ARM单片机都是Cortex-M3核心ARM单片机、开发环境都是Keil MDK + AC5编译器)。

一、ARM单片机与MCS-51单片机主要差异

(1)ARM单片机ROM、RAM、外设统一编址,只管读/写属性,不像MCS-51单片机分ROM地址、RAM地址,RAM又分内部RAM和外部RAM。
(2)ARM单片机是32位单片机,ROM、RAM地址都是32位的,单次读/写以32位为单位效率最高(4字节对齐)。
(3)丰富的中断资源,除了正常的外设中断,还包括各种异常处理中断,有时借助异常处理中断可以发现程序隐患。
(4)ARM单片机一般标称工作电压是3.3V,少数魔改的可以5V工作。

二、ARM单片机常见开发工具

(1)开源免费(免费是指可以自制,下同):DAP-Link(部分厂商基于官方DAP-Link修改的各种Link不开源、不免费)。
(2)准免费:ST-Link。官方提供固件,据说也可以调试非ST的芯片。
(2)准免费:JLink-OB。严格来说,只有开发板上配的JLink-OB才算正版,但这个很容易仿制,一般也没人去追究仿制人的法律责任。
(3)收费:JLink。分很多版本,正版的都不便宜,最常见的D版是Plus版。

如果仅需要烧录程序,部分ARM单片机出厂内置串口烧录Bootloader,可以通过串口烧录程序(特定串口+PC软件)。

三、ARM单片机调试

ARM单片机调试接口:
(1)SWD:GND、VDD、SWDIO、SWCLK。
(2)JTAG:GND、VDD、TDO、TMS、TDI、TCK。

建议使用SWD调试接口;如果调试引脚做调试功能外功能使用,建议调试器复位输出脚连接单片机复位脚,确保调试器可控芯片复位。

Keil MDK "Connect & Reset Option"设置:


连接类型选项:
Normal:默认的连接策略,连接后只是将 PC 停在当前执行的指令处
with Pre–reset:在连接前,先执行一次 HW RESET
under Reset:在连接过程中一直保持 HW RESET 有效(该选项适用于用户程序误将 JTAG/SWD 禁掉的情况)

复位类型选项:
Normal(复位编号0):默认的复位策略,对于i.MXRT来说等同于Core and peripherals方式
Core(复位编号1):借助Cortex-M内核模块SCB中的AIRCR寄存器的VECTRESET位功能来复位Core
Reset Pin(复位编号2):通过拉低J-Link的RESET引脚(一般也会接到MCU reset脚)来复位MCU
Core and peripherals(复位编号8):借助Cortex-M内核模块SCB中的AIRCR寄存器的VECTRESET位和SYSRESETREQ位来同时复位Core和MCU外设模块
Halt after BootLoader:引导加载程序在执行引导加载程序指令后停止CPU,并且仅对某些设备可用
Halt before BootLoader:引导加载程序在执行引导加载程序指令前停止CPU,并且仅对某些设备可用

四、ARM单片机时钟树

如果要单片机按目标频率工作,无论使用寄存器还是函数库编程,时钟树都无法绕开。
时钟树主要描述时钟源、内核时钟源和频率、外设总线时钟源和频率,其中内核时钟相当于MCS-51单片机的系统时钟,指令运行快慢主要取决于这个时钟的频率,外设总线时钟为外设提供时钟信号。

以STM32F103C8为例,其时钟树如下图:


其默认时钟配置如下图(STM32CubeMX截图):


内部RC振荡器提供8MHz时钟信号,经过分频后,内核时钟HCLK=8MHz,APB1、APB2时钟=8MHz。

如果内核时钟HCLK=8MHz,程序运行效率是很低的,如果没有省电需求,一般以内部RC振荡器或外部晶振产生基础频率经PLL倍频后作为内核时钟使用。


配置PLL作为系统时钟和外设时钟的方法可以参考这篇文章:
https://www.cnblogs.com/jzcn/p/16352893.html

对寄存器的操作,建议使用厂商提供的外设库操作,即使不使用其中的函数,也可以使用头文件中定义的大量常数宏,以避免在程序中直接使用立即数。

需要操作外设时,先看外设挂在哪条外设总线下,如果时钟树没有体现这些内容,可以查看功能框图,STM32F103C8功能框图如下图:


以SPI1为例,SPI1挂在APB2总线下,需要操作SPI1时,开/关APB2总线下的SPI1时钟,计算速率时,以APB2总线时钟PCLK2为时钟源。

五、开启/关闭外设时钟

ARM单片机默认关闭外设时钟,此时,对外设的操作无效,如GPIO时钟关闭时对GPIO的读写是无效的。

开/关外设时钟实例:

//开启GPIO时钟--函数库方式
  1. RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
复制代码
//开启GPIO时钟--寄存器方式
  1. RCC->APB2ENR |= RCC_APB2Periph_GPIOB;
复制代码

当有省电之类需求时,设置好I/O状态后,关闭外设时钟,如关闭GPIO时钟。

//关闭GPIO时钟--函数库方式

  1. RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, DISABLE);
复制代码
//关闭GPIO时钟--寄存器方式
  1. RCC->APB2ENR &= (~RCC_APB2Periph_GPIOB);
复制代码
六、程序风格

刚接触ARM单片机时,可能会觉得ARM单片机寄存器多如牛毛,甚至对同一个引脚的读/写操作的地址都不一样,如读PB的地址是(0x40010C00 + 0x08)、写PB的地址是(0x40010C00 + 0x0C),这么多寄存器,定义和书写记忆都很麻烦,为了简化定义和书写,ARM单片机寄存器一般使用(基地址+偏移地址)方式定义,使用结构体或(结构体+联合体)实现。以STM32F103单片机I/O寄存器为例,每组寄存器由一系列功能相同的寄存器组组成,声明一个GPIO结构体类型,然后分别定义各组GPIO的结构体:
  1. typedef struct
  2. {
  3.   __IO uint32_t CRL;
  4.   __IO uint32_t CRH;
  5.   __IO uint32_t IDR;
  6.   __IO uint32_t ODR;
  7.   __IO uint32_t BSRR;
  8.   __IO uint32_t BRR;
  9.   __IO uint32_t LCKR;
  10. } GPIO_TypeDef;

  11. #define GPIOA               ((GPIO_TypeDef *) 0x40010800)
  12. #define GPIOB               ((GPIO_TypeDef *) 0x40010C00)
  13. ……
复制代码
以写PB00为例,可以这样写:
  1. GPIOB -> ODR |= (0x01<<0);                        //PB00=1
  2. GPIOB -> ODR &= (~(0x01<<0));                //PB00=0
复制代码
如果不习惯这种程序风格,仍可按C51程序风格书写。寄存器和RAM地址在ARM单片机CPU看来是一样的,只管读写属性,以上述写PB00为例,可以这样写:
  1. #define        PB_O        (*((volatile uint32_t *)(0x40010C00 + 0x0C)))

  2. PB_O |= (0x01<<0);                        //PB00=1
  3. PB_O &= (~(0x01<<0));                //PB00=0
复制代码
Cortex-M3、Cortex-M4核心单片机有位带,位带可以实现RAM、寄存器位操作,虽然实现方式与MCS-51单片机不同,但是程序效率仍比直接对RAM、寄存器或、与立即数高。

网上流传的I/O位带操作宏:
  1. //位带操作,实现51类似的GPIO控制功能
  2. //具体实现思想,参考<<CM3权威指南>>第五章(87页~92页).
  3. //IO口操作宏定义
  4. #define IO_BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2))
  5. #define MEM_ADDR(addr)  *((volatile unsigned long  *)(addr))
  6. #define BIT_ADDR(addr, bitnum)   MEM_ADDR(IO_BITBAND(addr, bitnum))

  7. //IDR、ODR偏移量
  8. #define        GPIO_IDR_OFFSET                0x08
  9. #define        GPIO_ODR_OFFSET                0x0C

  10. //IO口地址映射
  11. #define        GPIOA_ODR_Addr        (GPIOA_BASE+GPIO_ODR_OFFSET)
  12. #define        GPIOB_ODR_Addr        (GPIOB_BASE+GPIO_ODR_OFFSET)
  13. #define        GPIOC_ODR_Addr        (GPIOC_BASE+GPIO_ODR_OFFSET)
  14. #define        GPIOD_ODR_Addr        (GPIOD_BASE+GPIO_ODR_OFFSET)
  15. #define        GPIOE_ODR_Addr        (GPIOE_BASE+GPIO_ODR_OFFSET)
  16. #define        GPIOF_ODR_Addr        (GPIOF_BASE+GPIO_ODR_OFFSET)
  17. #define        GPIOG_ODR_Addr        (GPIOG_BASE+GPIO_ODR_OFFSET)

  18. #define        GPIOA_IDR_Addr        (GPIOA_BASE+GPIO_IDR_OFFSET)
  19. #define        GPIOB_IDR_Addr        (GPIOB_BASE+GPIO_IDR_OFFSET)
  20. #define        GPIOC_IDR_Addr        (GPIOC_BASE+GPIO_IDR_OFFSET)
  21. #define        GPIOD_IDR_Addr        (GPIOD_BASE+GPIO_IDR_OFFSET)
  22. #define        GPIOE_IDR_Addr        (GPIOE_BASE+GPIO_IDR_OFFSET)
  23. #define        GPIOF_IDR_Addr        (GPIOF_BASE+GPIO_IDR_OFFSET)
  24. #define        GPIOG_IDR_Addr        (GPIOG_BASE+GPIO_IDR_OFFSET)

  25. //IO口操作,只对单一的IO口!
  26. //确保n的值小于16!
  27. #define PAout(n)   BIT_ADDR(GPIOA_ODR_Addr,n)  //输出
  28. #define PAin(n)    BIT_ADDR(GPIOA_IDR_Addr,n)  //输入

  29. #define PBout(n)   BIT_ADDR(GPIOB_ODR_Addr,n)  //输出
  30. #define PBin(n)    BIT_ADDR(GPIOB_IDR_Addr,n)  //输入

  31. #define PCout(n)   BIT_ADDR(GPIOC_ODR_Addr,n)  //输出
  32. #define PCin(n)    BIT_ADDR(GPIOC_IDR_Addr,n)  //输入

  33. #define PDout(n)   BIT_ADDR(GPIOD_ODR_Addr,n)  //输出
  34. #define PDin(n)    BIT_ADDR(GPIOD_IDR_Addr,n)  //输入

  35. #define PEout(n)   BIT_ADDR(GPIOE_ODR_Addr,n)  //输出
  36. #define PEin(n)    BIT_ADDR(GPIOE_IDR_Addr,n)  //输入

  37. #define PFout(n)   BIT_ADDR(GPIOF_ODR_Addr,n)  //输出
  38. #define PFin(n)    BIT_ADDR(GPIOF_IDR_Addr,n)  //输入

  39. #define PGout(n)   BIT_ADDR(GPIOG_ODR_Addr,n)  //输出
  40. #define PGin(n)    BIT_ADDR(GPIOG_IDR_Addr,n)  //输入
复制代码
以写PB00为例,可以简化为:
  1. PBout(0) = 1;
  2. PBout(0) = 0;
复制代码
七、点灯实例

功能:STM32F103C8 PB00接一个LED,亮0.5秒--灭0.5秒循环。电路如下图:

(1)启动文件。把Keil MDK或ST标准外设库中的启动文件startup_stm32f10x_ld.s添加到工程(根据芯片容量选择启动文件)。

(2)以外部8MHz晶振产生基础时钟,经PLL倍频到72MHz做为内核时钟。
使用ST标准外设库可以简单实现这一步,无需任何修改。

(3)计时。用SysTick定时器中断1mS计时,再累积1mS得到500mS。

(4)LED闪烁。将PB00设置为推挽输出态,每计时500mS改变一次PB00状态。

Proteus仿真效果如下图:


点灯实例工程(Keil + Proteus):


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册 微信登录

x
您需要登录后才可以回帖 登录 | 立即注册 微信登录

本版积分规则

APP|手机版|小黑屋|关于我们|联系我们|法律条款|技术知识分享平台

闽公网安备35020502000485号

闽ICP备2021002735号-2

GMT+8, 2025-7-25 04:42 , Processed in 0.202800 second(s), 9 queries , Redis On.

Powered by Discuz!

© 2006-2025 MyDigit.Net

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