KEIL 5.38的ARM-CM3/4 ARM汇编设计学习笔记6 - STM32的CAN学习与测试1
一、介绍
CAN总线是STM32的一个很重要的接口。在很多与伺服控制器、工控上位机等设备通讯的时候,CAN都是必须要支持的接口。然而奇怪的是,很多的嵌入式工程师似乎并不会使用。所以研究了相应的说明文件以后,我利用代码和调试来学习并测试了CAN接口。这里对学习和测试的过程进行说明。
有关CAN的协议这里就不赘述了。网上有很多讲的。我这里使用的开发与测试相关的软硬件如下表所示。
设备与配件 | 型号与参数 |
---|---|
IDE | Keil MDK-Community v5.38 |
开发板 | HX32F4 |
系统 | Windows 10 |
示波器 | SIGLENT SDS3104X HD |
二、目标
- 编写CAN的配置代码
- 发送CAN的数据帧或远程帧
- 在示波器上捕捉到CANH和CANL波形
- 接收到该帧
三、 实验设计
研究了一下STM32F4和F1的bxCAN,它们能发送远程/数据、标准/扩展这样一共4种帧。通过配置RTR和IDE位来实现。在本案例中,CAN1的GPIO口是PB8和PB9,功能配置都是AF9。那么一共要有下面的流程来实现从配置到应用。
首先和其他的接口或外设一样,整个设置过程有3步:
- 激活CAN和GPIO的时钟
- 设置GPIO
- 设置CAN
就设置CAN而言,一共有5步:
- 切换到Initialisation模式
- 设置波特率、回环与静默模式、中断
- 设置过滤器
- 启动过滤器
- 切换到Normal模式
接下来,就可以填写发送邮箱,就像填报表那样,发送数据了。
这里注意一下,CAN是没有DMA的。
这里如果只是为了学习,有个非常简单有效的办法。就是启动调试以后,在调试窗口填写邮箱,并发送。如果你还不是很熟悉有关的寄存器的话。
四、代码设计
4.1 创建CAN1_Drv.h和CAN1_Drv.s
创建这样一个C头文件,代码如下所示
#ifndef _CAN1_DRV_H_
#define _CAN1_DRV_H_
#include "stdint.h"
typedef struct {
void (*init)(uint32_t baudrate_Hz);
}CAN1_Def;
extern CAN1_Def can1;
#endif
再创建CAN1_Drv.s源文件,准备在这个文件中实现can1结构体。
get peripherials.s
rRCC rn r8
rGPIOB rn r9
rCAN1 rn r10
import SystemCoreClockUpdate
area text, code
align 4
preserve8
init proc
push {r4 - r11, lr}
; Enable the clocks.
; APB1 for CAN1, AHB1 for GPIOB
ldr rRCC, =RCC_BaseAddr
ldr r4, [rRCC, #RCC_AHB1ENR]
orr r4, #RCC_AHB1ENR_GPIOBEN
str r4, [rRCC, #RCC_AHB1ENR]
ldr r4, [rRCC, #RCC_APB1ENR]
orr r4, #RCC_APB1ENR_CAN1EN
str r4, [rRCC, #RCC_APB1ENR]
; Configure the pins for CAN1
; CAN_TX: PB8, AF9
; CAN_RX: PB9, AF9
ldr rGPIOB, =GPIOB_BaseAddr
ldr r4, [rGPIOB, #GPIOx_MODER]
bfc r4, #16, #4
orr r4, #(GPIOx_MODER_AFIO:shl:16):or:(GPIOx_MODER_AFIO:shl:18)
str r4, [rGPIOB, #GPIOx_MODER]
ldr r4, [rGPIOB, #GPIOx_OSPEEDR]
bfc r4, #16, #4
orr r4, #(GPIOx_OSPEEDR_VERYHIGH:shl:16):or:(GPIOx_OSPEEDR_VERYHIGH:shl:18)
str r4, [rGPIOB, #GPIOx_OSPEEDR]
ldr r4, [rGPIOB, #GPIOx_PUPDR]
bfc r4, #16, #4
orr r4, #(GPIOx_PUPDR_PU:shl:16):or:(GPIOx_PUPDR_PU:shl:18)
str r4, [rGPIOB, #GPIOx_PUPDR]
ldr r4, [rGPIOB, #GPIOx_AFRH]
bfc r4, #0, #4
orr r4, #(9:shl:0) :or: (9:shl:4)
str r4, [rGPIOB, #GPIOx_AFRH]
; Setup the CAN1
ldr rCAN1, =CAN1_BaseAddr
; Trans to Initialisation Mode
ldr r4, [rCAN1, #CAN_MCR]
bic r4, #CAN_MCR_SLEEP
orr r4, #CAN_MCR_INRQ
str r4, [rCAN1, #CAN_MCR]
; Set the Baudrate
; CAN1 is at APB1.
push {r0}
ldr r4, =SystemCoreClockUpdate
blx r4
lsr r4, r0, #2
; Now f_apb1 is r4.
pop {r1}
; TS1 and TS2 are 3, BRP + 1 is r4.
; Enable the loop-back mode
udiv r4, r4, r1
mov r5, #9
udiv r4, r4 ,r5
sub r4, #1
mov r5, #3
ldr r6, [rCAN1, #CAN_BTR]
bfc r6, #16, #6
bfi r6, r4, #0, #10
bfi r6, r5, #16, #4
bfi r6, r5, #20, #3
orr r6, #1:shl:30
str r6, [rCAN1, #CAN_BTR]
; Set the filter
ldr r4, [rCAN1, #CAN_FM1R]
orr r4, #1:shl:0
str r4, [rCAN1, #CAN_FM1R]
; The filtered STID is 11, data frame
; Enable the filter F0R1
mov r4, #11:shl:21
str r4, [rCAN1, #CAN_F0R1]
ldr r4, [rCAN1, #CAN_FA1R]
orr r4, #1:shl:0
str r4, [rCAN1, #CAN_FA1R]
ldr r4, [rCAN1, #CAN_FMR]
bic r4, #1:shl:0
str r4, [rCAN1, #CAN_FMR]
; Switch CAN1 Peripherial from Initialisation to Normal State
ldr r4, [rCAN1, #CAN_MCR]
bic r4, #1:shl:0
str r4, [rCAN1, #CAN_MCR]
; Make a test
ldr r4, =Test_String
ldr r4, [r4]
str r4, [rCAN1, #CAN_TDL0R]
mov r4, #4
ldr r5, [rCAN1, #CAN_TDT0R]
bfi r5, r4, #0, #3
str r5, [rCAN1, #CAN_TDT0R]
ldr r4, [rCAN1, #CAN_TI0R]
mov r4, #11:shl:21
str r4, [rCAN1, #CAN_TI0R]
pop {r4 - r11, lr}
bx lr
endp
Test_String dcb "Andy"
align 4
can1 dcd init
export can1
end
通过这样源文件,就成功配置好了bxCAN1。下面对这段代码进行解释。
4.2 源文件配置
get peripherials.s
rRCC rn r8
rGPIOB rn r9
rCAN1 rn r10
import SystemCoreClockUpdate
area text, code
align 4
preserve8
peripherials.s是另一个汇编源文件,里面是我定义的有关的寄存器地址。类似于stm32f4xx.h那种文件。代码如下所示。
if :def: _PERIPHERIALS_H_
else
gbla _PERIPHERIALS_H_
RCC_BaseAddr equ 0x40023800
RCC_CR equ 0x00
RCC_PLLCFGR equ 0x04
RCC_CFGR equ 0x08
RCC_CIR equ 0x0C
RCC_AHB1ENR equ 0x30
RCC_AHB2ENR equ 0x34
RCC_AHB3ENR equ 0x38
RCC_APB1ENR equ 0x40
RCC_APB2ENR equ 0x44
RCC_CR_HSEON equ 1:shl:16
RCC_CR_PLLON equ 1:shl:24
RCC_PLLCFGR_PLLSRC equ 1:shl:22
RCC_PLLCFGR_PLLP_DIV_2 equ 2_00:shl:16
RCC_PLLCFGR_PLLP_DIV_4 equ 2_01:shl:16
RCC_PLLCFGR_PLLP_DIV_6 equ 2_10:shl:16
RCC_PLLCFGR_PLLP_DIV_8 equ 2_11:shl:16
RCC_CFGR_PPRE2_APB2_DIV_2 equ 2_100:shl:13
RCC_CFGR_PPRE2_APB2_DIV_4 equ 2_101:shl:13
RCC_CFGR_PPRE2_APB2_DIV_8 equ 2_110:shl:13
RCC_CFGR_PPRE2_APB2_DIV_16 equ 2_111:shl:13
RCC_CFGR_PPRE1_APB1_DIV_2 equ 2_100:shl:10
RCC_CFGR_PPRE1_APB1_DIV_4 equ 2_101:shl:10
RCC_CFGR_PPRE1_APB1_DIV_8 equ 2_110:shl:10
RCC_CFGR_PPRE1_APB1_DIV_16 equ 2_111:shl:10
RCC_CFGR_SW_HSE equ 2_01
RCC_CFGR_SW_PLL equ 2_10
RCC_AHB1ENR_GPIOBEN equ 1:shl:1
RCC_APB1ENR_CAN1EN equ 1:shl:25
FLASH_BaseAddr equ 0x40023C00
FLASH_ACR equ 0x00
FLASH_ACR_DCEN equ 1:shl:10
FLASH_ACR_ICEN equ 1:shl:9
FLASH_ACR_PRFTEN equ 1:shl:8
GPIOB_BaseAddr equ 0x40020400
GPIOx_MODER equ 0x00
GPIOx_OTYPER equ 0x04
GPIOx_OSPEEDR equ 0x08
GPIOx_PUPDR equ 0x0C
GPIOx_IDR equ 0x10
GPIOx_ODR equ 0x14
GPIOx_BSRR equ 0x18
GPIOx_LCKR equ 0x1C
GPIOx_AFRL equ 0x20
GPIOx_AFRH equ 0x24
GPIOx_MODER_AFIO equ 2_10
GPIOx_OSPEEDR_LOW equ 2_00
GPIOx_OSPEEDR_MEDIATE equ 2_01
GPIOx_OSPEEDR_HIGH equ 2_00
GPIOx_OSPEEDR_VERYHIGH equ 2_11
GPIOx_PUPDR_NONE equ 2_00
GPIOx_PUPDR_PU equ 2_01
GPIOx_PUPDR_PD equ 2_10
CAN1_BaseAddr equ 0x40006400
; CAN control and status registers
CAN_MCR equ 0x00
CAN_MSR equ 0x04
CAN_TSR equ 0x08
CAN_RF0R equ 0x0C
CAN_RF1R equ 0x10
CAN_IER equ 0x14
CAN_ESR equ 0x18
CAN_BTR equ 0x1C
; CAN mailbox registers
CAN_TI0R equ 0x180
CAN_TDT0R equ 0x184
CAN_TDL0R equ 0x188
CAN_TDH0R equ 0x18C
CAN_TI1R equ 0x190
CAN_TDT1R equ 0x194
CAN_TDL1R equ 0x198
CAN_TDH1R equ 0x19C
CAN_TI2R equ 0x1A0
CAN_TDT2R equ 0x1A4
CAN_TDL2R equ 0x1A8
CAN_TDH2R equ 0x1AC
CAN_RI0R equ 0x1B0
CAN_RDT0R equ 0x1B4
CAN_RDL0R equ 0x1B8
CAN_RDH0R equ 0x1BC
CAN_RI1R equ 0x1C0
CAN_RDT1R equ 0x1C4
CAN_RDL1R equ 0x1C8
CAN_RDH1R equ 0x1CC
; CAN filter registers
CAN_FMR equ 0x200
CAN_FM1R equ 0x204
CAN_FS1R equ 0x20C
CAN_FFA1R equ 0x214
CAN_FA1R equ 0x21C
CAN_F0R1 equ 0x240
CAN_F27R2 equ 0x31C
CAN_MCR_SLEEP equ 1:shl:1
CAN_MCR_INRQ equ 1:shl:0
CAN_TIxR_TXRQ equ 1:shl:0
CAN_TIxR_RTR equ 1:shl:1
CAN_TIxR_IDE equ 1:shl:2
endif
end
将C函数void SystemCoreClockUpdate()
从外部引入。但是由于这个函数要求栈8字节地址预留,所以必须加入preserve8
伪指令,否则报警告。
接下来创建void init((uint32_t baudrate_Hz)
函数。
4.3 启动时钟
在Keil环境中,STM32F407的启动代码只给加载了内部RC震荡器作为AHB源。但是我们想切换到PLL。参考我前面的《KEIL 5.38的ARM-CM3/4 ARM汇编设计学习笔记2——设置PLL》,我将时钟设置为144MHz。之后就是使能AHB1的GPIOB时钟。
; Enable the clocks.
; APB1 for CAN1, AHB1 for GPIOB
ldr rRCC, =RCC_BaseAddr
ldr r4, [rRCC, #RCC_AHB1ENR]
orr r4, #RCC_AHB1ENR_GPIOBEN
str r4, [rRCC, #RCC_AHB1ENR]
ldr r4, [rRCC, #RCC_APB1ENR]
orr r4, #RCC_APB1ENR_CAN1EN
str r4, [rRCC, #RCC_APB1ENR]
4.4 配置引脚
如果是STM32F407,则需要将CAN的这两个引脚都设置为AFIO,根据芯片手册《STM32F405xx STM32F407xx》,CAN1的两个引脚是PB8和PB9,AFIO号是AF9。但是如果是STM32F103,那么CAN_RX配置为Input,CAN_TX配置为AFIO。
; Configure the pins for CAN1
; CAN_TX: PB8, AF9
; CAN_RX: PB9, AF9
ldr rGPIOB, =GPIOB_BaseAddr
ldr r4, [rGPIOB, #GPIOx_MODER]
bfc r4, #16, #4
orr r4, #(GPIOx_MODER_AFIO:shl:16):or:(GPIOx_MODER_AFIO:shl:18)
str r4, [rGPIOB, #GPIOx_MODER]
ldr r4, [rGPIOB, #GPIOx_OSPEEDR]
bfc r4, #16, #4
orr r4, #(GPIOx_OSPEEDR_VERYHIGH:shl:16):or:(GPIOx_OSPEEDR_VERYHIGH:shl:18)
str r4, [rGPIOB, #GPIOx_OSPEEDR]
ldr r4, [rGPIOB, #GPIOx_PUPDR]
bfc r4, #16, #4
orr r4, #(GPIOx_PUPDR_PU:shl:16):or:(GPIOx_PUPDR_PU:shl:18)
str r4, [rGPIOB, #GPIOx_PUPDR]
ldr r4, [rGPIOB, #GPIOx_AFRH]
bfc r4, #0, #4
orr r4, #(9:shl:0) :or: (9:shl:4)
str r4, [rGPIOB, #GPIOx_AFRH]
4.5 配置bxCAN1
使能MCR的INAK,清除SLEEP位。将bxCAN1唤醒,进入初始化状态。在初始化状态中涉及到MCR和BTR两个寄存器。
; Trans to Initialisation Mode
ldr r4, [rCAN1, #CAN_MCR]
bic r4, #CAN_MCR_SLEEP
orr r4, #CAN_MCR_INRQ
str r4, [rCAN1, #CAN_MCR]
设置波特率。这里我为了实现1个形参表示波特率,我调用void SystemCoreClockUpdate(void)
函数获得当前的HCLK的频率。由于这个函数的最后一句其实就是吧HCLK的值放到了r0,所以调用了这个函数以后,r0中就是HCLK的频率值。由于我的APB1是HCLK的4分频(我在RCC那边就是这么写的),我因此就获得了APB1的频率。
根据这些数值,算出了要填写的TS0、TS1和BRP的值。由于我们测试要使用回环模式,不然发的邮件一定是回不来的。这样填写一下BTR。如果不使用中断且只是发送邮件,CAN1的初始化就是完成了。
; Set the Baudrate
; CAN1 is at APB1.
push {r0}
ldr r4, =SystemCoreClockUpdate
blx r4
lsr r4, r0, #2
; Now f_apb1 is r4.
pop {r1}
; TS1 and TS2 are 3, BRP + 1 is r4.
; Enable the loop-back mode
udiv r4, r4, r1
mov r5, #9
udiv r4, r4 ,r5
sub r4, #1
mov r5, #3
ldr r6, [rCAN1, #CAN_BTR]
bfc r6, #16, #6
bfi r6, r4, #0, #10
bfi r6, r5, #16, #4
bfi r6, r5, #20, #3
orr r6, #1:shl:30
str r6, [rCAN1, #CAN_BTR]
4.6 接收和过滤器设置
但是我们还是要接受数据的,所以我们要配置接收。跟接收相关的其实就是个过滤器,涉及到那几个接收过滤器以及相关的配置寄存器。这里注意,如果没有任何一个过滤器是激活的,肯定是不会有任何信息被送到邮箱里的。过滤器配置寄存器的设置要在FMR的FINIT位为1的时候进行配置。配完了记得清除。
; Set the filter
ldr r4, [rCAN1, #CAN_FM1R]
orr r4, #1:shl:0
str r4, [rCAN1, #CAN_FM1R]
; The filtered STID is 11, data frame
; Enable the filter F0R1
mov r4, #11:shl:21
str r4, [rCAN1, #CAN_F0R1]
ldr r4, [rCAN1, #CAN_FA1R]
orr r4, #1:shl:0
str r4, [rCAN1, #CAN_FA1R]
ldr r4, [rCAN1, #CAN_FMR]
bic r4, #1:shl:0
str r4, [rCAN1, #CAN_FMR]
我这里设置的是使能过滤器0,设置为16位过滤器,List Mode模式, 导入邮箱0。通过的STID是11(0x0B)的标准数据帧。
4.7 转入Normal状态
清除MCR-INAK位将CAN1转入Normal状态。
; Switch CAN1 Peripherial from Initialisation to Normal State
ldr r4, [rCAN1, #CAN_MCR]
bic r4, #1:shl:0
str r4, [rCAN1, #CAN_MCR]
4.8 做个测试
我放了4个字节"Andy“在函数结束之后的位置。
; Make a test
ldr r4, =Test_String
ldr r4, [r4]
str r4, [rCAN1, #CAN_TDL0R]
mov r4, #4
ldr r5, [rCAN1, #CAN_TDT0R]
bfi r5, r4, #0, #3
str r5, [rCAN1, #CAN_TDT0R]
ldr r4, [rCAN1, #CAN_TI0R]
mov r4, #11:shl:21
str r4, [rCAN1, #CAN_TI0R]
pop {r4 - r11, lr}
bx lr
Test_String dcb "Andy"
endp
align 4
can1 dcd init
export can1
编译,调试。调试窗口如下所示。
在CAN1-TI0R-TXRQ那里,点击一下。会看到2个现象:
- RF0R-FMP0那里变成了1。这时候会发现RI0R,RDT0R,RDL0R也都有相应的更新。就是说,收到了数据。
- 示波器上也会有波形显示。
这里看到,示波器的CAN协议分析可以抓到这个包,数据也正确。但是唯一一点要说的是,由于我们采用的是回环模式自发自收,所以ACK那里显示no。如果用Lecroy的示波器则会提示ACK Error。这里暂时认为是回环模式的原因造成的。
关于鼎阳的这个示波器的CAN协议分析软件,根据和官方的确认,目前存在一个bug。那就是,必须要把波形拖拽到靠近屏幕的右边协议分析软件才能生效。这个要自己调整。大概就是让ACK应该出现的那个位置卡在屏幕右边缘。鼎阳官方确认这是个一个bug,可能在后面的固件升级的时候修正。那就让我们拭目以待吧。
五 结论
根据上面所述的实验,我们可以通过操作bxCAN相关寄存器的方法很简单地实现CAN的收发。如果要实现一个可靠的完整的CAN接口的通讯驱动,还需要实现环形队列和接收中断。就像其他的MCU通讯接口一样,CAN的配置与应用也是大致下面所述的个步骤。
- 使能外设和相关GPIO的时钟
- 配置管脚
- 使能该外设或进入该外设的初始化状态
- 配置波特率、中断和接收过滤器
- 进入正常状态
可以看出,通过以上的配置,就可以实现数据的收发。非常高效。