KEIL 5.38的ARM-CM3/4 ARM汇编设计学习笔记3——串口Stdio实现

一、介绍

这个测试主要是在ARM核里运行。所以如果使用正点的板子或者其他的407的板子,都可以做出来。做了好几天出来。也是为了自己练习编程,也是为了留个笔记,以后用的时候可以找到。

Всё, давай начнём.

任务目标

1、 实现USB-UART的串口驱动。
2、支持STDIO,并通过串口调试工具进行交互

发送比较简单,就是有发送的需求的时候就用usart->dr直接发。但是接收数据的话,这个UART没有CTS和DTS等其他信号的辅助,就做一个缓冲区,只要有数据来了就用中断ISR将数据送到缓冲区。读的时候就把数据从缓冲区里读出去。缓冲区的操作是FIFO。
试验环境

环境描述
MCUSTM32F407VGT6
IDEKEIL
串口USART1, Baud rate: 115200Hz
PinTX: PA10,RX: PA9

未来在使用这个驱动的时候,会通过管程来操作。也就是说,不会出现多线程对这个驱动进行调用。

二、工程创建

创建流程是:
第一步:创建新项目,选择STM32F407VGT6。
第二步:RTE选择CMSIS-CORE,CMSIS-RTX5-Lib,Device-Startup,Compiler-I/O-STDIN和Compiler-I/O-STDOUT。
在这里插入图片描述

第三步:在工作文件夹下创建Code/Application,Code/BSP和Code/Support三个文件夹。
第四步:在魔法棒下:

  1. Target标签选中IRAM2(其实没有真的用,只是勾上,任性。)
  2. C/C++(AC6)标签,将Language C调成c11;在Include Path里添加第三步里建的3个文件夹。
  3. Asm标签,将Assembler Option的汇编器选中除了GUN Syntax的其他选项;在Include Path里添加第三步里建的3个文件夹。
  4. Debug标签,选中CMSIS-DAP,并点一下Setting并确认。

第五步:点击“品”那个按钮,把原来的Group删了,创建3个Group,名字和我们创建的3个文件夹一致。
这样,我们的工程配置就完成了。

三、软件设计

第一步,BSP构建

在这个试验中,我在BSP里面需要构建Application、BSP和Support这3个包。这里我就用mermaid简单画画,我就不去开enterprise architecture了。

Application
BSP
Support

1, 添加前面的pll_config文件

把前面写的《KEIL 5.38的ARM-CM3/4 ARM汇编设计学习笔记2——设置PLL》中创建的pll_config.h和pll_config.s复制到BSP里面,并在“品”里面添加到BSP包里。但是修改有关的数据,将主频调成144MHz。这样后面实现115200的波特率就数值上比较好做。

2,创建irqn_vector.s

这个就是把stm32f407xx.h文件中的所有的中断向量连同中断号一起以汇编文件的方式定义一下。

;	irqn_vector.s
;	Author:超级喵窝窝
;

