KEIL 5.38的ARM-CM3/4 ARM汇编设计学习笔记3——串口Stdio实现
一、介绍
这个测试主要是在ARM核里运行。所以如果使用正点的板子或者其他的407的板子,都可以做出来。做了好几天出来。也是为了自己练习编程,也是为了留个笔记,以后用的时候可以找到。
Всё, давай начнём.
任务目标
1、 实现USB-UART的串口驱动。
2、支持STDIO,并通过串口调试工具进行交互
发送比较简单,就是有发送的需求的时候就用usart->dr直接发。但是接收数据的话,这个UART没有CTS和DTS等其他信号的辅助,就做一个缓冲区,只要有数据来了就用中断ISR将数据送到缓冲区。读的时候就把数据从缓冲区里读出去。缓冲区的操作是FIFO。
试验环境
环境 | 描述 |
---|---|
MCU | STM32F407VGT6 |
IDE | KEIL |
串口 | USART1, Baud rate: 115200Hz |
Pin | TX: 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三个文件夹。
第四步:在魔法棒下:
- Target标签选中IRAM2(其实没有真的用,只是勾上,任性。)
- C/C++(AC6)标签,将Language C调成c11;在Include Path里添加第三步里建的3个文件夹。
- Asm标签,将Assembler Option的汇编器选中除了GUN Syntax的其他选项;在Include Path里添加第三步里建的3个文件夹。
- Debug标签,选中CMSIS-DAP,并点一下Setting并确认。
第五步:点击“品”那个按钮,把原来的Group删了,创建3个Group,名字和我们创建的3个文件夹一致。
这样,我们的工程配置就完成了。
三、软件设计
第一步,BSP构建
在这个试验中,我在BSP里面需要构建Application、BSP和Support这3个包。这里我就用mermaid简单画画,我就不去开enterprise architecture了。
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里面的宏,就可以完成设置。设置包括三方面:
- 时钟启动,启动gpio和要用的USART。
- Pin脚设置,设置TX和RX的pin脚,主要是MODER和AFIO。有关AFIO的值要去查Datasheet。
- 中断设置,主要设置优先级。由于usart的中断是由NVIC直接控制。所以不需要对SYSCFG进行操作。
- 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
完成以上的设置以后,后面有关的设置利用汇编器进行计算。这里面有几点注意:
equ
、space
、rn
都前面不能有空格。IF...ELSE...ENDIF
、END
、AREA
、MACRO
、MEND
等前面必须有空格。- 汇编助记符、ARM寄存器名称和汇编器Directives不区分大小写,但是必须有一致性。比如可以写
equ
和EQU
,但是不能写eQu
。 - ‘\’可以实现分行
- 本文中的所有的宏定义、重命名只在本文中生效。用
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
- 用
space
划出一块内存给本文件里的函数。这个类似C语言的文内静态全局变量。 - 将段定义到rw段即可,则数据地址就会被定义到片上SRAM。但是如果非要定义到CCM上,需要进行链接脚本操作。
- 缓冲区里的数据都是字节,也只进行字节访问,所以不需要进行地址对齐。
- 后面的几个变量考虑用32位数据,所以必须进行地址对齐。
- 用
align 4
实现4字节地址对齐。
4.2.3 文内寄存器的重定义指定
经过分析,我们发现,其实本文中所有的函数中会常用的外设寄存器和其他内存地址有:
- RCC
- GPIO
- UART
- pBuf
- 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函数看的。汇编里面只要有个名字就行了,其他的都是浮云。
这里注意,
- 所有的函数在创建的时候,最好都来个
align 4
,以保证读取地址的正常。有人说,thumb不是16位的么。其实那都是老Thumb的。现在的是Thumb-II,有很多32位的指令的。对齐一下自然是极好的。 - 所有的函数上来就来一句将所有的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的值。
对缓冲区的操作是
以下的代码实现上述的图。这里有一个不方便的地方,就是Thumb-II指令集里没有直接取模的指令,所以只能先做udiv
或sdiv
整除,再用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
,在汇编的地方使用extern
或import
,如果是gnu汇编的话.global
就可以使用usb_uart这个结构体了。
4.2.6 ISR void USART1_IRQHandler(void)
当然,不要忘了还有这个ISR。做这个ISR的时候要注意:
- 进入ISR的时候,是以特权模式进来的。所以很多特权指令可以直接使用。
- 这个函数实现了以后,要引出。否则不能覆盖系统弱定义的那个函数。
- 进入ISR的时候会发现返回地址
lr
的值是0xFFFF FFFD
。这个是退出ISR的方法。参考《ARMv7-M Architecture Reference Manual》的B1-539和B1-540两页。
这个函数的流程是
下面的代码实现这个流程图的功能。
;
; 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点可以说的
- 这个函数里面可以使用IT块对小规模的条件运算和条件执行进行规划。
str
的条件执行不需要在IT块里。还有其它的不确定的是否需要IT块执行的条件执行指令,参考《ARM Architecture Reference Manual Thumb-2 Supplement》.- 就算是在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语言封装之内的很多函数,用汇编做往往有奇效。以下的优点在调试和程序设计中就很有体会。
- 很多程序设计其实用汇编没有以前人们说的那么难。
- 不用担心任何中断或异常,当然就包括RTOS调度打断你现在正在执行的那句语句。
- 所见即所得。每一句都是原子的。不存在C的一句里面不知道要走多远,影响了什么的问题。
- 写起来没那么慢。看起来代码量也不少,但是用C也得表达出同样的含义,也得不少语句。熟练了汇编写起来其实挺快的。
- 进入函数就有13个32位的通用寄存器任你使用,函数内的甚至都不用使用SP指针定义新的变量。
- 跟弱类型语言,或者说高级语言一样。没有严格的变量数据类型。你说是啥就是啥。