WWDG_IRQn                   equ		0	  ; Window WatchDog Interrupt                                         
PVD_IRQn                    equ		1	  ; PVD through EXTI Line detection Interrupt                         
TAMP_STAMP_IRQn             equ		2	  ; Tamper and TimeStamp interrupts through the EXTI line             
RTC_WKUP_IRQn               equ		3	  ; RTC Wakeup interrupt through the EXTI line                        
FLASH_IRQn                  equ		4	  ; FLASH global Interrupt                                            
RCC_IRQn                    equ		5	  ; RCC global Interrupt                                              
EXTI0_IRQn                  equ		6	  ; EXTI Line0 Interrupt                                              
EXTI1_IRQn                  equ		7	  ; EXTI Line1 Interrupt                                              
EXTI2_IRQn                  equ		8	  ; EXTI Line2 Interrupt                                              
EXTI3_IRQn                  equ		9	  ; EXTI Line3 Interrupt                                              
EXTI4_IRQn                  equ		10	  ; EXTI Line4 Interrupt                                              
DMA1_Stream0_IRQn           equ		11	  ; DMA1 Stream 0 global Interrupt                                    
DMA1_Stream1_IRQn           equ		12	  ; DMA1 Stream 1 global Interrupt                                    
DMA1_Stream2_IRQn           equ		13	  ; DMA1 Stream 2 global Interrupt                                    
DMA1_Stream3_IRQn           equ		14	  ; DMA1 Stream 3 global Interrupt                                    
DMA1_Stream4_IRQn           equ		15	  ; DMA1 Stream 4 global Interrupt                                    
DMA1_Stream5_IRQn           equ		16	  ; DMA1 Stream 5 global Interrupt                                    
DMA1_Stream6_IRQn           equ		17	  ; DMA1 Stream 6 global Interrupt                                    
ADC_IRQn                    equ		18	  ; ADC1, ADC2 and ADC3 global Interrupts                             
CAN1_TX_IRQn                equ		19	  ; CAN1 TX Interrupt                                                 
CAN1_RX0_IRQn               equ		20	  ; CAN1 RX0 Interrupt                                                
CAN1_RX1_IRQn               equ		21	  ; CAN1 RX1 Interrupt                                                
CAN1_SCE_IRQn               equ		22	  ; CAN1 SCE Interrupt                                                
EXTI9_5_IRQn                equ		23	  ; External Line[9:5] Interrupts                                     
TIM1_BRK_TIM9_IRQn          equ		24	  ; TIM1 Break interrupt and TIM9 global interrupt                    
TIM1_UP_TIM10_IRQn          equ		25	  ; TIM1 Update Interrupt and TIM10 global interrupt                  
TIM1_TRG_COM_TIM11_IRQn     equ		26	  ; TIM1 Trigger and Commutation Interrupt and TIM11 global interrupt 
TIM1_CC_IRQn                equ		27	  ; TIM1 Capture Compare Interrupt                                    
TIM2_IRQn                   equ		28	  ; TIM2 global Interrupt                                             
TIM3_IRQn                   equ		29	  ; TIM3 global Interrupt                                             
TIM4_IRQn                   equ		30	  ; TIM4 global Interrupt                                             
I2C1_EV_IRQn                equ		31	  ; I2C1 Event Interrupt                                              
I2C1_ER_IRQn                equ		32	  ; I2C1 Error Interrupt                                              
I2C2_EV_IRQn                equ		33	  ; I2C2 Event Interrupt                                              
I2C2_ER_IRQn                equ		34	  ; I2C2 Error Interrupt                                              
SPI1_IRQn                   equ		35	  ; SPI1 global Interrupt                                             
SPI2_IRQn                   equ		36	  ; SPI2 global Interrupt                                             
USART1_IRQn                 equ		37	  ; USART1 global Interrupt                                           
USART2_IRQn                 equ		38	  ; USART2 global Interrupt                                           
USART3_IRQn                 equ		39	  ; USART3 global Interrupt                                           
EXTI15_10_IRQn              equ		40	  ; External Line[15:10] Interrupts                                   
RTC_Alarm_IRQn              equ		41	  ; RTC Alarm (A and B) through EXTI Line Interrupt                   
OTG_FS_WKUP_IRQn            equ		42	  ; USB OTG FS Wakeup through EXTI line interrupt                     
TIM8_BRK_TIM12_IRQn         equ		43	  ; TIM8 Break Interrupt and TIM12 global interrupt                   
TIM8_UP_TIM13_IRQn          equ		44	  ; TIM8 Update Interrupt and TIM13 global interrupt                  
TIM8_TRG_COM_TIM14_IRQn     equ		45	  ; TIM8 Trigger and Commutation Interrupt and TIM14 global interrupt 
TIM8_CC_IRQn                equ		46	  ; TIM8 Capture Compare global interrupt                             
DMA1_Stream7_IRQn           equ		47	  ; DMA1 Stream7 Interrupt                                            
FSMC_IRQn                   equ		48	  ; FSMC global Interrupt                                             
SDIO_IRQn                   equ		49	  ; SDIO global Interrupt                                             
TIM5_IRQn                   equ		50	  ; TIM5 global Interrupt                                             
SPI3_IRQn                   equ		51	  ; SPI3 global Interrupt                                             
UART4_IRQn                  equ		52	  ; UART4 global Interrupt                                            
UART5_IRQn                  equ		53	  ; UART5 global Interrupt                                            
TIM6_DAC_IRQn               equ		54	  ; TIM6 global and DAC1&2 underrun error  interrupts                 
TIM7_IRQn                   equ		55	  ; TIM7 global interrupt                                             
DMA2_Stream0_IRQn           equ		56	  ; DMA2 Stream 0 global Interrupt                                    
DMA2_Stream1_IRQn           equ		57	  ; DMA2 Stream 1 global Interrupt                                    
DMA2_Stream2_IRQn           equ		58	  ; DMA2 Stream 2 global Interrupt                                    
DMA2_Stream3_IRQn           equ		59	  ; DMA2 Stream 3 global Interrupt                                    
DMA2_Stream4_IRQn           equ		60	  ; DMA2 Stream 4 global Interrupt                                    
ETH_IRQn                    equ		61	  ; Ethernet global Interrupt                                         
ETH_WKUP_IRQn               equ		62	  ; Ethernet Wakeup through EXTI line Interrupt                       
CAN2_TX_IRQn                equ		63	  ; CAN2 TX Interrupt                                                 
CAN2_RX0_IRQn               equ		64	  ; CAN2 RX0 Interrupt                                                
CAN2_RX1_IRQn               equ		65	  ; CAN2 RX1 Interrupt                                                
CAN2_SCE_IRQn               equ		66	  ; CAN2 SCE Interrupt                                                
OTG_FS_IRQn                 equ		67	  ; USB OTG FS global Interrupt                                       
DMA2_Stream5_IRQn           equ		68	  ; DMA2 Stream 5 global interrupt                                    
DMA2_Stream6_IRQn           equ		69	  ; DMA2 Stream 6 global interrupt                                    
DMA2_Stream7_IRQn           equ		70	  ; DMA2 Stream 7 global interrupt                                    
USART6_IRQn                 equ		71	  ; USART6 global interrupt                                           
I2C3_EV_IRQn                equ		72	  ; I2C3 event interrupt                                              
I2C3_ER_IRQn                equ		73	  ; I2C3 error interrupt                                              
OTG_HS_EP1_OUT_IRQn         equ		74	  ; USB OTG HS End Point 1 Out global interrupt                       
OTG_HS_EP1_IN_IRQn          equ		75	  ; USB OTG HS End Point 1 In global interrupt                        
OTG_HS_WKUP_IRQn            equ		76	  ; USB OTG HS Wakeup through EXTI interrupt                          
OTG_HS_IRQn                 equ		77	  ; USB OTG HS global interrupt                                       
DCMI_IRQn                   equ		78	  ; DCMI global interrupt                                             
RNG_IRQn                    equ		80	  ; RNG global Interrupt                                              
FPU_IRQn                    equ		81	  ; FPU global interrupt 
	  
	  end

3,将常用的寄存器和外设地址命名

其实主要是因为STM32没有很合适的,类似stm32f407xx.h那样的头文件可以帮你把所有外设都定义好。不要紧,我们手动把会用到的外设按照自己的使用偏好定义了就好。

;	registers.s
;	Author: 超级喵窝窝
; 	This file is used to define the registers used in this project.


; General Purpose Registers
callee_regs							rlist	{r4-r12,lr}

;SCB
SCB_BaseAddr						equ		0xE000ED00
SCB_CPUID							equ		0xE000ED00
SCB_VTOR							equ		0xE000ED08

;NVIC
NVIC_BaseAddr						equ		0xE000E100
NVIC_ISER							equ		0xE000E100
NVIC_ICER							equ		0xE000E180
NVIC_ISPR							equ		0xE000E200
NVIC_ICPR							equ		0xE000E280	
NVIC_IABR							equ		0xE000E300
NVIC_IPR							equ		0xE000E400

; The Software trigger interrupt register is only available in STM32?
NVIC_STIR							equ		0xE000EF00

;RCC 
RCC_BaseAddr						equ  	0x40023800 ;  
RCC_CR   							equ  	0x00   
RCC_CR_HSEON   						equ  	0x10000
RCC_CR_HSERDY   					equ  	0x20000
RCC_CR_PLLON   						equ  	0x1000000
RCC_CR_PLLRDY   					equ  	0x2000000

RCC_PLLCFGR  						equ  	0x04
RCC_PLLCFGR_PLLSRC_HSE 				equ  	0x400000
	
RCC_CFGR  							equ  	0x08
RCC_CFGR_PPRE1_DIV2  				equ  	0x800
RCC_CFGR_PPRE2_DIV2  				equ  	0x8000
RCC_CFGR_SW_PLL   					equ  	0x02

RCC_AHB1ENR							equ	  	0x30
RCC_APB2ENR							equ	  	0x44
	
RCC_ABP2ENR_SPI2					equ		0x01:ROL:12
RCC_APB2ENR_USART1EN				equ		0x01:ROL:4
RCC_AHB1ENR_GPIOAEN					equ		0x01:ROL:0
RCC_AHB1ENR_GPIOBEN					equ		0x01:ROL:1
RCC_AHB1ENR_GPIOCEN					equ		0x01:ROL:2	
;Flash
FLASH_BaseAddr  					equ  	0x40023C00
	
FLASH_ACR   						equ  	0x00	
FLASH_ACR_PRFTEN 					equ  	0x100
FLASH_ACR_ICEN   					equ  	0x200  
FLASH_ACR_DCEN   					equ  	0x400
FLASH_ACR_SET   					equ  	0x705

;GPIO
;GPIO_BaseAddr

GPIOA_BaseAddr						equ		0x40020000
GPIOB_BaseAddr						equ	 	0x40020400
GPIOC_BaseAddr						equ	 	0x40020800	
	
;GPIO registers offset
GPIO_MODER							equ		0x00	
GPIO_OTYPER							equ		0x04
GPIO_OSPEEDR						equ		0x08
GPIO_PUPDR							equ		0x0C
GPIO_BSRR							equ		0x18
GPIO_AFRL							equ		0x20
GPIO_AFRH							equ		0x24
;USART 
;USART_BaseAddr
USART1_BaseAddr						equ		0x40011000
USART2_BaseAddr						equ		0x40004400
	
;USART registers offset
USART_SR							equ		0x00
USART_DR 							equ		0x04
USART_BRR							equ		0x08
USART_CR1							equ		0x0C
USART_CR2							equ		0x10
USART_CR3							equ		0x14
USART_GTPR							equ 	0x18

USART_SR_TC							equ		1:ROL:6
USART_SR_RXNE						equ		1:ROL:5
											 
USART_CR1_TE						equ		1:ROL:3
USART_CR1_RE						equ		1:ROL:2
USART_CR1_UE						equ		1:ROL:13
USART_CR1_RXNEIE					equ		1:ROL:5
	
	end

这里,有关的汇编命令,包括rlist、:ROL:(这里有人也说是伪指令。不知道该叫什么。但是手册里面英文名字叫Directives)参考文件《ARM Developer Suite Assembler Guide》

例如:rlist是定义一个寄存器列表,:rol:是ARM汇编器的运算符。注意它们不是ARM指令或者是Thumb指令,是汇编器在汇编代码的时候运行的。跟C编译器的宏定义类似。

4,创建usb_uart.h和usb_uart.s

4.1, usb_uart.h的创建与封装

首先创建usb_uart.h文件,完成C语言的封装。

#ifndef _USB_UART_H_
#define	_USB_UART_H_

#include "stdint.h"

typedef struct{
	void (*init)(void);
	void (*send_ch)(uint8_t);
	uint8_t (*read_ch)(void);
}USB_UART_Def;
extern USB_UART_Def usb_uart;

#endif
4.2, usb_uart.s的创建与封装

就这块开发板而言,根据它的说明文档,使用的是uart1,并且波特率必须是115200。

4.2.1, 定义配置宏

那么创建usb_uart.s。首先定义本文内要用的宏定义。这里通过配置的Manual Configuration里面的宏,就可以完成设置。设置包括三方面:

  1. 时钟启动,启动gpio和要用的USART。
  2. Pin脚设置,设置TX和RX的pin脚,主要是MODER和AFIO。有关AFIO的值要去查Datasheet。
  3. 中断设置,主要设置优先级。由于usart的中断是由NVIC直接控制。所以不需要对SYSCFG进行操作。
  4. UART设置,主要包括波特率、使能、停止位和中断等。
;------------------------------------------
;	usb_uart.s
;	Author: 超级喵窝窝
;	Description: UART1 is used.
;						TX: PA10
;						RX: PA9
;				Baud rate: 115200Hz
;-------------------------------------------


			get registers.s
			get irqn_vector.s

;	Manual Configurations
GPIO_PORT					equ		GPIOA_BaseAddr			; 	Tell the Hardware engineer to forget UART5, Coz it uses PC12 and PD2.
USART_PORT					equ		USART1_BaseAddr
USART_IRQn					equ		USART1_IRQn
TX_Pin						equ		0x0A					;	PA10 is Port A Pin 10
RX_Pin						equ		0x09					;	PA9 is Port A Pin 9
GPIO_PORT_CLOCK_BUSEN		equ		RCC_AHB1ENR
GPIO_PORT_CLOCK_BIT			equ		RCC_AHB1ENR_GPIOAEN
USART_CLOCK_BUSEN			equ		RCC_APB2ENR
USART_CLOCK_BIT				equ		RCC_APB2ENR_USART1EN
RX_PIN_AF					equ		0x07					; 	Check this value with the datasheet.
TX_PIN_AF					equ		0x07					; 	Check this value with the datasheet.				
NVIC_USART_PRIO_VAL			equ		1						; 	Set the Priority of USART_IRQn to 1
buffer_size					equ		100

完成以上的设置以后,后面有关的设置利用汇编器进行计算。这里面有几点注意:

  1. equspacern都前面不能有空格。
  2. IF...ELSE...ENDIFENDAREAMACROMEND等前面必须有空格。
  3. 汇编助记符、ARM寄存器名称和汇编器Directives不区分大小写,但是必须有一致性。比如可以写equEQU,但是不能写eQu
  4. ‘\’可以实现分行
  5. 本文中的所有的宏定义、重命名只在本文中生效。用EXPORT引出的例外。
;Calculated by the Assembler

TX_PIN_MODER_VAL			equ		0x02 :rol: (2 * TX_Pin)
RX_PIN_MODER_VAL			equ		0x02 :rol: (2 * RX_Pin)
	IF RX_Pin<0x08
GPIO_PORT_AFIO				equ		GPIO_AFRL
RX_PIN_AFIO_VAL				equ		RX_PIN_AF :rol: (4* RX_Pin )
TX_PIN_AFIO_VAL				equ		TX_PIN_AF :rol: (4* TX_Pin )
	ELSE
GPIO_PORT_AFIO				equ		GPIO_AFRH
RX_PIN_AFIO_VAL				equ		RX_PIN_AF :rol: ( 4 * (RX_Pin - 0x08))
TX_PIN_AFIO_VAL				equ		TX_PIN_AF :rol: ( 4 * (TX_Pin - 0x08))
	ENDIF

; Calculate the address offsets of ISER, ICER, ICPR and IPR for USART1.
NVIC_ISER_USART1_OFFSET 	equ		\
	NVIC_ISER - NVIC_BaseAddr + USART_IRQn / 32 * 4
NVIC_ICER_USART1_OFFSET		equ		\
	NVIC_ICER - NVIC_BaseAddr + USART_IRQn / 32 * 4
NVIC_ICPR_USART1_OFFSET		equ		\
	NVIC_ICPR - NVIC_BaseAddr + USART_IRQn / 32 * 4
NVIC_IPR_USART1_OFFSET 		equ		\
	NVIC_IPR - NVIC_BaseAddr + USART_IRQn / 4 * 4	

NVIC_USART_BIT				equ		\
	1 :rol: (USART_IRQn :mod: 32)
NVIC_IPR_USART_PRIO_VAL		equ 	\
	NVIC_USART_PRIO_VAL :rol:(USART_IRQn :mod: 4 * 8 + 4)

; Configure the USART1
		; APB2:	72MHz
		; USARTDIV = 72MHz / ( 16 * 115200) = 39.0625
		; So the BSS_Mantissa = 39, Fraction = 1	
USART_BSS_VAL		equ		0x271
4.2.2 Buffer FIFO的定义

接下来做一个buffer,按照FIFO的规则进行访问。这里做一个固定尺寸的线性表就可以。定义一个3个标量。

名称用途
uart_buffer_nth_recv将随时接收到的数据写入FIFO
uart_buffer_nth_read读出已写入的数据
usart_buffer_nData缓冲区内的数据数量

关于定义数据变量和数据块。

;	Apply a buffer for the Received Data. This buffer is actually a FIFO.
	area USB_USART_DATA_SECTION, data	

uart_buffer				space		buffer_size
	align	4
uart_buffer_nth_recv	space		4
uart_buffer_nth_read	space		4
usart_buffer_nData		space		4
	
pBuf_BaseAddr			equ			uart_buffer
pRecv					equ			uart_buffer_nth_recv - uart_buffer
pRead					equ			uart_buffer_nth_read - uart_buffer
nData					equ			usart_buffer_nData - uart_buffer
  1. space划出一块内存给本文件里的函数。这个类似C语言的文内静态全局变量。
  2. 将段定义到rw段即可,则数据地址就会被定义到片上SRAM。但是如果非要定义到CCM上,需要进行链接脚本操作。
  3. 缓冲区里的数据都是字节,也只进行字节访问,所以不需要进行地址对齐。
  4. 后面的几个变量考虑用32位数据,所以必须进行地址对齐。
  5. align 4实现4字节地址对齐。
4.2.3 文内寄存器的重定义指定

经过分析,我们发现,其实本文中所有的函数中会常用的外设寄存器和其他内存地址有:

  1. RCC
  2. GPIO
  3. UART
  4. pBuf
  5. Buf_Size

考虑到在内核里有r0-r12共13个通用寄存器,我就尝试先奢侈一下。将所有的高寄存器(High Registers,r8 - r12)都用于放这种地址。当然,这里面还是占用了一个r7。不过先写写看,先不去规划寄存器。然后再定义有关的宏,用于在函数中加载地址。

; Define the registers commonly used in this file.
rRCC			rn		r12
rGPIO			rn		r11
rUART			rn		r10	
rNVIC			rn		r9
rpBuf			rn		r8
rBuf_Size		rn		r7

; MACROs used to load the internal registers
		macro
		load_rRCC
		ldr		rRCC, =RCC_BaseAddr
		mend

		macro
		load_rGPIO
		ldr		rGPIO, =GPIO_PORT
		mend
		
		macro
		load_rUART
		ldr		rUART, =USART_PORT
		mend
		
		macro
		load_rNVIC
		ldr		rNVIC, =NVIC_BaseAddr
		mend
		
		macro
		load_rpBuf
		ldr		rpBuf, =pBuf_BaseAddr
		mov		rBuf_Size, #buffer_size
		mend

这里有一个很容易昏头的问题。就是这些宏不能只在初始化的时候执行一次就可以的。我面对的是寄存器,不是C语言中的全局变量。所以每次进入函数的时候都可能是被改了的。

4.2.4 初始化函数void init(void)

其实这个函数格式只是给C函数看的。汇编里面只要有个名字就行了,其他的都是浮云。

这里注意,

  1. 所有的函数在创建的时候,最好都来个align 4,以保证读取地址的正常。有人说,thumb不是16位的么。其实那都是老Thumb的。现在的是Thumb-II,有很多32位的指令的。对齐一下自然是极好的。
  2. 所有的函数上来就来一句将所有的callee寄存器都压栈。很省事,如果你和我一样是个懒人,那就这样干,一了百了。
area USB_UART_CODE_SECTION, code
			
			align 4
init		proc
			push	callee_regs
		; 	Enable the clocks for GPIOA and USART1
			
			load_rRCC
			ldr		r0, [rRCC, #GPIO_PORT_CLOCK_BUSEN]
			orr 	r0, #GPIO_PORT_CLOCK_BIT
			str		r0, [rRCC, #GPIO_PORT_CLOCK_BUSEN]
			ldr 	r0, [rRCC, #USART_CLOCK_BUSEN]
			orr 	r0, #USART_CLOCK_BIT
			str 	r0, [rRCC, #USART_CLOCK_BUSEN]
			
		; Configure the pins

			load_rGPIO
			ldr 	r0, [rGPIO, #GPIO_MODER]
			orr		r0,	#TX_PIN_MODER_VAL :or: RX_PIN_MODER_VAL
			str 	r0, [rGPIO, #GPIO_MODER]
			ldr		r0, [rGPIO, #GPIO_PORT_AFIO]
			orr		r0, #RX_PIN_AFIO_VAL:OR:TX_PIN_AFIO_VAL
			str		r0, [rGPIO, #GPIO_PORT_AFIO]			
		
		; Enable the receive interrupt by set the CR_RXNEIE bit		
			load_rUART
			ldr		r0, [rUART, #USART_CR1]
			orr		r0, #USART_CR1_UE 
		; Enable the interrupt of USART1
			orr		r0, #USART_CR1_RXNEIE
			orr		r0, #USART_CR1_RE
			str		r0, [rUART, #USART_CR1]
			mov		r0, #USART_BSS_VAL
			str 	r0, [rUART, #USART_BRR]
		; Set the NVIC bits.
			load_rNVIC
			ldr		r1, [rNVIC, #NVIC_IPR_USART1_OFFSET]
			orr		r1, #NVIC_IPR_USART_PRIO_VAL
			str		r1, [rNVIC, #NVIC_IPR_USART1_OFFSET]			
			ldr		r1, =NVIC_USART_BIT
			str		r1, [rNVIC, #NVIC_ISER_USART1_OFFSET];
			
			pop 	callee_regs
			bx 		lr
			endp

可以看出,所有在C下面尤其是还用标准库、HAL库什么各种各样的库来的,在汇编这里其实都很简单的。使用了汇编器的Directives以后,很多汇编指令也变得可读了,而且,貌似就只是数指令数,也不见得比C要多。

4.2.5 void send_ch(uint8_t)函数的实现

这个汇编下面的macro…mend是个很强的存在。macro … mend之间是宏定义。我这里就直接指定,这个宏里面的语句必须使用r1,提醒我如果要用的话,必须保证r1可以用。r1本来就是caller寄存器,也应该由调用方保护。这个东西就跟C语言中的inline或者宏函数差不多。用汇编宏实现轮询SR_TC。

其次,对于ARM-CM4来说,第一个参数的值就是进入函数以后r0的值。

			macro
$label		wait_for_sr_tc_macro_via_r1	
$label.test_sr_tc_m
			ldr		r1, [rUART, #USART_SR]
			tst		r1, #USART_SR_TC
			beq		$label.test_sr_tc_m			
			mend
				
			align 	4
send_ch		proc
			push	callee_regs
			
			load_rUART
			ldr		r2, [rUART, #USART_CR1]
			orr		r2, #USART_CR1_TE
			str 	r2,	[rUART, #USART_CR1]
before_send		wait_for_sr_tc_macro_via_r1	; 	It is important to wait for the tc to be set.
											; 	By checking this bit via DEBUG window is not possible
											;	to find if this bit is set or cleared.
			str		r0, [rUART, #USART_DR]
after_sent		wait_for_sr_tc_macro_via_r1	; 	This is the same as the previous waiting.
			bic		r2, #USART_CR1_TE
			str 	r2,	[rUART, #USART_CR1]				
			pop		callee_regs
			bx 		lr
			endp

void send_ch(uint8_t)里面核心就是把数据放进DR寄存器,但是这里要注意,放进去之前和之后要轮询一下TC标志位,这样就能保证发送数据时这个UART口是正常的。这个操作是很有必要的。因为如果靠调试器查的话,往往是感觉TXE和TC永远是同步的在置位和清零。

4.2.5 uint8_t read_ch(void)函数的实现

这个函数实现函数从缓冲区里读数据。如果是没有数据就在里面轮询着。
返回值就是r0的值。

对缓冲区的操作是

Yes
No
Start
Whether the buffer is empty?
nData > 0
Read the data out.
Update the pRead
End

以下的代码实现上述的图。这里有一个不方便的地方,就是Thumb-II指令集里没有直接取模的指令,所以只能先做udivsdiv整除,再用mls这样的指令进行计算余数。要是RISC-V就好办多了。一条语句就给你把商和余数都获得了。

align 	4
read_ch		function
			push 	callee_regs
			load_rpBuf					
wait_for_data
			ldr		r1, [rpBuf, #nData]
			cbnz	r1, data_received
			b		wait_for_data
data_received
			ldr		r2, [rpBuf, #pRead] ; Get the value of pRead to r2.
			ldrb	r0, [rpBuf, r2]		; Take the data from pBuf_BaseAddr + [pRead] to r0.
			
			sub		r1, #1
			str		r1, [rpBuf, #nData]	; Update the value of nData. Then r1 is free.
			
			add		r2, #1				; Update the pRead
			udiv	r1, r2, rBuf_Size	; Perform a MOD operator: pRead = Pread % buffer_size
			mls		r2, r1, rBuf_Size, r2
			str		r2, [rpBuf, #pRead]
			
			pop		callee_regs
			bx		lr
			endfunc	
4.2.5 引出结构体usb_uart

把函数们做好了以后,定义一个数据块,并引出去就可以了。还是,要注意4字节地址对齐。

align 	4
usb_uart	dcd init, send_ch, read_ch
			export usb_uart

这样在工程中任何地方只要有C语言的地方使用extern,在汇编的地方使用externimport,如果是gnu汇编的话.global就可以使用usb_uart这个结构体了。

4.2.6 ISR void USART1_IRQHandler(void)

当然,不要忘了还有这个ISR。做这个ISR的时候要注意:

  1. 进入ISR的时候,是以特权模式进来的。所以很多特权指令可以直接使用。
  2. 这个函数实现了以后,要引出。否则不能覆盖系统弱定义的那个函数。
  3. 进入ISR的时候会发现返回地址lr的值是0xFFFF FFFD。这个是退出ISR的方法。参考《ARMv7-M Architecture Reference Manual》的B1-539和B1-540两页。

这个函数的流程是

Yes
No
Start
Disable the interrupt.
Whether the buffer is full?
nData > 0
Delete the oldest data and forward the pRead.
Increase nData by 1.
Forward pRecv and write the buffer.
Enable the Interrupt.
End

下面的代码实现这个流程图的功能。

;
;	USART1_IRQHandler	
;	Disable the Interrupt --> Clear the pending -->
;	do something --> Enable the Interrupt.
;	
			align	4
USART1_IRQHandler	proc
			export 	USART1_IRQHandler	
			push	callee_regs
			load_rNVIC
			load_rpBuf
			ldr		r4, =NVIC_USART_BIT	; 	r4 is the NVIC_USART_BIT			
			str		r4, [rNVIC, #NVIC_ICER_USART1_OFFSET]
			str		r4, [rNVIC, #NVIC_ICPR_USART1_OFFSET]
			
			; Insert the data to the buffer FIFO	
			ldr		r0, [rpBuf, #nData]		;	r0 holds the value of nData
			ldr		r1, [rpBuf, #pRead]		; 	r1 holds the value of pRead			
			cmp		r0, rBuf_Size			;	if the buffer is full, 
			ittte	eq			
			addeq	r1, #1					;	Increase the address of pRead by 1 circularly
			udiveq	r2, r1, rBuf_Size		;	Do a MOD operand.
			mlseq	r1, r2, rBuf_Size, r1	;	otherwise just add the nData by 1
			addne	r0, #1;			
			streq	r1, [rpBuf, #pRead]		;	Update the pRead if the buffer is full,
			strne	r0, [rpBuf, #nData]		; 	otherwise update the value of nData.
			
			; Read out the data from the USART1 RDR, which is Receive data register.
			; Store the data to the buffer.
			load_rUART						; 	High registers!! Haha!
			ldr		r0, [rpBuf, #pRecv]
			ldrb	r1, [rUART, #USART_DR]
			strb	r1, [rpBuf,r0]			; 	Store the data from USART_DR to  
											;	pBuf_BaseAddr + [pRecv]
			
			; Update the pRecv
			add		r0, #1					;	Increase the pRecv by 1.
			udiv	r1, r0, rBuf_Size		;	Perform a MOD operand.
			mls		r0, r1, rBuf_Size, r0	;
			str		r0, [rpBuf, #pRecv]		; 	Write back the pRecv.
											; 	Re-enable the interrupt.
			str		r4, [rNVIC, #NVIC_ISER_USART1_OFFSET] 	
			
			pop		callee_regs
			bx 		lr
			endp
		
		end		

关于这里面的程序设计,有3点可以说的

  1. 这个函数里面可以使用IT块对小规模的条件运算和条件执行进行规划。
  2. str的条件执行不需要在IT块里。还有其它的不确定的是否需要IT块执行的条件执行指令,参考《ARM Architecture Reference Manual Thumb-2 Supplement》.
  3. 就算是在ISR中,该保护一下寄存器还是要保护一下的。

第二步,Support的构建

这样,串口的驱动就有了。因为我只是为了支持STDIO的,所以不需要更多的函数,例如大规模数据读写什么的。暂时先不考虑DMA这些东西。

右键Support,add new items,user code template, STDOUT via USART。
右键Support,add new items,user code template, STDIN via USART。

把wizard里面的变量配置一下。其实我用不到,但是看着数值对不上让人不爽。

里面其他没用的删掉,就这样干。

/*-----------------------------------------------------------------------------
 * Name:    stdin_USART.c
 * Purpose: STDIN USART Template
 * Rev.:    1.0.0
 *-----------------------------------------------------------------------------*/
 
#include "stdin_USART.h"
#include "usb_uart.h"
#include "cmsis_os2.h"                  // ::CMSIS:RTOS2


//-------- <<< Use Configuration Wizard in Context Menu >>> --------------------
 
#define USART_DRV_NUM           1
 
//   <o>Baudrate
#define USART_BAUDRATE          115200

#define _USART_Driver_(n)  Driver_USART##n
#define  USART_Driver_(n) _USART_Driver_(n)
  
/**
  Initialize stdin
 
  \return          0 on success, or -1 on error.
*/
int stdin_init (void) { 
  return (0);
}
 
 
/**
  Get a character from stdin
 
  \return     The next character from the input, or -1 on read error.
*/
int stdin_getchar (void) {
  static uint8_t buf;
	buf = usb_uart.read_ch();
//  osDelay(1);
  return buf;
}

/*-----------------------------------------------------------------------------
 * Name:    stdout_USART.c
 * Purpose: STDOUT USART Template
 * Rev.:    1.0.0
 *-----------------------------------------------------------------------------*/
#include "usb_uart.h"
#include "stdout_USART.h"
 
//-------- <<< Use Configuration Wizard in Context Menu >>> --------------------
 
// <h>STDOUT USART Interface
 
//   <o>Connect to hardware via Driver_USART# <0-255>
//   <i>Select driver control block for USART interface
#define USART_DRV_NUM           1
 
//   <o>Baudrate
#define USART_BAUDRATE          115200
 
 
#define _USART_Driver_(n)  Driver_USART##n
#define  USART_Driver_(n) _USART_Driver_(n)
 


 
/**
  Initialize stdout
 
  \return          0 on success, or -1 on error.
*/
int stdout_init (void) {
  

  return (0);
}
 
 
/**
  Put a character to the stdout
 
  \param[in]   ch  Character to output
  \return          The character written, or -1 on write error.
*/
int stdout_putchar (int ch) {
  usb_uart.send_ch((uint8_t)ch);
  return (ch);
}

第三步 测试

去主函数里做个线程测试一下就好。

/*----------------------------------------------------------------------------
 * CMSIS-RTOS 'main' function template
 *---------------------------------------------------------------------------*/
 
#include "RTE_Components.h"
#include  CMSIS_device_header
#include "cmsis_os2.h"

#include "bsp.h"
#include "support.h"
#include "stdio.h"
/*----------------------------------------------------------------------------
 * Application main thread
 *---------------------------------------------------------------------------*/
__NO_RETURN static void app_main (void *argument) {
  int val[20];
	(void)argument;
  // ...
  for (;;) {
		scanf("%d", val);
		printf("%d\r\n",val[0]);
		osDelay(3);
	}
}
 
int main (void) {
 extern void test_case(void);
  // System Initialization
	pll_config();
  SystemCoreClockUpdate();
  
	usb_uart.init();
  stdout_init();
	stdin_init();
	test_case();
  osKernelInitialize();                 // Initialize CMSIS-RTOS
  osThreadNew(app_main, NULL, NULL);    // Create application main thread
  osKernelStart();                      // Start thread execution
  for (;;) {}
}

第四步,编译、链接、下载、复位、运行,Ура!

四、调试与验证

打开串口调试助手,设置好串口号和波特率。发送几个数据看看能不能回应我。
在这里插入图片描述
当然,可以修改测试线程看看其他的效果。这里我就不演示了。

五、体会

我认为下位机的软件设计中,汇编还是有很多可以发挥的地方。我指的是直接使用汇编实现程序设计,而不是搞什么性能优化或特殊指令使用。没有必要在所有的地方都用汇编,在很多的末端函数,被C语言封装之内的很多函数,用汇编做往往有奇效。以下的优点在调试和程序设计中就很有体会。

  1. 很多程序设计其实用汇编没有以前人们说的那么难。
  2. 不用担心任何中断或异常,当然就包括RTOS调度打断你现在正在执行的那句语句。
  3. 所见即所得。每一句都是原子的。不存在C的一句里面不知道要走多远,影响了什么的问题。
  4. 写起来没那么慢。看起来代码量也不少,但是用C也得表达出同样的含义,也得不少语句。熟练了汇编写起来其实挺快的。
  5. 进入函数就有13个32位的通用寄存器任你使用,函数内的甚至都不用使用SP指针定义新的变量。
  6. 跟弱类型语言,或者说高级语言一样。没有严格的变量数据类型。你说是啥就是啥。

六、参考文件

  1. ARM Developer Suite Assembler Guide
  2. Arm®v7-M Architecture Reference Manual
  3. ARM Architecture Reference Manual Thumb-2 Supplement
  4. RM0090 Reference manual STM32F405/415, STM32F407/417, STM32F427/437 and STM32F429/439 advanced Arm®-based 32-bit MCUs
  5. STM32F405xx STM32F407xx Datasheet
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值