ARM接口技术

ARM接口技术

FS4412开发环境搭建

交叉编译工具链搭建:

①:将交叉编译工具链安装到具体目录下:新建了一个路径,这里是/home/linux_4412/toolchain/,然后将资源文件gcc-4.6.4.tar.xz移动到此路径下,解压到当前目录(tar xvf gcc-4.6.4.tar.xz),解压后的目录为==gcc-4.6.4==

img

②:切换至该路径下的bin目录下。ls看一下:

img

画红框的就是我们所用的编译工具arm-gcc

③:为了使用该编译工具不每一次都切换到~/linux_4412/toolchain/gcc-4.6.4/bin绝对路径下,我们需要做一下全局环境配置:

使用vi .banshrc打开全局环境配置文件,在最后一行输入以下语句(在原有的PATH全局变量下追加新路径)

img

④:上一步保存退出,使用source .bashrc使配置生效

⑤:使用 arm-none-linux-gnueabi-gcc -v命令

img

如出现上述界面,说明已安装成功

使用file + 文件名可查看文件信息

img

img

img

不同架构下的处理器,机器码无法移植,试图在x86处理器下运行arm-gcc编译后生成的可执行文件,会显示格式错误

loadb 0x40008000

将选中的程序下载到对应的地址

go 0x40008000

运行0x40008000为起始地址的地址

作业:

什么是交叉编译?为何要有交叉编译-电子发烧友网 (elecfans.com)

地址映射表

ARM体系架构中提到的6大类指令,除了load/store指令之外,均不会操作到cpu之外的东西。

每一个硬件控制器里面都会有一些寄存器,CPU可以往这些寄存器里写入或读取数据(STR、LDR指令),完成对硬件控制器的操控,进而控制硬件(硬件是由硬件控制器直接控制的)

img

硬件控制原理 :

CPU本身是不能直接控制硬件的,硬件一般是由其对应的控制器来控制, SOC中将各个硬件控制器的寄存器映射到了CPU地址空间中的一段范围,这样CPU就可以通过读写寄存器来间接控制硬件

注:这里的寄存器在SOC中但在CPU之外,有地址,访问方式与内存一样,常用于控制硬件

再比如:STR,R1,[R2],与LDR,R3,[R4],对于32位处理器来说,寄存器都是32位的,所存放的地址是32位的,也就是说R2里面存放的地址范围是4G大小,即地址空间为4G大小,看下面的图:

img

CPU的4G内存划分,RAM、ROM?

img

.

这样看来地址映射表中除了内存,还包含RECV保留空间以及硬件控制器寄存器地址映射空间(SFR特殊功能寄存器),这些共同组成了4G寻址空间

即:在一个处理器中,一般会将Flash、RAM、寄存器等存储设备分别映射到寻址空间中的不同地址段,我们将这个映射关系成为这个处理器的地址映射表

RAM、ROM、硬盘及内存 - 知乎 (zhihu.com)

==此外注意:==

芯片上电后PC被自动置为0,也就是自动开始执行iROM里的程序,执行完才会执行我们自己写的程序


GPIO实验

GPIO简介:

GPIO(General-purpose input/output)

即通用型输入输出,GPIO可以控制连接在其之上的引脚实现信号的输入和输出

芯片的引脚与外部设备相连,从而实现与外部硬件设备的通讯、控制及信号采集等功能

img

实验步骤 :

\1. 通过电路原理图分析LED的控制逻辑

\2. 通过电路原理图查找LED与Exynos4412的连接关系

\3. 通过数据手册分析GPIO中哪些寄存器可以控制LED

\4. 通过程序去操控对应的寄存器完成对LED的控制

1.LED控制逻辑:

img

分析LED2一端(阳极)与DC33V(即直流3.3V)相连,另一端(阴极)直接连接到三极管的集电极,发射极接地,基级与一端接网络标号为CHG_COK导线相连

基级导线上的电信号为高电平时,三极管导通,LED2的阴极接地,阳极接正极,LED2亮

---------------------为低电平时,三极管断开,LED2的阴极浮空,阳极接正极,LED2灭

2.LED2与FS4412连接关系:

img

根据网络标号CHG_COK进行查找,可以发现,LED2的另外一端其实连接到FS4412芯片的GPX2_7引脚上

(芯片内部GPIO硬件控制器与GPX2_7相连,GPX2_7可以输出高低电平控制LED2的亮灭)

3.分析数据手册中的与LED2相关的寄存器

img

GPX0,GPX1,GPX2,GPX3分别代表一组引脚,共32个引脚,每组8个引脚,这里从芯片的电气原理图也能看出

我们直接找与GPX2引脚相关的寄存器:

Base Address: 0x1100_0000**(GPIO)**

img

img

上面的基地址就是GPIO在地址映射表中的起始地址,而上面四个寄存器的绝对地址=基地址+offset

下图为每一个寄存器具体在地址映射表的位置

img

再看具体的寄存器的功能:

①:GPX2CON寄存器:

img

也就是说对于寄存器GPX2CON,配置最高的4位(31~28),可以将引脚7配置为不同的模式

对于其他的引脚,可以配置32位中的其他位,比如3~0位,可以配置引脚0

②:GPX2DAT寄存器

img

每四位控制一个引脚

引脚的状态和配置的位相同,即第0位到第7位分别控制0~7这八个引脚

注意:GPX2DAT也是32位寄存器,只不过是其余的位没有使用,所以只写了0~7位

4.程序实现LED2的控制:

实验一:控制LED2熄灭(开发板上电默认点亮)

led-asm.s:

img

LED_CONFIG子程序实现将GPX2_7配置为输出模式

LED_OFF子程序实现将所有的GPX2引脚输出低电平(此开发板一上电默认点亮LED2)

编写Makefile:

img

.elf只能放到linux上运行,不能直接放到开发板上跑,那么怎么观察实验结果呢?

要将.elf文件使用交叉编译工具arm-none-linux-gnueabi-objcopy转换为.bin二进制文件,然后将这个.bin文件拷贝至共享文件夹下,在使用SecureCRT软件下载,观察实验现象

make:

img

将led-asm.bin文件拷贝至共享文件夹下(Share):cpled-asm.bin /mnt/hgfs/Share/

下载烧录文件执行:

①:给开发板上电,serial-com3窗口出现提示信息方可,有时没有出现提示信息,请检查:

串口线是否松动、SD卡是否松动、与电脑相连接的usb数据线是否安插牢固,如果还不行,请将拨码开关拨成EMMC模式

倒计时前按下回车键

②:使用loadb 0x40008000,准备将文件下载至0x40008000地址(该地址处于1G扩展内存0x40000000~0x80000000之内)

img

③:使用tranfer菜单栏命令,选择共享文件夹下的.bin文件,点击ok烧录

④:go 0x40008000运行改地址段内的程序

可观察到LED2被熄灭

实验二:控制LED2闪烁

led-asm.s:
​
.text
​
_start:
​
MAIN:
​
•    BL LED_CONFIG
​
LOOP:
​
•    BL LED_ON
​
•    BL DELAY
​
•    BL LED_OFF
​
•    BL DELAY
​
•    B LOOP
​
​
​
LED_CONFIG:
​
•    LDR R2,=0x11000c40
​
•    LDR R1,=0x10000000
​
•    STR R1,[R2]
​
•    MOV PC,LR
​
​
LED_ON:
​
•    LDR R2,=0x11000c44
​
•    LDR R1,=0x00000080
​
•    STR R1,[R2]
​
•    MOV PC,LR
​
​
LED_OFF:
​
•    LDR R2,=0x11000c44
​
•    LDR R1,=0x00000000
​
•    STR R1,[R2]
​
•    MOV PC,LR
​
​
DELAY:
​
•    LDR R1,=1000000
​
L:
​
•    SUB R1,R1,#1
​
•    CMP R1,#0
​
•    BNE L
​
•    MOV PC,LR
​
​
STOP:
​
•    B STOP
​
​
​
.end

烧录方式与实验一相同

可观察到led2亮度比刚上电时暗了一些,但是”好像“并没有闪烁,这是因为灯闪烁太快了,我们人眼无法分辨,下面进行改进:

根据SecureCRT:系统频率为1000MHZ(即每秒钟进行10亿次,理想情况下每秒可执行10亿条指令,一个时钟周期执行一条指令)

img

因此可以大致推测,下列代码中标红部分代码块执行一亿次,要花费大概零点几秒,即延时时间大概也就是零点几秒左右

DELAY:
​
•    LDR R1,=1000000
​
L:
​
•    SUB R1,R1,#1
​
•    CMP R1,#0
​
•    BNE L
​
•    MOV PC,LR

所以先将LDR ,R1,=10000000改为LDR,R1,=100000000,即一亿

可观察到LED2每秒钟闪烁2到3次

实验三:自己写的流水灯:

有问题,第四个灯不亮

.text2:
​
_start:
​
MAIN:
​
•    BL LED_CONFIG
​
LOOP:
​
•    BL LED2_ON
​
•    BL DELAY
​
•    BL LED2_OFF
​
​
•    BL LED3_ON
​
•    BL DELAY
​
•    BL LED3_OFF
​
​
•    BL LED4_ON
​
•    BL DELAY
​
•    BL LED4_OFF
​
​
•    BL LED5_ON
​
•    BL DELAY
​
•    BL LED5_OFF
​
•    B LOOP
​
​
LED_CONFIG:
​
•    @LED2
​
•    LDR R2,=0x10000c40
​
•    LDR R1,=0x10000000
​
•    STR R1,[R2]
​
•    
​
•    @LED3
​
•    LDR R2,=0x11000c20
​
•    LDR R1,=0x00000001
​
•    STR R1,[R2]
​
•    @LED4
​
•    LDR R2,=0x114001e0
​
•    LDR R1,=0x00010000
​
•    STR R1,[R2]
​
​
•    @LED5
​
•    LDR R2,=0x114001e0
​
•    LDR R1,=0x00100000
​
•    STR R1,[R2]
​
•    MOV PC,LR
​
​
LED2_ON:
​
•    LDR R2,=0x11000c44
​
•    LDR R1,=0x00000080
​
•    STR R1,[R2]
​
•    MOV PC,LR
​
​
LED2_OFF:
​
•    LDR R2,=0x11000c44
​
•    LDR R1,=0x00000000
​
•    STR R1,[R2]
​
•    MOV PC,LR
​
​
LED3_ON:
​
•    LDR R2,=0x11000c24
​
•    LDR R1,=0x00000001
​
•    STR R1,[R2]
​
•    MOV PC,LR
​
​
LED3_OFF:
​
•    LDR R2,=0x11000c24
​
•    LDR R1,=0x00000000
​
•    STR R1,[R2]
​
•    MOV PC,LR
​
​
LED4_ON:
​
•    LDR R2,=0x114001e4
​
•    LDR R1,=0x00000010
​
•    STR R1,[R2]
​
•    MOV PC,LR
​
​
LED4_OFF:
​
•    LDR R2,=0x114001e4
​
•    LDR R1,=0x00000000
​
•    STR R1,[R2]
​
•    MOV PC,LR
​
​
LED5_ON:
​
•    LDR R2,=0x114001e4
​
•    LDR R1,=0x00000020
​
•    STR R1,[R2]
​
•    MOV PC,LR
​
​
LED5_OFF:
​
•    LDR R2,=0x114001e4
​
•    LDR R1,=0x00000000
​
•    STR R1,[R2]
​
•    MOV PC,LR
​
​
DELAY:
​
•    LDR R1,=1000000000
​
L:
​
•    SUB R1,R1,#1
​
•    CMP R1,#0
​
•    BNE L
​
•    MOV PC,LR
​
​
STOP:
​
•    B STOP
​
​
.end

C工程与寄存器封装

模板:

Makefile:

#=============================================================================#  
​
NAME = interface     
​
CROSS_COMPILE = arm-none-linux-gnueabi-
​
\#=============================================================================#
​
CC = $(CROSS_COMPILE)gcc
​
LD = $(CROSS_COMPILE)ld
​
OBJDUMP = $(CROSS_COMPILE)objdump   ------------------------------------- //反汇编
​
OBJCOPY = $(CROSS_COMPILE)objcopy   -------------------------------------//可以将.elf文件转换为二进制文件
​
CFLAGS  += -g -O0 -mabi=apcs-gnu -mfpu=neon -mfloat-abi=softfp -fno-builtin \
​
•           -nostdinc -I ./common/include                                                     
​
\#============================================================================#
​
OBJSss  := $(wildcard start/*.S) $(wildcard common/src/*.S) $(wildcard *.S) \    //这一部分是对目录的展开,因为不同文件可能会放到不同的目录下,便于寻找
​
•           $(wildcard start/*.c) $(wildcard common/src/*.c)                 \
​
•           $(wildcard usr/*.c) $(wildcard *.c)
​
OBJSs      := $(patsubst %.S,%.o,$(OBJSss))
​
OBJS     := $(patsubst %.c,%.o,$(OBJSs))
​
\#============================================================================#
​
%.o: %.S
​
•    $(CC) $(CFLAGS) -c -o $@ $<-------------------------------------------//.s文件到.o文件
​
%.o: %.c
​
•    $(CC) $(CFLAGS) -c -o $@ $<-------------------------------------------//.c文件到.o文件
​
all:clean $(OBJS)
​
•    $(LD) $(OBJS) -T map.lds -o $(NAME).elf-------------------------------//链接所有的.o生成.elf文件
​
•    $(OBJCOPY) -O binary  $(NAME).elf $(NAME).bin-------------------------//将.elf文件生成二进制文件
​
•    $(OBJDUMP) -D $(NAME).elf > $(NAME).dis-------------------------------//将.elf文件反汇编
​
\#============================================================================#
​
clean:
​
•    rm -rf $(OBJS) *.elf *.bin *.dis *.o
​
\#============================================================================#

map.lds:

(设置链接的排版与格式)

OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
​
/*OUTPUT_FORMAT("elf32-arm", "elf32-arm", "elf32-arm")*/
​
OUTPUT_ARCH(arm)
​
ENTRY(_start)
​
SECTIONS
​
{
​
•    . = 0x40008000;
​
•    . = ALIGN(4);
​
•    .text      :
​
•    {
​
•        start/start.o(.text)
​
•        *(.text)
​
•    }
​
•    . = ALIGN(4);
​
•    .rodata :
​
•    { *(.rodata) }
​
•    . = ALIGN(4);
​
•    .data :
​
•    { *(.data) }
​
•    . = ALIGN(4);
​
•    .bss :
​
•    { *(.bss) }
​
}

start.S:(启动代码详解)

.text
​
.global _start
​
_start: 
​
•    /*
​
•     \* Vector table
​
•     */
​
•    b reset     (填充跳转指令,占据异常向量表的32字节空间)
​
•    b .
​
•    b .
​
•    b .
​
•    b .
​
•    b .
​
•    b .
​
•    b .
​
​
reset:
​
•    /*
​
•     \* Set vector address in CP15 VBAR register        设置异常向量表的基地址
​
•     */
​
•    ldr    r0, =_start                                 将start地址赋给r0寄存器
​
•    mcr    p15, 0, r0, c12, c0, 0    @Set VBAR         将cp15协处理器中的c12寄存器的值修改为r0寄存器数据
​
​
//如果不设置异常向量表的基地址,ARM会默认异常向量表的基地址为0x0,设置之后基地址变为我们修改之后的,以后出现异常源,系统会自动设置PC为该基地址+偏移量,即前面绿色代码b.部分对应的位置
​
​
​
•    /*
​
•     \* Set the cpu to SVC32 mode, Disable FIQ/IRQ      设置为SVC模式,同时关掉FIQ/IRQ中断(因为启动核心代码时需要高特权,并且希望不被中断打断)
​
•     */  
​
•    mrs r0, cpsr          -----------------------------将cpsr寄存器中的数据读取到r0寄存器
​
•    bic r0, r0, #0x1f     -----------------------------位清零,低五位清零再赋给r0寄存器
​
•    orr    r0, r0, #0xd3  -----------------------------将r0寄存器与0xd3按位或运算
​
•    msr    cpsr ,r0        ----------------------------将r0寄存器的值再赋给cpsr寄存器,此时cpsr寄存器的值变为0xd3,即SVC模式,此时FIQ/IRQ也被禁止
​
​
•    /*
​
•     \* Defines access permissions for each coprocessor
​
•     */  
​
•    mov    r0, #0xfffffff
​
•    mcr    p15, 0, r0, c1, c0, 2      
​
​
•    /*
​
•     \* Invalidate L1 I/D                                                                                                                  
•     */
​
•    mov    r0, #0                    @Set up for MCR
​
•    mcr    p15, 0, r0, c8, c7, 0    @Invalidate TLBs  -----使页表失效
​
•    mcr    p15, 0, r0, c7, c5, 0    @Invalidate icache
​
•    
​
•    /*
​
•     \* Set the FPEXC EN bit to enable the FPU--------------使能浮点型运算单元
​
•     */
​
•    mov r3, #0x40000000
​
•    fmxr FPEXC, r3
​
•    
​
•    /*
​
•     \* Disable MMU stuff and caches------------------------使MMU失效      MMU:物理地址与虚拟地址的转换
​
•     */
​
•    mrc    p15, 0, r0, c1, c0, 0
​
•    bic    r0, r0, #0x00002000        @Clear bits 13 (--V-)
​
•    bic    r0, r0, #0x00000007        @Clear bits 2:0 (-CAM)
​
•    orr    r0, r0, #0x00001000        @Set bit 12 (---I) Icache
​
•    orr    r0, r0, #0x00000002        @Set bit 1 (--A-) Align
​
•    orr    r0, r0, #0x00000800        @Set bit 11 (Z---) BTB
​
•    mcr    p15, 0, r0, c1, c0, 0
​
​
•    /*
​
•     \* Initialize stacks                                                                                                                
•     */
​
init_stack:     
​
•    /*svc mode stack*/
​
•    msr cpsr, #0xd3----------------------------------设置为svc模式
​
•    ldr sp, _stack_svc_end---------------------------初始化sp栈指针,指向栈的最高地址
​
​
•    /*undef mode stack*/
​
•    msr cpsr, #0xdb
​
•    ldr sp, _stack_und_end
​
​
•    /*abort mode stack*/    
​
•    msr cpsr,#0xd7
​
•    ldr sp,_stack_abt_end
​
​
•    /*irq mode stack*/    
​
•    msr cpsr,#0xd2
​
•    ldr sp, _stack_irq_end
​
•    
​
•    /*fiq mode stack*/
​
•    msr cpsr,#0xd1
​
•    ldr sp, _stack_fiq_end
​
•    
​
•    /*user mode stack, enable FIQ/IRQ*/
​
•    msr cpsr,#0x10
​
•    ldr sp, _stack_usr_end
​
​
•    /*Call main*/
​
•    b main----------------------跳转到mian函数入口
​
​
​
_stack_svc_end:      
​
•    .word stack_svc + 512----------------------------申请四字节大小空间,用于存放stack_svc+512地址,即sv模式下的栈的最高地址(末尾地址),下面同理
​
_stack_und_end:      
​
•    .word stack_und + 512
​
_stack_abt_end:      
​
•    .word stack_abt + 512
​
_stack_irq_end:      
​
•    .word stack_irq + 512
​
_stack_fiq_end:
​
•    .word stack_fiq + 512
​
_stack_usr_end:      
​
•    .word stack_usr + 512
​
​
​
/*
​
 *申请各个模式下的栈空间
​
 */
​
.data
​
stack_svc:      
​
•    .space 512---------------------------------------占据512字节的空间,告诉编译器这里是被占据的,到时候会作为svc模式下的栈来使用,下面同理
​
stack_und:
​
•    .space 512
​
stack_abt:      
​
•    .space 512
​
stack_irq:      
​
•    .space 512
​
stack_fiq:      
​
•    .space 512
​
stack_usr:      
​
•    .space 512
​
​
​
​

img

因为ARM采用满减栈,因此要使每一个模式下的SP栈指针初始化为各自的栈的最高地址,从高往低压栈

寄存器封装:

一条C语句可能会被编译器编译成很多条汇编语句,所以延时时间要比汇编程序的延时短

因此,这里的延时数1为百万,汇编延时数为1亿

1.宏定义封装:

img

img

与不使用宏定义相比,所编译生成的文件大小一样都是8908字节,可见宏定义只是做了替换,不占用文件长度

2.结构体封装:

img

参与封装成结构体的成员,必须具备以下两个属性:

①:控制的是同一种对象的属性

②:存储空间必须连续

?另外为什么会少4个字节?

img

以后用到寄存器的时候,直接包含exynos_4412.h即可

寄存器操作的标准化:

上面讲到的寄存器操作方式,操作特定位的同时都会影响到其他位,这是危险的,下面采用标准化方式:

#include "exynos_4412.h"
​
​
int main()
​
{
​
•    GPX2.CON = GPX2.CON & (~(0XF << 28)) | (0X1 << 28);
​
​
•    while(1)
​
•    {
•        GPX2.DAT |= (1 << 7);
​
•        delay(1000000);
​
•        GPX2.DAT &= (~(1 << 7));
​
•        delay(1000000);
​
•    }
​
•    return 0;
​
}
​
​
/*
​
\*   1.uisigned int a;将a的第三位置1,其他位保持不变
​
\*         ******** ******** ******** ********
​
\*         ******** ******** ******** ****1***
​
\*         a = a | (1 << 3);
​
*
​
*
​
\*   2.uisigned int a;将a的第三位置0,其他位保持不变
​
\*         ******** ******** ******** ********
​
\*         ******** ******** ******** ****0***
​
\*         a = a & (~(1 << 3));
​
*
​
\*   3.unsigned int a;将a的第[7:4]位置为0101,其他位不变
​
\*         ******** ******** ******** ********
​
\*         ******** ******** ******** 0101****
​
*
​
\*      1).先清零
​
\*      11111111 11111111 11111111 00001111
​
\*      00000000 00000000 00000000 11110000
​
\*      00000000 00000000 00000000 00001111
​
\*      a = a & (~(0XF << 4));
​
*
​
\*      2).再置位
​
\*      00000000 00000000 00000000 01010000
​
\*      00000000 00000000 00000000 00000101
​
\*      a = a | (0X5 << 4);
​
*
​
\*      --------------
​
\*      a = a & (~(0XF << 4) | (0X5 <<4)
​
*
​
*/
​
​
​
​

UART

UART与通信概述

Universal Asynchronous Receiver Transmitter

即通用异步收发器,是一种通用的串行、异步通信总线

该总线有两条数据线,可以实现全双工的发送和接收

在嵌入式系统中常用于主机与辅助设备之间的通信

并行通信:

img

可以一次性发送多个数据位

串行通信:

img

一次只能发送一个数据位

上图有点问题:同一时刻,一根线只能发送一个数据位,怎么可能一根线同时高低电平呢

单工与双工通信:

img

单工:只有一根线,且只能单向通信

半双工:同一时刻,只能一方发送、一方接收(==只有一根线)==,双向通信

全双工:同一时刻,双方均可发送和接收(两根线)

串行、并行对比:

并行总线速度比串行的快,但是却需要很多数据线,浪费资源

再者,串行总线线与线之间可能存在信号干扰

波特率

波特率用于描述UART通信时的通信速度,其单位为

bps(bit per second)即每秒钟传送的bit的数量

UART帧格式:

img

空闲位:当不发送数据时,数据线上为高电平

起始位:将数据线置为低电平,起始位为0,表示发送方开始发送数据(区别于空闲位)

数据位:实际要发送的数据,注意是5~8位,且先发低位,再发高位

校验位:可有可无(可以设置打开或关闭),可用于检查发送数据的正确性,一般为奇偶校验位,比如01010101,偶数个1时该位置1,奇数时置0

停止位:占用1/1.5或2位,用于表示本次发送的结束

当要发送多个字节数据时,按照UART帧格式循环发送,不允许连续发送

累计误差:

首先,假如发送方发送一个数据:0011,那么接收方怎么判断发送方发送了多少个0和多少个1呢?(因为比如01和0011的电平很相似)

答案是通过波特率,打个比方,假如波特率为1,即每秒传输1个比特位,就可以通过高低电平持续的时间,计算高低电平分别是多少位(其他波特率类似,比如115200bps,每一位传输时间为1/115200s)

再来看下面这个问题,假如此时发送方要发送11111111,还是假定波特率为1,那么在发送方看来,发送这组数据用时8秒,由于时钟不同,假如接收方时钟比较慢,比发送方慢了0.1秒,总的时间只走到7.2秒;

如果发送10个数据的话,发送方就会认为按照波特率,我10秒之内发送了10个位,而接收方9秒之内接收了10个数据,根据波特率,接收方只会认为自己接受了9个比特位,而发送方实际发送了10个比特位,那么就会少一位,出现误差,此后的数据将会全部紊乱,且数据比特位数越大,累计误差越大,因此不允许连续发送,最大一次只能发送8位,这是由于UART采用异步通信

异步通信:发送方与接收方时钟不同步

硬件连接

img

UART控制器

一般情况下处理器中都会集成UART控制器

我们使用UART进行通信时候只需对其内部的相关寄存器进行设置即可

(一般UART控制器会内部集成发送器、接收器)

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Exynos_4412下的UART:

本次实验我们使用丝印为CON7的串口,先来看电气原理图:

img

img

引脚的功能设置:

img

设置引脚功能的实质是让引脚在芯片内部连接到某一个对应的控制器上

也就是说某个引脚可能被多个控制器所共用,即引脚复用,比如上面的:

img

可见这两个引脚被GPIO与UART复用了,可以配置引脚的功能,使引脚与内部的对应的控制器相连

对于上面的两个电气原理图:4412芯片先将引脚配置为UART收发模式,以发送TXD2为例,芯片通过此引脚,经过SP3232EEA使驱动增强,再通过CON7串口向外发送数据

UART控制器组成与控制逻辑:

UART提供了ch0-ch4共5个通道,即共集成了5个UART控制器(这五个控制器内的寄存器都一模一样),每一个UART控制器都有一个发送器、一个接收器

本实验用ch2通道,即UART2

The Baud-rate generator uses SCLK_UART. The transmitter and the receiver contain FIFOs and data shifters. The data to be transmitted is written to Tx FIFO, and copied to the transmit shifter. The data is then shifted out by the transmit data pin (TxDn). The received data is shifted from the receive data pin (RxDn), and copied to Rx FIFO from the shifter.

SCLK_UART:UART控制器时钟,100MHZ;每一个控制器时钟频率都不一样,可以对每一个比特位做精准的时间控制,产生特定的波特率

对于FIFO模式:

img

控制器组成与逻辑:

img

最后有更为详细的通信过程图解

------------------------------------------------------------------------------------------------------------------------------------

UART寄存器详解:

本实验用到的寄存器有:

GPA1CON: 使用它的[7:0]位,将GPA1_0、GPA1_1分别配置成UART2接收引脚、发送引脚

ULCON2: 使用它的[6:0]位,设置UART2的帧格式,8位数据位、1位停止位、无校验位、正常模式

UCON2: 设置UART2的接收和发送模式为轮询模式,使用UCON2[3:0]

UBRDIV2和UFRACVAL2:设置UART2的波特率为115200bps,使用的是UBRDIV2[15:0]和UFRACVAL2[3:0]

UTXH2: 存放将要发送的数据,由发送器负责发送

URXH2: 存放由接收器接收到的数据

可能用到:

LOOP_BACK MODE回环模式:内部将TXD与RXD短接,自己发送自己接收,常用于测试通信

发送和接收数据的模式:

1.轮询:CPU不断扫描缓冲区,对于发送不满就写,对于接收不空就读

2.中断:通知机制,对于发送不满通知CPU可写,对于接收有数据通知CPU可读

3.DMA:直接存储器访问,CPU不需要参与,数据直接被DMA搬运至寄存器,或数据被DMA搬运至发送器

img

波特率设置方法:

img

小数位四舍五入即可

程序要写一个死循环,防止结束后程序跑飞(因为没有linux系统,没有进程)

-------------------------------------------------------------------------------------------------------------------------------------------------------------

UART编程:

注意:

SecureCRT默认显示字符

主频SCLK_UART为100MHZ,用于作为波特率产生器的时钟源

PART1:顺序发送'A','B','C','D'

1.UART2初始化:主要是配置:发送接收引脚、传输帧格式、发送接收模式、波特率

img

2.发送数据:

配置好寄存器之后,就要开始发送数据了。数据的发送,主要是UTXH2这个寄存器(Transmit Holding Register),实际发送数据的时候,只需要往这个寄存器里面写入数据就可以了。写入数据之后,UART相关的电路,自动发送数据给接收端。

img

img

但是从结果来看,不是按照ABCD的顺序发送的,是随机的,那么究竟是什么原因呢?

图解:

img

根据上面的图解:CPU工作频率为1000MHZ,而波特率为115200bps,cpu写入的速度远大于发送数据位的速度,但是仅仅UTXH2寄存器为空时才可写入。因此,‘A’写入时,发送器还没有发送完,cpu又企图将'B'写入,但是发现写不进去,再写'C'.,还是无法写入..........,某一时刻,发送器完成了'A'的全部数据位的发送,此时UTXH2空了下来,此时cpu不一定执行到哪一条指令,即不一定写哪个字符,因此发送的数据是随机的

改进方法一:延时

img

img

延时的目的是给发送器足够的时间发送数据,一段时间后再写入,即可做到有序发送,但是这种方法延时时间不好把握,效率不高

改进方法二:UTRSTAT2状态位判断

img

img

使用UTRSTAT2寄存器(发送接收状态寄存器),当发送Buffer为空(即UTXH2为空)时UTRSTAT2位[1]会自动置为1,当接收Buffer有数据时(即URXH2有数据时),UTRSTAT2位[0]会置为1

img

因此while(!(UART2.UTRSTAT2 & (1 << 1)));

是判断UTRSTAT2的位[1]是否为1,即Buffer是否为空,等待为空时退出,写入数据,非空时阻塞等待。

这样就保证了缓冲区为空时,才会写数据,且按顺序发送,PART1实验完成

PART2:通过电脑上的SecureCRT软件,向开发板发送一个字符,然后开发板收到后,将此数据+1返回

程序:

img

结果:

img

可以看到,键盘输入123abcd后,SecureCRT软件界面上显示了每个字符ASCII码加一后的结果。

而且:开发板刚上电要下载程序时,输入loadb命令的实质就是,通过键盘将数据发送给开发板,开发板接收到数据后,不做处理原样发送返回,于ScureCRT终端显示,而不是输入了直接就显示出来输入数据;对于该实验也是,通过键盘输入一字符,该字符并不会在SecureCRT显示,而是通过串口发送到UART(接收器),再由CPU进行运算,然后UART(发送器)将运算后的数据返回到SecureCRT显示

SecureCRT使用注意:

串口连接成功后,如下图所示,其中在串口号的前面会有一个绿色的对勾。如果串口发送数据过来,在其界面的空白处就会显示串口数据信息;如果需要发送串口信息,只要将光标定位到空白区域,然后输入信息即可(默认情况下,输入的信息不会显示)。

img

即发送什么数据直接通过键盘输入即可,界面上不会显示键盘输入的信息,而是显示通过串口接收到的数据信息

总结:

通信过程:

img

Transmit Holding Register(发送保持寄存器):即UTXH

Receive Holding Register(接受保持寄存器):即URXH

in FIFO mode, all bytes of Buffer Register are used as FIFO register. In non-FIFO mode, only 1 byte of Buffer Register is used as Holding register.

在FIFO模式下,缓冲区寄存器的所有字节都被用作FIFO寄存器。

在非fifo模式下,缓冲寄存器中只有1个字节被用作保持寄存器

PART2流程:

通过键盘输入一个字符1,被电脑接收到之后,发送给开发板,接收器接收到数据后,会放到接收保持寄存器(接收缓冲区)CPU会不断轮询扫描,有数据就将缓冲区内容读取到data变量,没有数据就return 0; 当cpu读到数据之后,在其内部进行运算,原有数据ASCII码加1,非空while阻塞等待,当发送保持寄存器为空时,写到发送保持寄存器,再由发送器(移位器)进行发送,即发送字符2

拓展:

串口通信实验 (renrendoc.com)

UART通信-pudn.com

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------

输入输出重定向:

对UART编程的代码进行分析:

UART2_Send_Byte类似于标准输出函数putchar、UART2_Recv_Byte类似于标准输入函数getchar

void UART2_Send_Byte(char data)
​
{
​
•    /*等待发送寄存器空闲*/
​
•    while(!(UART2.UTRSTAT2 & (1 << 1)));
​
•    /*将要发送的数据写入UTXH2寄存器*/
​
•    UART2.UTXH2 = data;
​
}
​
​
char UART2_Recv_Byte(void)
​
{
•    char data = 0;
​
•    /*判断接收器是否接收到数据*/
​
•    if(UART2.UTRSTAT2 & (1 << 0))
​
•    {
•        data = UART2.URXH2;
•        return data;
•    }
​
​
​
•    else
​
•    {
•        return 0;
•    }
​
}
​
​

img

下面我们再实现一个类似于库函数puts的自定义函数:

void UART2_Send_Str(char *s)

{

​    while(*s)

​    {

​          UART2_Send_Byte(*s++);

​    }

}

可实现在SecureCRT上显示字符串的功能

也就是说对于我们自己写的这三个函数,输出和输入都是定向到串口,下面再来详细解释输出输入重定向:

老师提供的interface中有自定义的printf函数(UART/common/src/):

该printf函数与C库函数中的printf有两点不同:

1.来源不同

以前经常使用的printf属于标准C库函数,由linux系统提供,用户可以直接调用

而裸机开发板上没有装linux系统,所有的库函数都不能用,老师提供的是自定义printf函数,不是c库函数,当然功能也没有库函数的全面

2.输出定向不同

先来看ls命令:

如果直接在终端上敲ls命令,终端上就会被输出当前目录下的文件;但如果敲ls > out.txt命令,终端上不会显示,系统将当前目录下的文件输出到out.txt文件中,也就是说发生了输出重定向

具体是这样实现的:

前者ls命令执行后,系统会统计当前目录所有文件,然后将信息输出给显卡驱动,显卡再控制屏幕显示文件信息,即定向到屏幕

后者ls > out.txt执行后,系统也会统计文件信息,然后将信息输出到磁盘,由磁盘将文件信息写到文件out.txt

img

对于老师提供的自定义printf:

vsprintf对参数进行解析,但不会输出,将解析后的信息放到printfbuffer缓冲区,然后由puts输出,当然这个puts函数也是自定义函数,位于common/src/

img

来看puts的原型:

puts调用putc函数,逐个字符发送,而putc函数具体实现是若串口2发送寄存器非空则等待,空则写入数据,由发送器发送到串口输出端

img

即如下图:

库函数printf定向到显卡,自定义函数printf定向到串口

img

具体实现流程:printf调用puts,puts调用putc,putc再实现数据由串口发出

img

开发板的标准输入输出都是串口,下面是开发板刚上电时,SecureCRT的显示:

img

作业:

若使用UART协议发送一个字节的数据0x63,画出信号线上的时序图

注:8位数据位、无校验位、一位停止位

注意:数据位从最低位开始传输

编程实现电脑远程控制LED状态

注:在终端上输入‘2’,LED2点亮,再次输入‘2’,LED2熄灭... ...

#include "exynos_4412.h"


void UART2_Init(void);
void UART2_Send_Byte(char data);
char UART2_Recv_Byte(void);
void UART2_Send_Str(char *s);

int main()
{
	/*
	UART2_Init();

	while(1)
	{
		UART2.UTXH2 = 'A';
		UART2.UTXH2 = 'B';
		UART2.UTXH2 = 'C';
		UART2.UTXH2 = 'D';
	}
	*/

	/*	
	UART2_Init();
	while(1)
	{
		UART2_Send_Byte('A');
		UART2_Send_Byte('B');
		UART2_Send_Byte('C');
		UART2_Send_Byte('D');
	}
	*/

	/*	
	char data = 0;
	UART2_Init();

	while(1)
	{
		data = UART2_Recv_Byte();
		if(data)
		{
			data += 1;
			UART2_Send_Byte(data);
		}
	}
	*/

	/*
	UART2_Init();
	while(1)
	{
		//UART2_Send_Str("hello world\n");
		printf("hello world\n");
	}
	*/

	char data = 0;
	int count = 0;
	UART2_Init();
	GPX2.CON = GPX2.CON & (~(0xF << 28)) | (0x1 << 28);
	GPX2.DAT = GPX2.DAT & (~(1 << 7));
	
	while(1)
	{
		data = UART2_Recv_Byte();

		if('2' == data)
		{
			if(0 == count % 2)
			{
				GPX2.DAT = GPX2.DAT | (1 << 7);
				UART2_Send_Str("ON ");
				count++;
			}

			else if(1 == count % 2)
			{
				GPX2.DAT = GPX2.DAT & (~(1 << 7));
				UART2_Send_Str("OFF ");
				count++;
			}
		}
	}


	return 0;
}


void UART2_Init(void)
{
	/*1.将GPA1_0 GPA1_1分别设置成UART2的接收引脚和发送引脚,使用的是GPA1CON[7:0]*/
	GPA1.CON = GPA1.CON & (~(0xFF << 0)) | (0x22 << 0);

	/*2.设置UART2的帧格式,使用的是ULCON2[6:0]  8位数据位 1位停止位 无校验位 正常模式*/
	UART2.ULCON2 = UART2.ULCON2 & (~(0X7F << 0)) | (0X3 << 0);

	/*3.设置UART2的接受和发送模式为轮询模式,使用UCON2[3:0]*/
	UART2.UCON2 = UART2.UCON2 & (~(0XF << 0)) | (0X5 << 0); //(1 << 5);

	/*4.设置UART2的波特率为115200bps,使用的是UBRDIV2[15:0]和UFRACVAL2[3:0]*/
	UART2.UBRDIV2 = 53;
	UART2.UFRACVAL2 = 4;
}

void UART2_Send_Byte(char data)
{
	/*等待发送寄存器空闲*/
	while(!(UART2.UTRSTAT2 & (1 << 1)));
	/*将要发送的数据写入UTXH2寄存器*/
	UART2.UTXH2 = data;
}


char UART2_Recv_Byte(void)
{
	char data = 0;
	/*判断接收器是否接收到数据*/
	if(UART2.UTRSTAT2 & (1 << 0))
	{
		data = UART2.URXH2;
		return data;
	}

	else
	{
		return 0;
	}
}

void UART2_Send_Str(char *s)
{
	while(*s)
	{
 		 UART2_Send_Byte(*s++);
	}
}

WDT

关键字:

自动监控、发送复位

两种模式:普通定时器、WDT看门狗

PCLK:100MHZ

一级分频0~255->1~256、二级分频

WDT介绍:

Watch Dog Timer

即看门狗定时器,其主要作用是当发生软件故障时可产生复位信号使SOC复位,其本质是一个计数器

img

WDT原理:

img

WDT工作逻辑:

img

使用是的PCLK为100MHZ,经过一级分频(通过WTCON[15:8]设置分频),分频之后得到一个过程值,会再被分频(通过 WDTCON[4:3]设置分频,可选固定分频),经二级分频分频之后,该频率就会作为WTCNT工作频率,WTCNT减到0后,会在不同模式产生不同信号:

①:普通定时模式,产生一个中断信号,通知CPU作某个处理;

②:看门狗模式:产生一个复位信号,让CPU复位

可通过以下公式计算递减周期(计数周期):

img

一级分频所采用的8位预分频器,容纳的数据范围是0-255,0不能做除数,因此实际的分频数位1-256,即原来的预分频值基础上加1

WDT寄存器详解:

WTCON:

img

WTCON[15:8]:设置预分频值,他是你实际想分频数减一

WTCON[7:6]:虽然保留,但是要设置为00

WTCON[5]:看门狗使能/禁止位

WTCON[4:3]:二级分频时钟选择位

WTCON[2]:使能中断位

WTCON[1]:虽保留,但要设置为0

WTCON[0]:使能/禁止复位信号

If you want to use the normal timer that WDT provides, enable the interrupt and disable the WDT.

如果您想使用WDT提供的正常定时器,请启用中断并禁用WDT

WTCNT:存放当前WDT计数值最大不超过65535

The WTCNT register contains the current count values for the WDT during normal operation. WDT counter logic cannot automatically load the content of WTDAT register into the timer count register if it enables the WDT initially. Therefore, you should set the WTCNT register to an initial value before enabling it

WTCNT寄存器包含正常操作期间WDT的当前计数值。WDT计数器逻辑不能自动将WTDAT寄存器的内容加载到计时器计数寄存器中,如果它最初启用WDT。因此,您应该在启用WTCNT寄存器之前将其设置为初始值

WTDAT:

The WTDAT register specifies the time-out duration. You cannot load the content of WTDAT into the timer counter at initial WDT operation. However, by using 0x8000 (initial value) drives the WDT counter first time-out. In this case, WDT counter logic reloads the value of WTDAT automatically into WTCNT.

WTDAT寄存器指定超时时间。 不能将WTDAT的内容加载到计时器计数器中

在初始WDT操作时。 但是,通过使用0x8000(初始值)驱动WDT计数器第一次超时。 在这个

在这种情况下,WDT计数器逻辑自动将WTDAT的值重新加载到WTCNT中。

常用于实时时钟

WTCLRINT:中断清除,这里暂时不用

--------------------------------------------------------------------------------------------------------------

WDT编程:

注意:

WTCON[5]默认上电开启,但开发板刚上电,执行的是引导程序,而非我们写的程序,而引导程序会将该位清零,因此我们如果要用WDT,需要再启用该位;而且按位域配置时需要先设置完其他位并装入计数初值后,才开启该位

开发板里固化的引导程序已经初始化串口,我们就没必要再去初始化串口了,这一点从开发板一上电SecureCRT就打印提示芯片信息可以看出,这些信息显然是从串口传来的。

1.装入初值后递减到0,观察结果

img

调试结果:

img

也就是说,当WTCON与WTCNT配置好之后,开始使能WDT,计数器开始递减,同时进入死循环,由于没有刷新计数值,计数器在递减到0之后,就开始产生复位信号,开发板复位(重启),于是就有了上面的结果

2.执行程序,不断喂狗,观察结果:

img

img

循环中第一句打印出WTCNT里的当前计数值15260=3052*5,然后喂狗刷新数据,接着进行一段时间的延时(这里不到1秒),然后开始递减,又打印递减后的数据,

接着又开始喂狗,计数值又会被更新,这样的话就不会减到0,WDT就不会发生复位信号

同时注意:调用自定义的printf函数,不需要进行串口的初始化,因为引导程序已经初始化过【不然开发板一上电,SecureCRT也不会显示硬件信息】

一定要注意,如果按位域配置寄存器时,合理的逻辑是:先配置完WTCON的部分与WTCNT位,最后再使能WDT位

另外,这样写也是可以的:

img

img

这样做是先配置WTCNT计数器,装入初值后,一次性全部配置了WTCON寄存器,也可达到与上面一样的效果

轮询与中断:

关键字:

中断是异常的一种,异常不是一种错误,而是一种功能实现机制

边沿触发没有对与错之分,不同场合下有不同需求罢了

EXT_INT4x_FLTCONn 滤波寄存器

cpu与硬件的交互方式

轮询

CPU执行程序时不断地询问硬件是否需要其服务,若需要则给予其服务,若不需要一段时间后再次询问,周而复始

中断

CPU执行程序时若硬件需要其服务,对应的硬件给CPU发送中断信号,CPU接收到中断信号后将当前的程序暂停下来,转而去执行中断服务程序,执行完成后再返回到被打断的点继续执行

DMA

硬件产生数据后,硬件控制器可将产生的数据直接写入到存储器中,整个过程无需CPU的参与

轮询实现按键实验:

#include "exynos_4412.h"



int main()

{

​    /*将GPX1_1配置为输入*/

​    GPX1.CON = GPX1.CON & (~(0xF << 4));



\#if 0

​    int count = 1;

​    while(1)

​    {

​        /*判断GPX1_1状态,检测按键是否按下*/

​        if(!(GPX1.DAT & (1 << 1)))

​        {

​            if(count)

​            {

​                printf("key pressed\n");

​                count = 0;

​            }

​        }



​        else

​        {

​            count = 1;

​        }



​    }

\#else 1

​    while(1)

​    {

​        if(!(GPX1.DAT & (1 << 1)))

​        {

​            printf("key pressed\n");

​            while(!(GPX1.DAT & (1 << 1)));  //优化:等待松手,没有这一句,相对于cpu而言我们从按下按键到松手时间长,会不断循环打印出key pressed

​        }

​    }



\#endif



​    return 0;

}

按键电气原理图:

img

①:首先使用GPX1CON寄存器将GPX1_1引脚配置为输入功能

img

②:使用GPX1DAT寄存器判断按键是否按下

---------------------------------------------------------------------------------------------------------------------------------------------

GPIO中断寄存器详解:

img

①:GPX1CON配置GPX1_1为中断功能

img

②:EXT_INT41_CON选择中断触发方式:

img

这里选择比较合适的下降沿触发

③:EXT_INT41_FLTCON0用于电平滤波,这里我们不用

img

为什么要滤波?

因为按键是机械硬件,按下时会产生抖动(不会立刻闭合),或者接触不良也会产生杂波,滤除杂波可以较理想的达到合适的电平

④: EXT_INT41_MASK选择使能或屏蔽中断:

img

这里我们选择使能,注意0为使能

⑤: EXT_INT41_PEND挂起中断请求:(系统会自动置1)

img

图解

img

cpu响应分为两种情况:

cpu执行正常程序时,遇到按键触发IRQ中断(中断已被挂起),会很快响应进入异常处理程序;

cpu正在执行IRQ中断,此时发出IRQ请求后,中断也被挂起,等待cpu处理完之前的异常程序再响应自己

GPIO中断编程:

#include "exynos_4412.h"



int main()

{

​    /*1.将GPX1_1设置为中断功能*/

​    GPX1.CON = GPX1.CON | (0xF << 4);



​    /*2.设置中断触发方式*/

​    EXT_INT41_CON = EXT_INT41_CON & (~(0x7 << 4)) | (0x2 << 4);    



​    /*3.使能GPX1_1的中断功能*/

​    EXT_INT41_MASK = EXT_INT41_MASK & (~(1 << 1));


​    return 0;

}

作业:

使用轮询的方式检测Key3按键的状态,实现按一次按键,LED2点亮,再次按下,LED2熄灭

#include "exynos_4412.h"



void delay(unsigned int time)

{

​    while(time--);

}



int main()

{



\#if 0

​    /*将GPX1_1配置为输入*/

​    GPX1.CON = GPX1.CON & (~(0xF << 4));



​    int count = 1;

​    while(1)

​    {

​        /*判断GPX1_1状态,检测按键是否按下*/

​        if(!(GPX1.DAT & (1 << 1)))

​        {

​            if(count)

​            {

​                printf("key pressed\n");

​                count = 0;

​            }

​        }



​        else

​        {

​            count = 1;

​        }



​    }

\#endif



\#if 0

​    /*将GPX1_1配置为输入*/

​    GPX1.CON = GPX1.CON & (~(0xF << 4));



​    while(1)

​    {

​        if(!(GPX1.DAT & (1 << 1)))

​        {

​            printf("key pressed\n");

​            while(!(GPX1.DAT & (1 << 1)));

​            delay(1000000);

​        }

​    }



\#endif



\#if 1

​    /*作业*/

​    /*将GPX1_2配置为输入*/

​    GPX1.CON = GPX1.CON & (~(0xF << 8));

//    printf("%x\n",GPX2.DAT);



​    /*将GPX2_7配置为输出*/

​    GPX2.CON = GPX2.CON & (~(0xF << 28)) | (1 << 28);



​    /*将GPX2_7输出低电平*/

​    GPX2.DAT = GPX2.DAT & (~(1 << 7));

//    printf("%x\n",GPX2.DAT);



​    int count = 0;

​    while(1)

​    {

​        /*判断按键key3是否按下*/

​        if(!(GPX1.DAT & (1 << 2)))

​        {

​            count = ~ count;



​            /*按键按下*/

​            if(count)

​            {

​                /*点亮led2*/

​                GPX2.DAT = GPX2.DAT | (1 << 7);

​                /*等待松手*/

​                while(!(GPX1.DAT& (1 << 2)));

​                /*延时消抖*/

​                delay(1000000);

​                //printf("hhh\n“);

​            }



​            else

​            {

​                /*熄灭led2*/

​                GPX2.DAT = GPX2.DAT & (~(1 << 7));

​                /*等待松手*/

​                while(!(GPX1.DAT& (1 << 2)));

​                /*延时消抖*/

​                delay(1000000);

​            }

​        }

​    }



\#endif



\#if 0

​    GPX1.CON = GPX1.CON & (~(0xF << 4));

​    while(1){

​        if( !(GPX1.DAT & (1 << 1)) ){

​            GPX2.CON = 0x10000000;

​            GPX2.DAT = 0x00000080;

​            while(!(GPX1.DAT & (1 << 1)));

​        }



​        if(GPX1.DAT & (1 << 1)){

​            GPX2.CON = 0x10000000;

​            GPX2.DAT = 0x00000000;

​        }

​        /*

​         \* else{

​         \* GPX1_12.CON = 0x10000000;

​         \* GPX2.DAT = 0x00000000;

​         \* }

​         \* */

​    }

\#endif



​        return 0;

}

注意:

为了更理想的效果,需要加上等待松手与延时消抖:

写等待松手是防止循环判断,因为cpu眼里我们松开按键是很慢的;

写延时消抖是防止按键产生的机械抖动,否则即使松手,按键电平也会不断变化,加上延时,可以确保延时后的电平为高电平

调试记录:

过程中由于疏忽将GPX1.DAT写成了GPX2.DAT,但是令我奇怪的是:

错误程序对于k3可行,可以达到预期效果,即按一下亮,再按一下灭,但是如果将程序稍作改动,移植到k2上(只用改配置寄存器与数据寄存器即可),按一下亮,此后再按就不会灭了

为了究其原因,我做了以下测试:

我在延时消抖下面加上了printf("hhh\n");打印测试,发现只打印一次,仔细观察才发现:GPX1.DAT写成了GPX2.DAT,改过来后对于k3,k2均正常显示

但是为什么即使我写错了,k3也能正常显示,可以达到我想要的效果呢?而k2就不行呢

原因竟然是GPX2.DAT的复位值为0x8c,即1000 1100,这样的话,:

对于k3(连接的是GPX1_2引脚),而 while(!(GPX2.DAT & (1 << 2)));不满足会立刻跳出

对于k2(连接的是GPX1_1引脚),而 while(!(GPX2.DAT & (1 << 1)));会永远成立,不会跳出,因此才有了printf只打印一次的现象,led2按一下亮,此后再按就不会灭了

将程序改为GPX1.DAT一切正常


中断控制器:

中断控制器:

引入以下问题:

①:如果同时将多个中断发给cpu,cpu无法同时处理

②:如果cpu此时正在处理中断,那么不会响应新的中断

③:响应的中断类型该怎么选择

④:中断信号送给多核处理器的哪一个cpu

于是,便引入了中断控制器,对中断进行统一的管理:

中断控制器作用:

多个中断同时产生时可对这些中断挂起排队,然后按照优先级依次发送给CPU处理

可以为每一个中断分配一个优先级

一个中断正在处理时若又产生其它中断,可将新的中断挂起,待CPU空闲时再发送

可以为每一个中断选择一个CPU处理

可以为每一个中断选择一个中断类型(FIQ或IRQ)

CPU接收到中断信号后并不能区分是哪个外设产生的,此时CPU可查询中断控制器 来获取当前的中断信号是由哪个硬件产生的,然后再进行对应的处理

可以打开或禁止每一个中断

...........................

注意:

同时产生多个中断:中断控制器可以设置中断的优先级,高优先级可以排前面,低优先级排后面,但高优先级中断不能打断低优先级中断

如果cpu正在处理中断:中断控制器会将中断挂起,等cpu处理完中断后,再处理被挂起的中断

默认开发板上电,使用的是cpu0

每一个cpu和中断控制器都有一个接口,来选择是否将中断信号送达

为中断信号选择类型的寄存器、配置优先级的寄存器默认即可,不需要设置

中断号 = SPI号+32

GPX1_1对应的中断号 57---------EINT9

Table 9-2 GIC Interrupt Table (SPI[127:0])

img

中断控制器的寄存器:

ARM硬件中断一共160个

Total 160 interrupts including Software Generated Interrupts (SGIs[15:0], ID[15:0]), Private Peripheral Interrupts

(PPIs[15:0], ID[31:16]) and Shared Peripheral Interrupts (SPIs[127:0], ID[159:32]) are supported. For SPI, you can

service a maximal 32 * 4 = 128 interrupt requests.

总共160个中断,包括软件生成的中断(SGI[15:0],ID[15:0]),私有外围中断

支持(PPIs[15:0],ID[31:16])和共享外围中断(SPIs[127:0], ID[159:32])。 对于SPI,你可以

服务最大32 * 4 = 128个中断请求。

ICDDCR: GIC的总开关----9.5.1.12

img

ICDISER_CPU: 设置160个中断中某个中断的使能或禁止 ,那么57号中断使用的是 (ICDISER1_CPU0)进行配置------------9.5.16

img

ICDIPTR_CPU: 设置中断的目标cpu:可以通过设置具体偏移地址的寄存器的某些位来选择特定中断的目标cpu,比如57号中断使用的是偏移地址为0x0838的寄存器(ICDIPTR14_CPU0)的[15:8]

每一个寄存器管理四个中断源,每一个字节管理一个中断源,向该字节写数据可以为中断选择目标cpu

img

img

ICCICR_CPUn: 打开我们想要中断得到处理的中断控制器与目标cpu之间的接口(开关)

img

中断控制器程序:

#include "exynos_4412.h"

int main()

{
​    /*外设层次 - 配置引脚,让外设的硬件控制器能够产生中断信号给中断控制器GIC*/

​    /*1.将GPX1_1设置为中断功能*/

​    GPX1.CON = GPX1.CON | (0xF << 4);



​    /*2.设置中断触发方式*/

​    EXT_INT41_CON = EXT_INT41_CON & (~(0x7 << 4)) | (0x2 << 4);    



​    /*3.使能GPX1_1的中断功能*/

​    EXT_INT41_MASK = EXT_INT41_MASK & (~(1 << 1));



​    /*中断控制器层次 - 让中断控制器接收外设发来的中断信号并进行管理再转发给合适的CPU处理*/

​    /*4.使能GIC中断控制器,使其能够接受到中断后转发给cpu处理*/

​    ICDDCR = ICDDCR | (1 << 0);



​    /*5.使能57号中断EINTR9,使中断控制器接收到57号中断并转发给cpu接口*/

​    ICDISER.ICDISER1 = ICDISER.ICDISER1 | (1 << 25);



​    /*6.为57号中断EINTR9选择cpu0*/

​    ICDIPTR.ICDIPTR14 = ICDIPTR.ICDIPTR14 & (~(0xFF << 8)) | (1 << 8);



​    /*7.打开GIC与cpu0间的接口,使得中断控制器能将中断信号送达cpu0*/

​    CPU0.ICCICR = CPU0.ICCICR | (1 << 0);


​    return 0;

}

img


中断处理:

1.工程模板代码结构分析:

img

首先各目录下的文件都被Makefile编译生成.bin文件(其实是.elf文件后使用交叉编译工具转换为.bin),interface.bin结构是有顺序的

map.lds决定链接的格式与结构:即哪些源文件生成的.o链接到前,哪些链接到后

从map.lds脚本链接文件可知,start.s生成的机器码先被执行【因为任何处理器在执行用户程序之前都要先运行一段启动代码,最后配置好一些东西,再跳转到main函数入口去执行用户程序】

下面我们看start.s文件究竟做了哪些事

简要概括为如下几点:

设置异常向量表地址

设置为svc模式,并关闭FIQ/IRQ中断

使页表失效、关闭MMU

申请每个模式下的栈空间,初始化各个模式下的栈指针

前面C工程与寄存器封装有更为详细的说明,忘了可以翻一翻

2.中断处理框架搭建

start.S:

.text

.global _start

_start:

	/*

	 * Vector table

	 */ 

	b reset

	b .

	b .

	b .

	b .

	b .

	/*PC从异常向量表跳到IRQ异常处理程序*/

	b irq_handler

	b .


reset:

	/*

	 * Set vector address in CP15 VBAR register

	 */ 

	ldr	r0, =_start

	mcr	p15, 0, r0, c12, c0, 0	@Set VBAR


	/*

	 * Set the cpu to SVC32 mode, Disable FIQ/IRQ

	 */  

	mrs r0, cpsr

	bic r0, r0, #0x1f

	orr	r0, r0, #0xd3

	msr	cpsr ,r0


	/*

	 * Defines access permissions for each coprocessor

	 */  

    mov	r0, #0xfffffff

    mcr	p15, 0, r0, c1, c0, 2  	


	/*

	 * Invalidate L1 I/D       
     */

	mov	r0, #0					@Set up for MCR

	mcr	p15, 0, r0, c8, c7, 0	@Invalidate TLBs

	mcr	p15, 0, r0, c7, c5, 0	@Invalidate icache

	
	/*

	 * Set the FPEXC EN bit to enable the FPU

	 */ 

	mov r3, #0x40000000

	fmxr FPEXC, r3

	
	/*

	 * Disable MMU stuff and caches

	 */

	mrc	p15, 0, r0, c1, c0, 0

	bic	r0, r0, #0x00002000		@Clear bits 13 (--V-)

	bic	r0, r0, #0x00000007		@Clear bits 2:0 (-CAM)

	orr	r0, r0, #0x00001000		@Set bit 12 (---I) Icache

	orr	r0, r0, #0x00000002		@Set bit 1 (--A-) Align

	orr	r0, r0, #0x00000800		@Set bit 11 (Z---) BTB

	mcr	p15, 0, r0, c1, c0, 0



	/*

	 * Initialize stacks                                                                                                                  

	 */

init_stack:     

	/*svc mode stack*/

	msr cpsr, #0xd3

	ldr sp, _stack_svc_end



	/*undef mode stack*/

	msr cpsr, #0xdb

	ldr sp, _stack_und_end



	/*abort mode stack*/	

	msr cpsr,#0xd7

	ldr sp,_stack_abt_end



	/*irq mode stack*/	

	msr cpsr,#0xd2

	ldr sp, _stack_irq_end

	

	/*fiq mode stack*/

	msr cpsr,#0xd1

	ldr sp, _stack_fiq_end

	

	/*user mode stack, enable FIQ/IRQ*/

	msr cpsr,#0x10
        
	ldr sp, _stack_usr_end



	/*Call main*/

	b main



/*irq中断处理程序*/

irq_handler:

			//因为主程序被irq打断时,LR保存的是被打断指令的下下条指令的地址
			//因此要人为修复

			sub lr,lr,#4

			

			//因为IRQ模式下的R0-R12与USER模式下使用的是同一组寄存器

			//所以处理异常程序之前需要将USER模式下寄存器中的值压栈保护

			stmfd sp!,{r0-r12,lr}



			//处理异常

			bl do_irq


			//异常返回

			//1.将r0-r12寄存器中的值出栈恢复现场

			//2.将irq模式下的spsr中的值恢复给cpsr,使得cpu能恢复到被中断打断之前的状态

			//3.将栈中lr的值出给pc

			ldmfd sp!,{r0-r12,pc}^



_stack_svc_end:      

	.word stack_svc + 512

_stack_und_end:      

	.word stack_und + 512

_stack_abt_end:      

	.word stack_abt + 512

_stack_irq_end:      

    .word stack_irq + 512

_stack_fiq_end:

    .word stack_fiq + 512

_stack_usr_end:      

    .word stack_usr + 512



.data

stack_svc:      

	.space 512

stack_und:

	.space 512

stack_abt:      

	.space 512

stack_irq:      

	.space 512

stack_fiq:      

	.space 512

stack_usr:      

	.space 512

3.中断编程:

interface.c:

#include "exynos_4412.h"

void delay(unsigned int time)

{
	while(time--);
}

#if 1 

//异常处理程序

void do_irq()

{
	unsigned int irqnum = 0;

	/*从中断控制器获取当前中断的中断号*/

	irqnum = CPU0.ICCIAR & 0x3FF;


	switch(irqnum)

	{
		case 0:

			//0号中断的处理程序

			break;

		case 1:

			//1号中断的处理程序

			break;

		//..........

		case 57:

			//57号中断的处理程序


			printf("key2 pressed\n");

			/*清除EXIT_INT41_PEND中断挂起位*/

			EXT_INT41_PEND |= (1 << 1);


			/*将处理完成的中断的中断号写回GIC,告知GIC该中断已经处理完,可以发送其他中断*/

			CPU0.ICCEOIR = CPU0.ICCEOIR & (~(0x3FF)) | (57);

			/*

		   	asm

		   	(

		   	"mov pc,lr\n"

		   	);

		   	*/

			break;

		case 159:

			//159号中断的处理程序

			break;

		default:

			break;
	}
}


int main()

{
	/*外设层次 - 配置引脚,让外设的硬件控制器能够产生中断信号给中断控制器GIC*/

	/*1.将GPX1_1设置为中断功能*/

	GPX1.CON = GPX1.CON | (0xF << 4);


	/*2.设置中断触发方式*/

	EXT_INT41_CON = EXT_INT41_CON & (~(0x7 << 4)) | (0x2 << 4);	


	/*3.使能GPX1_1的中断功能*/

	EXT_INT41_MASK = EXT_INT41_MASK & (~(1 << 1));


	/*中断控制器层次 - 让中断控制器接收外设发来的中断信号并进行管理再转发给合适的CPU处理*/

	/*4.使能GIC中断控制器,使其能够接受到中断后转发给cpu处理*/

	ICDDCR = ICDDCR | (1 << 0);


	/*5.使能57号中断EINTR9,使中断控制器接收到57号中断并转发给cpu接口*/

	ICDISER.ICDISER1 = ICDISER.ICDISER1 | (1 << 25);


	/*6.为57号中断EINTR9选择cpu0*/

	ICDIPTR.ICDIPTR14 = ICDIPTR.ICDIPTR14 & (~(0xFF << 8)) | (1 << 8);


	/*7.打开GIC与cpu0间的接口,使得中断控制器能将中断信号送达cpu0*/

	CPU0.ICCICR = CPU0.ICCICR | (1 << 0);

	GPX2.CON = GPX2.CON & (~(0XF << 28)) | (0X1 << 28);

	while(1)

	{
		GPX2.DAT |= (1 << 7);

		delay(1000000);

		GPX2.DAT &= (~(1 << 7));

		delay(1000000);
	}

	return 0;
}

#endif 

代码思路详解:

1.首先,开发板一上电,将编译后生成的.bin文件烧录到开发板;

2.因为先执行start.s启动代码(任何芯片上电的第一段启动代码都是拿汇编写的),所以会

首先执行到b reset,执行完这句指令,直接跳到reset标号;

我们来看一下reset里写的什么逻辑,做了哪些事?

1)设置异常向量表地址

2)设置cpu到svc模式,关闭FIQ/IRQ

4)配置协处理器、关闭页表、MMU等

5)初始化栈

4.最后在初始化USER模式下的栈,打开FIQ/IRQ后跳到main函数(C与汇编的混合编程)

5.进入到mia函数,首先执行中断初始化程序:

外设层次:

1)配置引脚为中断功能

2)设置中断触发方式为下降沿触发

3)使能引脚的中断功能

GIC中断控制器层次:

4)使能GIC,使其可以接收并转发中断信号

5)使能57号中断,使GIC能够接收到57号中断信号

6)为当前中断选择cpu0

7)使能开启中断控制器与cpu0之间的接口

6.然后就进入了while循环,某一时刻假如按键按下触发了中断,根据异常处理机制:

会先自动完成4件事:CPSR备份、修改CPSR、保存返回地址到LR、设置PC为异常向量地址

于是跳到异常向量表对应的irq异常源地址,因为这里不能直接写异常处理程序,因此我们用

一个跳转指令b irq_handler来跳到irq异常处理程序的入口:

7.进入到irq_handler函数(写到main之后):

1)首先将lr修正,具体原因如下:

先来看连接寄存器LR的原理: ​ 当执行跳转指令或产生异常时,LR寄存器中不会凭空产生一个返回地址 ​ 其原理是当执行跳转指令或产生异常时,处理器内部会将PC寄存器中的 ​ 值拷贝到LR寄存器中,然后再将LR寄存器中的值自减4

但是执行跳转指令BL与处理异常时保存的返回地址不同,因为:

所以当产生异常时,LR的值为被异常打断的下下条指令的地址,地址不是我们想要的,因此再减去4修正

8.压栈保护:

因为IRQ模式与USER模式使用的r-r12是同一组寄存器,因此若直接使用就可能出现寄存器的值的覆盖问题,需要将可能受到破坏的寄存器压栈保护现场,而且就lr而言,虽然已经被修正了,但是如果存在非叶子函数,就可能出现BL指令,那么LR寄存器的值也会遭到破坏,因此都需要压栈

stmfd sp!,{r0-r12,lr}

9.处理异常do_irq:

处理异常:首先需要获取当前中断的中断号,因为需要对外设进行区分,到底是哪一个硬件控制器产生的什么中断

对不同的中断进行不同的处理,下面主要讨论57号中断的处理:

先打印按键按下提示,再清除中断挂起位,最后CPU告诉GIC已经处理完了当前中断,可以发送其他中断了

10.异常返回

//1.将r0-r12寄存器中的值出栈恢复现场

//2.将irq模式下的spsr中的值恢复给cpsr,使得cpu能恢复到被中断打断之前的状态

//3.将栈中lr的值出给pc

ldmfd sp!,{r0-r12,pc}^

便可返回主程序,如果再触发中断,按照上面的步骤,开始下一轮的循环

代码中遇到的问题及注意点:

①:为什么不加mov pc,lr?

bl do_irq 这一句后面为什么不加mov pc,lr用于返回呢?

因为在do_irq程序里编译之后的汇编已经有返回指令了

==②:EXIT_INT41_PEND[1] 很特殊,写1清零,要记住==

③:为什么按一次会打印这么多?

img

需要清除中断挂起标志位,写1清除

④:.cpu0如何对中断信号进行区分,即到底是由哪一个硬件控制器产生的:

img

cpu无法区分,但是中断控制器可以,GIC转发给cpu时,会将低10位写成中断号,cpu处理之前直接读取即可

⑤:.为什么按多次只打印一次?

img

GIC会一直等待cpu空闲,但是cpu空闲GIC不知道,需要设置寄存器让它知道:

cpu处理完后,直接使用ICCEOIR寄存器将已经处理完成的中断号写回中断控制器GIC,表示cpu已经处理完一个中断,可以发送其他中断了

实际开发,中断方式用的比较多,效率高,轮询几乎很少用,效率低

作业:

使用中断的方式检测Key3按键的状态,实现按一次按键,LED2点亮,再次按下,LED2熄灭

#if 1

/*作业*/

void GPIO_Init();

void INTR_Init();

void do_irq();

int main()

{
	GPIO_Init();

	INTR_Init();

//	printf("%x\n",EXT_INT41_PEND);

	while(1)

	{
		delay(1000000);
	}

	return 0;
}


/*GPX1_2初始化*/

void GPIO_Init()

{
	GPX2.CON = GPX2.CON & (~(0xF << 28)) | (1 << 28);

	GPX2.DAT = GPX2.DAT & (~(1 << 7));
}



/*中断初始化*/

void INTR_Init()

{
	/*配置GPX1_2为中断引脚*/

	GPX1.CON = GPX1.CON & (~(0xFF << 8)) | (0xF << 8);


	/*配置触发方式为下降沿触发*/

	EXT_INT41_CON = EXT_INT41_CON & (~(0x7 << 8)) | (0x2 << 8);

	//printf("%x\n",EXT_INT41_FLTCON0 & (0XFF << 16));


	/*使能中断功能*/

	EXT_INT41_MASK = EXT_INT41_MASK & (~(1 << 2));


	/*使能GIC*/

	ICDDCR = ICDDCR | 1;


	/*使能58号中断,使GIC可以接收到并转发*/

	ICDISER.ICDISER1 = ICDISER.ICDISER1 | (1 << 26);


	/*为58号中断选择cpu0*/

	ICDIPTR.ICDIPTR14 = ICDIPTR.ICDIPTR14 & (~(0xFF << 16)) | (0x1 << 16);

	/*使能GIC与CPU0接口*/

	CPU0.ICCICR = CPU0.ICCICR | 1;
}


/*异常处理程序*/

void do_irq()

{
	unsigned irq_num = 0;

	static int count = 0;

	irq_num = CPU0.ICCIAR & (0x3FF);

	if(58 == irq_num)

	{
		count = ~ count;

		if(count)
		{
			GPX2.DAT = GPX2.DAT | (1 << 7);
		}

		else
		{
			GPX2.DAT = GPX2.DAT & (~(1 << 7));
		}

	//	printf("%x\n",EXT_INT41_PEND);

		EXT_INT41_PEND |= (1 << 2);

		CPU0.ICCEOIR = CPU0.ICCEOIR & (~(0x3FF)) | (58);
	}
}
#endif

作业存在几个问题:

1.第一次按led2有时不亮

2.按键未作消抖处理,亮灭有时不稳定

ADC

ADC简介:

ADC ADC(Analog to Digital Converter)即模数转换器,指一个能将模拟信号转化为数字信号的电子元件

分辨率 ADC的分辨率一般以==输出二进制数的位数==来表示,当最大输入电压一定时,位数越高,分辨率越高; n位的ADC能区分输入电压的最小值为满量程输入的1/2^n; 比如一个12位的ADC,最大输入电压为1.8v,那么该ADC能区分的最小电压为==1.8v/2^12≈0.00044v==,当转换的结果为m时,则 实际的电压值为m*(1.8v/2^12);

即0~1.8v对应0-(2^12-1),一个单位数据对应1.8/2的12次方

Exynos_4412下的ADC控制器:

电气原理图:

以VR1丝印,先找到电位器,电位器中间连接导线是XadcAIN3标号,再通过它找到核心板上的引脚,可见与XadcAIN3相连

ADC专用引脚:

可以看到这四个引脚不像之前学的引脚存在复用的情况,它只有模拟量输入的功能,说明是ADC专属引脚

分时复用ADC:虽然有四个模拟输入通道,但是同一时刻只能转换一个引脚上的模拟信号

ADC概述:

The 10-bit or 12-bit CMOS Analog to Digital Converter (ADC) comprises of 4-channel analog inputs. It converts the analog input signal into 10-bit or 12-bit binary digital codes at a maximum conversion rate of 1MSPS with 5MHz A/D converter clock. A/D converter operates with on-chip sample-and-hold function. ADC supports low power mode.

10位或12位CMOS模数转换器(ADC)由==4通道模拟输入==组成。 它将模拟输入信号以==1MSPS的最大转换速率==转换为10位或12位二进制数字代码 , 使用的是5MHz A/D转换器时钟。 A/D转换器具有片上采样和保持功能。 ADC支持低功耗模式。

电压范围是==0~1.8V==

ADC寄存器详解:

ADCCON寄存器(只用低17位):

==注意:==

1.PCLK:100MHZ,需要降频,因此分频器必须打开,通过PRSCEN位使能

2.设置分频值注意不要超过最大转换频率1MHZ,分频值19~255;

如果将[13:6]位设置为19,则ADC时钟频率为100/(19+1) =5MHZ,则ADC转换频率为5/5=1MHZ

即设置为19时可得到最大转换频率1MHZ,最低就是19,不能再低了

3.待机模式要关闭(正常模式打开),因为待机模式下有些功能不可用,为了降低功耗

4.读触发位[1]:

ADC寄存器转换完成之后,会放到ADCDAT寄存器,当我们从该寄存器读取数据后,ADC会自动触发下一次转换。

同时要注意:假如该位打开,那么使能触发位会失效

5.使能触发[0]:

写1开始转换,且转换开始后该位自动清零

6.转换结束标志[15]:转换完成该位自动置1

ADCDAT寄存器:

ADCDAT:注意读完之后,将高20位清零,因为高20位可能存在随机值,对结果产生影响

ADCMUX寄存器:

我们使用3通道

程序:

#include "exynos_4412.h"

void ADC_Init();

int main()

{
	unsigned int AdcValue = 0;

	ADC_Init();

	while(1)

	{
		/*触发ADC转换*/

		ADCCON = ADCCON |(1 << 0);


		/*等待转换结束*/

		while(!(ADCCON & (1 << 15)));


		/*读取ADC转换数据*/

		AdcValue = ADCDAT & (0xFFF);


		/*转换成实际的电压值 mv*/

		AdcValue = AdcValue * 0.44;
      

		/*打印转换结果*/

		printf("AdcValue=%dmv\n",AdcValue);

	}

	return 0;

}

void ADC_Init()

{
	/*选择ADC转换精度为12bit*/

	ADCCON = ADCCON | (1 << 16);


	/*使能ADC转换分频器*/

	ADCCON = ADCCON | (1 << 14);


	/*设置分频值为19,ADC时钟频率=PCLK/(19+1)=5MHZ,ADC转换频率=5/5MHZ=1MHZ*/

	ADCCON = ADCCON & (~(0xFF << 6)) | (19 << 6);


	/*关闭待机模式,使能正常模式*/

	ADCCON = ADCCON & (~(1 << 2));


	/*关闭读触发ADC转换*/

	ADCCON = ADCCON & (~(1 << 1));



	/*选择ADC转换通道为3通道*/

	ADCMUX = 3;
}

注意:最后再配置使能触发位,等待转换结束后再从ADCDAT寄存器读取,读取后还要进行实际电压值的转换

作业:

1.编程实现通过LED状态显示当前电压范围

注:

电压在1501mv~1800mv时,LED2、LED3、LED4、LED5点亮 电压在1001mv~1500mv时,LED2、LED3、LED4点亮 电压在501mv~1000mv时,LED2、LED3点亮 电压在0mv~500mv时,LED2闪烁

代码:

#if 1

void ADC_Init();
void GPIO_Init();
void delay(unsigned int );

int main()
{
	unsigned int AdcValue = 0;
	unsigned int n = 0;
	ADC_Init();
	GPIO_Init();

	while(1)
	{
		/*触发ADC转换*/
		ADCCON = ADCCON |(1 << 0);

		/*等待转换结束*/
		while(!(ADCCON & (1 << 15)));

		/*读取ADC转换数据*/
		AdcValue = ADCDAT & (0xFFF);

		/*转换成实际的电压值 mv*/
		AdcValue = AdcValue * 0.44;

		printf("AdcValue = %dmv\n",AdcValue);

		n = AdcValue/500;

		switch(n)
		{
		case 0:

				GPX2.DAT = GPX2.DAT  | (1 << 7);
				delay(1000000);

				GPX2.DAT = GPX2.DAT & (~(1 << 7));
				delay(1000000);

				GPX1.DAT = GPX1.DAT & (~(1 << 0));
				GPF3.DAT = GPF3.DAT & (~(1 << 4));
				GPF3.DAT = GPF3.DAT & (~(1 << 5));

				break;

		case 1:

				GPX2.DAT = GPX2.DAT |(1 << 7);
				GPX1.DAT = GPX1.DAT |(1 << 0);
				GPF3.DAT = GPF3.DAT & (~(1 << 4));
				GPF3.DAT = GPF3.DAT & (~(1 << 5));

				break;

		case 2:

				GPX2.DAT = GPX2.DAT |(1 << 7);
				GPX1.DAT = GPX1.DAT |(1 << 0);
				GPF3.DAT = GPF3.DAT |(1 << 4);
				GPF3.DAT = GPF3.DAT & (~(1 << 5));

				break;

		case 3:

				GPX2.DAT = GPX2.DAT |(1 << 7);
				GPX1.DAT = GPX1.DAT |(1 << 0);
				GPF3.DAT = GPF3.DAT |(1 << 4);
				GPF3.DAT = GPF3.DAT |(1 << 5);

				break;
		}
	}

	return 0;
}

void ADC_Init()
{
	/*选择ADC转换精度为12bit*/
	ADCCON = ADCCON | (1 << 16);

	/*使能ADC转换分频器*/
	ADCCON = ADCCON | (1 << 14);

	/*设置分频值为19,ADC时钟频率=PCLK/(19+1)=5MHZ,ADC转换频率=5/5MHZ=1MHZ*/
	ADCCON = ADCCON & (~(0xFF << 6)) | (19 << 6);

	/*关闭待机模式,使能正常模式*/
	ADCCON = ADCCON & (~(1 << 2));

	/*关闭读触发ADC转换*/
	ADCCON = ADCCON & (~(1 << 1));

	/*选择ADC转换通道为3通道*/
	ADCMUX = 3;
}

void GPIO_Init()
{
	GPX2.CON = GPX2.CON & (~(0xF << 28)) | (1 << 28);
	GPX1.CON = GPX1.CON & (~(0xF)) | (1 << 0);
	GPF3.CON = GPF3.CON & (~(0xF << 16)) | (1 << 16);
	GPF3.CON = GPF3.CON & (~(0xF << 20)) | (1 << 20);
}

void delay(unsigned int time)
{
	while(time --);
}

#endif

作业中存在几个问题:

1.每一次触发ADC转换都要等到led2闪烁的延时之后才会发生,这样的话,会影响到转换,但同时也有优点,会降低功耗。当然在本实验中,延时时间要合适的话,就不会有太大影响

2.使用switch...case 无法精确定位到边界值,比如题目要求500mv边界上led2是闪烁状态,但使用switch..case判断此时n=1,led2亮、led3亮

RTC实验:

RTC简介:

RTC(Real Time Clock)即实时时钟,它是一个可以为系统提供精确的时间基准的元器件,RTC一般采用精度较高的晶振作为时钟源,有些RTC为了在主电源掉电时还可以工作,需要外加电池供电

RTC工作需要提供一个高精度晶振,是独立的时钟源,不与其它硬件共用,晶振频率为32.768kHZ

BCD码用四位二进制数表示一位十进制数

C语言只支持三种进制:十进制、十六进制、八进制

==三星手册里R写的地址:DAY与WEEK地址写反了==,用的时候反着用就行了

Exynos_4412下的RTC:

RTC概述:

Real Time Clock (RTC) unit can operate using the backup battery while the system power is off. Although power is off, backup battery can store the time by Second, Minute, Hour, Day of the week, Day, Month, and Year data. The RTC unit works with an external ==32.768== kHz crystal and performs the function of alarm.

RTC工作逻辑:

需要注意的是,32.768KHZ时钟源经过分频器分频之后,得到1HZ的频率,该频率主要用于秒的计数

RTC寄存器:

RTCCON:

此次实验只用低0位,用于RTC的控制,如果要修改时间需要将该位置为1,使能RTC控制,修改完后需要再置为0,防止误操作。它更象一把锁,用于安全管理,防止随意篡改或误操作

存储时间的寄存器:

用于存储年月日时分秒周信息,注意BCDDAY与BCDWEEK地址反了,用的时候要反着用

程序:

#include "exynos_4412.h"

#if 1
/*作业*/

void GPIO_Init();
void ADC_Init();
void delay(unsigned int time);

int main()
{
	unsigned int AdcValue = 0;
	unsigned int n = 0;
	unsigned int last = 0;
	GPIO_Init();
	ADC_Init();

	/*使能RTC控制位*/
	RTCCON |= (1 << 0);

	RTC.BCDYEAR = 0x022;
	RTC.BCDMON  = 0x12;
	RTC.BCDWEEK = 0x31;
	RTC.BCDHOUR = 0x23;
	RTC.BCDMIN  = 0x59;
	RTC.BCDSEC  = 0x50;
	RTC.BCDDAY  = 0x7;

	/*禁止RTC控制位*/
	RTCCON &= (1 << 0);

	while(1)
	{
		/*使能触发ADC转换*/
		ADCCON = ADCCON | 1;

		/*等待转换结束*/
		while(!(ADCCON & (1 << 15)));

		/*获取电压值*/
		AdcValue = ADCDAT & (0xFFF);
		AdcValue = AdcValue * 0.44;

		n = AdcValue / 500;

		switch(n)
		{
		case 0:

				GPX2.DAT |= (1 << 7);
				delay(100000);

				GPX2.DAT &= (~(1 << 7));
				delay(100000);

				GPX1.DAT &= (~(1 << 0));
				GPF3.DAT &= (~(1 << 4));
				GPF3.DAT &= (~(1 << 5));

				if(RTC.BCDSEC != last)
				{
					last = RTC.BCDSEC;
					printf("电压值:%d\t20%x-%x-%x %x:%x:%x 星期:%x\n",AdcValue,RTC.BCDYEAR,RTC.BCDMON,RTC.BCDWEEK,RTC.BCDHOUR,RTC.BCDMIN,RTC.BCDSEC,RTC.BCDDAY);
				}

			break;

		case 1:

			GPX2.DAT |= (1 << 7);
			GPX1.DAT |= (1 << 0);
			GPF3.DAT &= (~(1 << 4));
			GPF3.DAT &= (~(1 << 5));

			break;

		case 2:

			GPX2.DAT |= (1 << 7);
			GPX1.DAT |= (1 << 0);
			GPF3.DAT |= (1 << 4);
			GPF3.DAT &= (~(1 << 5));

			break;

		case 3:

			GPX2.DAT |= (1 << 7);
			GPX1.DAT |= (1 << 0);
			GPF3.DAT |= (1 << 4);
			GPF3.DAT |= (1 << 5);

			break;
		}
	}
	return 0;
}

void GPIO_Init()
{
	/*配置引脚为输出功能*/

	GPX2.CON = GPX2.CON & (~(0xF << 28)) | (1 << 28);
	GPX1.CON = GPX1.CON & (~(0xF)) | (1 << 0);
	GPF3.CON = GPF3.CON & (~(0xF << 16)) | (1 << 16);
	GPF3.CON = GPF3.CON & (~(0xF << 20)) | (1 << 20);
}

void ADC_Init()
{
	/*1.选择ADC转换精度:12bit*/
	ADCCON |= (1 << 16);

	/*2.使能分频器*/
	ADCCON |= (1 << 14);

	/*3.关闭待机模式,使能正常模式*/
	ADCCON &= (~(1 << 2));

	/*4.设置分频值为19,ADC转换频率=1MHZ*/
	ADCCON = ADCCON & (~(0xFF << 6)) | (19 << 6);

	/*5.选择转换通道为3通道*/
	ADCMUX = 3;

	/*关闭读触发*/
	ADCCON &= (~(1 << 1));
}

void delay(unsigned int time)
{
	while(time --);
}

#endif






#if 0

/*课堂代码*/
int main()
{
	/*使能RTC控制*/	
	RTCCON = RTCCON | 1;

	/*修正时间信息*/
	RTC.BCDYEAR = 0x023;
	RTC.BCDMON  = 0x12;
	RTC.BCDDAY  = 0x7;
	RTC.BCDWEEK = 0x31;
	RTC.BCDHOUR = 0x23;
	RTC.BCDMIN  = 0x59;
	RTC.BCDSEC  = 0x50;

	/*禁止RTC控制*/
	RTCCON = RTCCON & (~(1 << 0));

	unsigned int last = 0;
	while(1)
	{
		if(RTC.BCDSEC != last)
		{
			last = RTC.BCDSEC;
			printf("date:20%x-%x-%x %x:%x:%x week-day:%x\n",
					RTC.BCDYEAR,RTC.BCDMON,RTC.BCDWEEK,RTC.BCDHOUR,RTC.BCDMIN,RTC.BCDSEC,RTC.BCDDAY);
		}
	}

	return 0;
}


#endif

#if 0 

/*课前DIY原始代码*/
int main()
{

	RTCCON = RTCCON | 1;

	RTC.BCDYEAR = RTC.BCDYEAR & (~(0xFFF)) | (0x022);
	RTC.BCDMON = RTC.BCDMON & (~(0x1F)) | (0x12);
	RTC.BCDDAY = RTC.BCDDAY & (~(0x7)) | (0x7);
	RTC.BCDHOUR = RTC.BCDHOUR & (~(0x3F)) | (0x12);
	RTC.BCDMIN = RTC.BCDMIN & (~(0x7F)) | (0x59);
	RTC.BCDSEC = RTC.BCDSEC & (~(0x7F)) | (0x10);
	RTC.BCDWEEK = RTC.BCDWEEK & (~(0x3F)) | (0x25);

	RTCCON = RTCCON | 0;

	//	printf("date:%3d:%2d:%2d %2d:%2d:%2d week-day:%d\n",
	//			RTC.BCDYEAR,RTC.BCDMON,RTC.BCDDAY,RTC.BCDHOUR,RTC.BCDMIN,RTC.BCDSEC,RTC.BCDWEEK);

	while(1)
	{
		printf("date:20%x:%x:%x %x:%x:%x week-day:%x\n",
				RTC.BCDYEAR & (0xFFFF),RTC.BCDMON & (0x1F),RTC.BCDWEEK & (0x3F),
				RTC.BCDHOUR & (0x3F),RTC.BCDMIN & (0x7F),RTC.BCDSEC & (0x7F),RTC.BCDDAY & (0x7));
	}

	return 0;
}

#endif

调试的时候遇到过以下问题:

1.旋动电位器时电压几乎不变,始终停留在200多,经过对比课堂源码,ADC_Init()设置分频值的时候,19应该左移6位,忘记写|了,19会覆盖低位数据出现错误,调整后正常显示

2.代码中不使用延时就可实现每隔一秒就打印一下日期,主要思路是每一循环都和上一秒的秒值比较,如果没发生变化,说明还停留在上一秒,就不执行打印语句,如果发生了变化,就打印日期

3.代码中,BCD码和十进制的转换还可以使用转换函数,这里不予以说明,采用替代方法,使用十六进制去代替BCD码的显示

PWM实验:

PWM简介:

有源蜂鸣器 有源蜂鸣器只要接上额定电源就可以发出声音 无源蜂鸣器 无源蜂鸣器利用电磁感应原理,为音圈接入交变电流后形成的电磁铁与永磁铁相吸或相斥而推动振膜发声

方式一:可以使用GPIO控制

使用GPIO硬件控制器不断交替输出高低电平,进行控制,但是延时函数会消耗cpu资源

方式二:PWM控制

PWM PWM(Pulse Width Modulation)即脉冲宽度调制,通过对脉冲的宽度进行调制,来获得所需要波形

PWM参数:

Exynos_4412下的PWM:

电气原理图:

蜂鸣器一端连接到正极,一端连接到三极管的集电极,当连接的GPD0_0这条导线上为高电平时,三极管导通,蜂鸣器内部的振膜被吸合,当导线上为低电平时,三极管截止,蜂鸣器振膜释放。如果导线上来的是连续的脉冲信号时,振膜会不断震动,声音频率在20~20000HZ之间时可被人耳听到

前面讲过引脚的功能设置与引脚复用,可以通过GPIO的GPD0CON寄存器配置GPD0_0引脚为PWM输出功能,内部实质是GPD0_0引脚与PWM控制器相连接

PWM概述:

大意是4412芯片上集成了5个32位的PWM控制器,本质是递减计数器,其中PWM0、1、2、3有输出功能,4没有输出功能,且对于PWM0,支持死区功能;使用的原始时钟是100MHZ==APB-PCLK==,其中PWM0、PWM1共用一个一级分频器,PWM2、3、4共用另一个一级分频器,同时所有的PWM都有私有的二级分频器

PWM0死区功能:==驱动大电流设备,保护PWM不受损坏==

APB-PCLK作为时钟源(100MHZ),timer0与timer1共享PCLK 8位分频器(一级分频),timer2,timer3,timer4共用另外的一级分频器

周期和什么有关系?

==和TCONB的值、递减频率有关 ,参考t=s/v==

递减频率是PCLK经过一级分频和二级分频后的频率,供递减计数器使用

PWM工作逻辑:

第一次需要我们手动更新递减计数器的值为TCNTB0中的值,因为刚开始PWM并不工作,不会自动加载TCNTB0中的值到递减计数器

每次减到0都会自动装载TCNTB0的值到递减计数器

过程中会不断比较TCNTB与TCMPB的值,相等时进行低电平到高电平的反转

PWM时钟树:

PWM寄存器详解:

TCFG0:设置一级分频

TCFG1:设置二级分频

TCON:控制寄存器,控制PWM细节的实现:

TCNTB0:设置周期值:

TCMPB0:设置高电平值(占空比):

程序:

#include "exynos_4412.h"

void delay(unsigned int time)
{
	while(time--);
}

int main()
{
	/*1.配置GPD0_0为PWM输出功能*/
	GPD0.CON = GPD0.CON & (~(0xF)) | (0x2);

	/*2.设置一级分频为100倍分频*/
	PWM.TCFG0 = PWM.TCFG0 & (~(0xFF)) | (99);

	/*3.设置二级分频为1倍分频,PWM递减频率=PCLK/(99+1)/1=1MHZ*/
	PWM.TCFG1 = PWM.TCFG1 & (0xF);

	/*4.设置为自动重装载*/
	PWM.TCON = PWM.TCON | (1 << 3);

#if 0
	/*5.设置PWM的频率为500HZ*/
	PWM.TCNTB0 = 2000;

	/*6.设置占空比为50%*/
	PWM.TCMPB0 = 1000;
#endif
	
	/*5.设置PWM的频率为1000HZ*/
	PWM.TCNTB0 = 1000;

	/*6.设置占空比为60%*/
	PWM.TCMPB0 = 600;

	/*7.将TCNTB0的值手动装载到递减计数器*/
	PWM.TCON = PWM.TCON | (1 << 1);

	/*8.关闭手动装载*/
	PWM.TCON = PWM.TCON & (~(1 << 1));

	/*9.使能递减计数器,递减计数器开始递减*/
	PWM.TCON = PWM.TCON | (1 << 0);

	while(1)
	{
		PWM.TCON = PWM.TCON | (1 << 0);
		delay(1000000);

		PWM.TCON = PWM.TCON & (~(1 << 0));
		delay(1000000);
	}

	return 0;
}

注意:

1.手动更新后还要关闭手动装载,否则会使自动装载失效

2.计算TCNTB0的周期值的时候,使用公式t=s/v

IIC总线原理:

关键点记录:

第一个字节低0位确定好后续字节的发送方向后,后续发送数据的双方发送数据方向不会改变

起始信号与结束信号之间双方可以发送任意多个字节数据,且第一个字节一定是主机发给从机,相比之下UART只能发送一个字节数据

IIC发送数据先发高位、再发低位,而UART相反,且IIC发送的数据必须为8位即一个字节,UART可发5、6、7、8位数据

==一位应答位,低电平应答、高电平非应答==

SCL时钟线是告诉发送器什么时候发数据、接收器什么时候接收数据的,因为发送器和接收器共用这根线

==为什么IIC可以发任意多个字节?==

因为发送器与接收器用的是同一个时间基准(SCL线),可以发任意多个字节的数据没有误差,是同步通信,而UART是异步通信,如果通信数据发送多个字节,可能会有累计误差,影响数据的准确性

主机发送停止信号有两种请况,其一主机不想发送了,其二从机不想接收了(非应答),同时注意:==起始信号、停止信号必须由主机发送==

IIC总线简介:

IIC总线是Philips公司在八十年代初推出的一种串行、半双工总线 主要用于近距离、低速的芯片之间的通信;IIC总线有两根双向的信号线一根数据线SDA用于收发数据,一根时钟线SCL用于通信双方时钟的同步;IIC总线硬件结构简单,成本较低,因此在各个领域得到了广泛的应用

IIC总线 IIC总线是一种多主机总线,连接在IIC总线上的器件分为主机和从机,主机有权发起和结束一次通信,而从机只能被主机呼叫;当总线上有多个主机同时启用总线时,IIC也具备冲突检测和仲裁的功能来防止错误产生; 每个连接到IIC总线上的器件都有一个唯一的地址(7bit),且每个器件都可以作为主机也可以作为从机(同一时刻只能有一个主机),总线上的器件增加和删除不影响其他器件正常工作;IIC总线在通信时总线上发送数据的器件为发送器,接收数据的器件为接收器;

当多主机会产生总线裁决问题。当多个主机同时想占用总线时,企图启动总线传输数据,就叫做总线竞争。I2C通过总线仲裁,以决定哪台主机控制总线

IIC通信过程:

1.主机发送==起始信号==启用总线 2.主机发送==一个字节数据==指明==从机地址和后续字节的传送方向== 3.被寻址的从机发送==应答信号==回应主机 4.发送器发送一个字节数据 5.接收器发送应答信号回应发送器 … … (循环步骤4、5) n.通信完成后主机发送==停止信号==释放总线

IIC总线寻址方式:

IIC总线上传送的数据是广义的,既包括地址,又包括真正的数据 主机在发送起始信号后必须先发送一个字节的数据,该数据的高7位为从机地址,最低位表示后续字节的传送方向,'0'表示主机发送数据,'1'表示主机接收数据;总线上所有的从机接收到该字节数据后都将这7位地址与自己的地址进行比较,如果相同,则认为自己被主机寻址,然后再根据第8位将自己定为发送器或接收器

为了让数据精准到达(而不是广播的形式发送),我们给IIC总线上的每一个设备都给一个唯一的地址,这个地址就是*设备地址*,用来区分不同的IIC设备的。

IIC信号的实现:

起始信号和停止信号

==SCL为高电平时,SDA由高变低表示起始信号== ==SCL为高电平时,SDA由低变高表示停止信号== 起始信号和停止信号都是由==主机发出==,起始信号产生后总线处于占用状态 停止信号产生后总线处于空闲状态

字节传送与应答:

IIC总线通信时每个字节为8位长度,数据传送时,==先传送高位,后传送低位==,发送器发送完一个字节数据后接收器必须发送==1位应答位==来回应发送器即一帧共有9位

应答:是一个低电平信号,即拉低回应。

非应答:是一个高电平信号,也许,叫做应答非更合适。

每发送一个字节数据,主设备释放SDA 线,转移SDA的控制权给从机,等待从机的应答信号(ACK)

为什么可以应答?

因为IIC是半双工通信协议,比如在主机发送一字节数据给从机后,从机可以发送应答位给主机,此时就应答位而言接收方是主机,发送方是从机,但一般我们说的发送器和接收器是对于传输数据而言的

同步信号:

IIC总线在进行数据传送时,时钟线SCL为低电平期间发送器向数据线上发送一位数据,在此期间数据线上的信号允许发生变化,时钟线SCL为高电平期间接收器从数据线上读取一位数据,在此期间数据线上的信号不允许发生变化,必须保持稳定(起始信号与停止信号除外)

SCL时钟线是告诉发送器什么时候发数据、接收器什么时候接收数据的,因为发送器和接收器共用这根线

SCL低电平时发送器往SDA线上放数据位,此时SDA数据线上允许数据发生变化;SCL高电平时接收器从SDA线上读取数据,此时SDA线上的数据不允许变化

实际使用中,会有很多的设备挂载到SCL、SDA线上,主机与从机通信时,==SCL提供时间节拍,保持通信的同步==,这也是它为什么可以发送多个字节数据的原因

同步:约定好发送数据只能在时钟的低跳变时,接收(采样)数据只能在时钟的高跳变时

典型IIC时序:

注:阴影部分表示数据由主机向从机传送,无阴影部分则表示数据由从机向主机传送;A表示应答, A非表示非应答,S表示起始信号,P表示终止信号

主机向从机发送数据:

发送停止信号有两种情况:

1.发送到最后一个数据时,主机不想发送了

2.发送到某一个数据时,从机不想接收后续的数据了,此时从机不应答,主机没有接收到应答信号,认为没有接收到数据,将不再发送,发送停止信号结束。但注意此时==该数据已被接收到,只是后续的数据不再接收罢了==

从机向主机发送数据:

注意第一个字节数据读写位为1,最后主机不想接收后续数据了,就先发一个非应答信号,再发送一个停止信号来结束此次通信

主机先向从机发送数据,然后从机再向主机发送数据:

当发送数据过程中,主机不想再发送或者从机不想再接收数据了,主机会直接发送一个起始信号,开始第二次通信,此次传输数据的方向可以改变了

注意:主机是直接发送的起始信号,并没有先发停止信号,再开始发送起始信号开始第二次通信,因为如果发送停止信号的话,SDA线可能会被其他的主机占用,影响第二次通信

从IIC实测波形入手,搞懂IIC通信 - 知乎 (zhihu.com)

作业:

若使用IIC总线让从机给主机发送一个字节的数据0xA2,画出SCL和SDA上的时序图

注:从机地址为0x63

http://static.makeru.com.cn/upload/mavendemo/course/20221227/1672129313141754492.jpg

IIC控制器与MPU6050:

Exynos4412下的IIC控制器:

电气原理图:

标号为I2C_SDA5与I2C_SCL5的线分别连接到4412上的GPB_2与GPB_3引脚上,使用前==需要将引脚配置为I2C功能==

Exynos_4412下的IIC控制器支持 master transmit, master receive, slave transmit, and slave receive四种模式

支持中断和轮询模式:

IIC中断挂起:通知cpu数据已经发送出去或者接收到新的数据,IIC寄存器中有对应的中断挂起标志位,当数据发送完成或接收到数据时,该位自动置为1

IIC工作逻辑:

将需要发送的数据写入到I2CDS寄存器中,移位寄存器会自动发送出去;

接收到的数据也会放到移位寄存器里我们需要是可以直接读取

如果4412作为从机时,会被其他主机寻址,这样的话,将接收到的数据中的地址提取出来放到比较器,与地址寄存器(可以配置4412的地址)的地址值进行比较,判断自己是否被寻址

IIC寄存器详解:

I2CCON:

第4位是中断挂起标志位,当发送数据完成或者接收到数据时,该位自动置为1

第5位是中断使能位,0关闭,1打开

第6位是时钟选择位,可选分频16/512分频

第7位一般作为从机使用,当然如果4412作为主机接收到从机的数据后,也可以应答,只不过大多数情况下是作为从机使用时,该位才会写为1

IICSTAT:

第4位:发送接收功能的开关

第5位:发送起始信号与停止信号

第6、7位:配置主机的工作模式

IICDS:

用于发送接收数据

MPU6050原理:

MPU6050是一个运动处理传感器,其内部集成了3轴加速度传感器

和3轴陀螺仪(角速度传感器),以及一个可扩展数字运动处理器

MPU6050工作参数:

可测量X、Y、Z轴三个方向的角速度

可编程设置角速度测量范围为±250、±500、±1000、±2000°/sec

可测量X、Y、Z轴三个方向的加速度

可编程设置加速度测量范围为±2g、±4g、±8g、±16g

可编程设置低功耗模式

可编程设置采样频率

MPU6050通信接口:

MPU6050可以使用IIC总线和其他器件进行数据交互,我们可以使用IIC总线向MPU6050中的控制寄存器写入数据来设置MPU6050的工作参数

也可以使用IIC总线从MPU6050中的数据寄存器读取数据来获取加速度、角速度等信息

实验中用到的MPU6050寄存器:

/****************MPU6050内部常用寄存器地址****************/

#define	SMPLRT_DIV		0x19	//陀螺仪采样率,典型值:0x07(125Hz)
#define	CONFIG			0x1A	//低通滤波频率,典型值:0x06(5Hz)
#define	GYRO_CONFIG		0x1B	//陀螺仪自检及测量范围,典型值:0x18(不自检,2000°/s)
#define	ACCEL_CONFIG	0x1C	//加速计自检及测量范围及高通滤波频率,典型值:0x0(不自检,2G,5Hz)
#define	ACCEL_XOUT_H	0x3B
#define	ACCEL_XOUT_L	0x3C
#define	ACCEL_YOUT_H	0x3D
#define	ACCEL_YOUT_L	0x3E
#define	ACCEL_ZOUT_H	0x3F
#define	ACCEL_ZOUT_L	0x40
#define	TEMP_OUT_H		0x41
#define	TEMP_OUT_L		0x42
#define	GYRO_XOUT_H		0x43
#define	GYRO_XOUT_L		0x44
#define	GYRO_YOUT_H		0x45
#define	GYRO_YOUT_L		0x46
#define	GYRO_ZOUT_H		0x47
#define	GYRO_ZOUT_L		0x48
#define	PWR_MGMT_1		0x6B	//电源管理,典型值:0x00(正常启用)
#define	SlaveAddress	0x68	//MPU6050-I2C地址

ADO=0/1:设置I2C地址,MPU6050芯片ADO引脚接法可选,可以避免与其他芯片地址冲突

MPU6050读写时序:

向MPU6050的一个寄存器写一个字节的数据:

1.主机(Exynos4412)发送起始信号 2.主机发送从机地址(MPU6050的地址)及读写方向(写) 3.从机(MPU6050)发送应答信号 4.主机发送一个字节数据(要写的寄存器的地址) 5.从机发送应答信号 6.主机发送一个字节数据(要写到寄存器的数据) 7.从机发送应答信号8.主机发送停止信号

从MPU6050的一个寄存器读一个字节的数据:

1.主机(Exynos4412)发送起始信号 2.主机发送从机地址(MPU6050的地址)及读写方向(写) 3.从机(MPU6050)发送应答信号 4.主机发送一个字节数据(要写的寄存器的地址) 5.从机发送应答信号 6.主机(Exynos4412)发送起始信号 7.主机发送从机地址(MPU6050的地址)及读写方向(读)

8.从机(MPU6050)发送应答信号 9.从机发送一个字节数据(要读的寄存器中的数据) 10.主机发送非应答信号(不再接收更多的数据) 11.主机发送停止信号

陀螺仪实验代码:

#include "exynos_4412.h"

/****************MPU6050内部寄存器地址****************/

#define	SMPLRT_DIV		0x19	//陀螺仪采样率,典型值:0x07(125Hz)
#define	CONFIG			  0x1A	//低通滤波频率,典型值:0x06(5Hz)
#define	GYRO_CONFIG		0x1B	//陀螺仪自检及测量范围,典型值:0x18(不自检,2000deg/s)
#define	ACCEL_CONFIG	0x1C	//加速计自检、测量范围及高通滤波频率,典型值:0x00(不自检,2G,5Hz)
#define	ACCEL_XOUT_H	0x3B
#define	ACCEL_XOUT_L	0x3C
#define	ACCEL_YOUT_H	0x3D
#define	ACCEL_YOUT_L	0x3E
#define	ACCEL_ZOUT_H	0x3F
#define	ACCEL_ZOUT_L	0x40
#define	TEMP_OUT_H		0x41
#define	TEMP_OUT_L		0x42
#define	GYRO_XOUT_H		0x43
#define	GYRO_XOUT_L		0x44
#define	GYRO_YOUT_H		0x45
#define	GYRO_YOUT_L		0x46
#define	GYRO_ZOUT_H		0x47
#define	GYRO_ZOUT_L		0x48
#define	PWR_MGMT_1		0x6B	//电源管理,典型值:0x00(正常启用)
#define	WHO_AM_I		  0x75	//IIC地址寄存器(默认数值0x68,只读)
#define	SlaveAddress	0x68	//MPU6050-I2C地址

/************************延时函数************************/

void mydelay_ms(int time)
{
	int i,j;
	while(time--)
	{
		for(i=0;i<5;i++)
			for(j=0;j<514;j++);
	}
}

/**********************************************************************
 * 函数功能:I2C向特定地址写一个字节
 * 输入参数:
 * 		slave_addr: I2C从机地址
 * 			  addr: 芯片内部特定地址
 * 			  data:写入的数据
**********************************************************************/

void iic_write (unsigned char slave_addr, unsigned char addr, unsigned char data)
{
	/*对时钟源进行512倍预分频  打开IIC中断(每次完成一个字节的收发后中断标志位会自动置位)*/
	I2C5.I2CCON = I2C5.I2CCON | (1<<6) | (1<<5);

	/*设置IIC模式为主机发送模式  使能IIC发送和接收*/
	I2C5.I2CSTAT = 0xd0;
	/*将第一个字节的数据写入发送寄存器  即从机地址和读写位(MPU6050-I2C地址+写位0)*/
	I2C5.I2CDS = slave_addr<<1;
	/*设置IIC模式为主机发送模式  发送起始信号启用总线  使能IIC发送和接收*/
	I2C5.I2CSTAT = 0xf0;

	/*等待从机接受完一个字节后产生应答信号(应答后中断挂起位自动置位)*/
	while(!(I2C5.I2CCON & (1<<4)));

	/*将要发送的第二个字节数据(即MPU6050内部寄存器的地址)写入发送寄存器*/
	I2C5.I2CDS = addr;
	/*清除中断挂起标志位  开始下一个字节的发送*/
	I2C5.I2CCON = I2C5.I2CCON & (~(1<<4));
	/*等待从机接受完一个字节后产生应答信号(应答后中断挂起位自动置位)*/
	while(!(I2C5.I2CCON & (1<<4)));

	/*将要发送的第三个字节数据(即要写入到MPU6050内部指定的寄存器中的数据)写入发送寄存器*/
	I2C5.I2CDS = data;
	/*清除中断挂起标志位  开始下一个字节的发送*/
	I2C5.I2CCON = I2C5.I2CCON & (~(1<<4));
	/*等待从机接受完一个字节后产生应答信号(应答后中断挂起位自动置位)*/
	while(!(I2C5.I2CCON & (1<<4)));

	/*发送停止信号  结束本次通信*/
	I2C5.I2CSTAT = 0xD0;
	/*清除中断挂起标志位*/
	I2C5.I2CCON = I2C5.I2CCON & (~(1<<4));
	/*延时*/
	mydelay_ms(10);
}

/**********************************************************************
 * 函数功能:I2C从特定地址读取1个字节的数据
 * 输入参数:         slave_addr: I2C从机地址
 * 			       addr: 芯片内部特定地址
 * 返回参数: unsigned char: 读取的数值
**********************************************************************/

unsigned char iic_read(unsigned char slave_addr, unsigned char addr)
{

	unsigned char data = 0;

	/*对时钟源进行512倍预分频  打开IIC中断(每次完成一个字节的收发后中断标志位会自动置位)*/
	I2C5.I2CCON = I2C5.I2CCON | (1<<6) | (1<<5);

	/*设置IIC模式为主机发送模式  使能IIC发送和接收*/
	I2C5.I2CSTAT = 0xd0;
	/*将第一个字节的数据写入发送寄存器  即从机地址和读写位(MPU6050-I2C地址+写位0)*/
	I2C5.I2CDS = slave_addr<<1;
	/*设置IIC模式为主机发送模式  发送起始信号启用总线  使能IIC发送和接收*/
	I2C5.I2CSTAT = 0xf0;
	/*等待从机接受完一个字节后产生应答信号(应答后中断挂起位自动置位)*/
	while(!(I2C5.I2CCON & (1<<4)));

	/*将要发送的第二个字节数据(即要读取的MPU6050内部寄存器的地址)写入发送寄存器*/
	I2C5.I2CDS = addr;
	/*清除中断挂起标志位  开始下一个字节的发送*/
	I2C5.I2CCON = I2C5.I2CCON & (~(1<<4));
	/*等待从机接受完一个字节后产生应答信号(应答后中断挂起位自动置位)*/
	while(!(I2C5.I2CCON & (1<<4)));

	/*清除中断挂起标志位  重新开始一次通信  改变数据传送方向*/
	I2C5.I2CCON = I2C5.I2CCON & (~(1<<4));

	/*将第一个字节的数据写入发送寄存器  即从机地址和读写位(MPU6050-I2C地址+读位1)*/
	I2C5.I2CDS = slave_addr << 1 | 0x01;
	/*设置IIC为主机接收模式  发送起始信号  使能IIC收发*/
	I2C5.I2CSTAT = 0xb0;
	/*等待从机接收到数据后应答*/
	while(!(I2C5.I2CCON & (1<<4)));


	/*禁止主机应答信号(即开启非应答  因为只接收一个字节)  清除中断标志位*/
	I2C5.I2CCON = I2C5.I2CCON & (~(1<<7))&(~(1<<4));
	/*等待接收从机发来的数据*/
	while(!(I2C5.I2CCON & (1<<4)));
	/*将从机发来的数据读取*/
	data = I2C5.I2CDS;

	/*直接发起停止信号结束本次通信*/
	I2C5.I2CSTAT = 0x90;
	/*清除中断挂起标志位*/
	I2C5.I2CCON = I2C5.I2CCON & (~(1<<4));
	/*延时等待停止信号稳定*/
	mydelay_ms(10);

	return data;

}


/**********************************************************************
 * 函数功能:MPU6050初始化
**********************************************************************/

void MPU6050_Init ()
{
	iic_write(SlaveAddress, PWR_MGMT_1, 0x00); 		//设置使用内部时钟8M
	iic_write(SlaveAddress, SMPLRT_DIV, 0x07);		//设置陀螺仪采样率
	iic_write(SlaveAddress, CONFIG, 0x06);			//设置数字低通滤波器
	iic_write(SlaveAddress, GYRO_CONFIG, 0x18);		//设置陀螺仪量程+-2000度/s
	iic_write(SlaveAddress, ACCEL_CONFIG, 0x0);		//设置加速度量程+-2g
}



/**********************************************************************
 * 函数功能:主函数
 **********************************************************************/

int main(void)
{

	unsigned char zvalue_h,zvalue_l;						//存储读取结果
	short int zvalue;

	/*设置GPB_2引脚和GPB_3引脚功能为I2C传输引脚*/
	GPB.CON = (GPB.CON & ~(0xF<<12)) | 0x3<<12;			 	//设置GPB_3引脚功能为I2C_5_SCL
	GPB.CON = (GPB.CON & ~(0xF<<8))  | 0x3<<8;				//设置GPB_2引脚功能为I2C_5_SDA

	uart_init(); 											//初始化串口
	MPU6050_Init();											//初始化MPU6050

	printf("\n********** I2C test!! ***********\n");
	while(1)
	{
		zvalue_h = iic_read(SlaveAddress, GYRO_ZOUT_H);		//获取MPU6050-Z轴角速度高字节
		zvalue_l = iic_read(SlaveAddress, GYRO_ZOUT_L);		//获取MPU6050-Z轴角速度低字节
		zvalue  =  (zvalue_h<<8)|zvalue_l;					//获取MPU6050-Z轴角速度

		printf(" GYRO--Z  :Hex: %d	\n", zvalue);			//打印MPU6050-Z轴角速度
		mydelay_ms(100);
	}
	return 0;
}


1.刚开始我自己写的程序运行结果是:能读到数据-11823,但是无论怎么旋转开发板,读数一直不变

对比老师的实验代码发现:

/*将第一个字节的数据写入发送寄存器  即从机地址和读写位(MPU6050-I2C地址+读位1)*/
	I2C5.I2CDS = slave_addr << 1 | 0x01;
	/*设置IIC为主机接收模式  发送起始信号  使能IIC收发*/
	I2C5.I2CSTAT = 0xb0;
	/*等待从机接收到数据后应答*/
	while(!(I2C5.I2CCON & (1<<4)));


/*禁止主机应答信号(即开启非应答  因为只接收一个字节)  清除中断标志位*/
	I2C5.I2CCON = I2C5.I2CCON & (~(1<<7))&(~(1<<4));
	/*等待接收从机发来的数据*/
	while(!(I2C5.I2CCON & (1<<4)));
	/*将从机发来的数据读取*/
	data = I2C5.I2CDS;

第一段代码最后while(!(I2C5.I2CCON & (1<<4)));等待是从机应答; 第二段代码==要先配置为非应答,同时清除中断挂起标志位==,因为上一步中断挂起位被应答置1了,一清除SDA线上会再发来从机的数据,再次判断是否接收到数据即可

也就是说前面主机发送完从机地址和读写位后,从机的应答触发了中断标志置位,要想接收数据需要再次触发(清除中断标志位后,又会再接收数据),当接收完成,中断标志位置位,读取I2CDS即可

2.MPU6050初始化时,要先配置电源管理,否则数据误差较大

作业:

代码:

==IIC与MPU6050.zip==

1.综合项目: 实时监测开发板的放置状态,当监测到开发板水平放置时,每隔一分钟向终端上打印一次当前的时间以及开发板的状态 如:“2023-04-05 23:45:00 Status: Normal” 当监测到开发板发生倾斜时,每隔一秒钟向终端上打印一次当前的时间以及开发板的状态 如:“2023-04-05 23:45:00 Status: Warning” 同时让蜂鸣器产生“滴滴”的警报声,在警报状态下,若按下Key2按键,解除蜂鸣器的警报声 提示: 开发板水平静止放置时MPU6050的Z轴上的加速度应该等于重力加速度的值(9.8m/s2),而其X轴和Y轴上的加速度应该等于0 当开发板发生倾斜时MPU6050的Z轴上的加速度的分量会减小,而其X轴和Y轴上的加速度分量会增大 我们可以以此来判断开发板是否发生倾斜

问题及实验现象:

1.代码中陀螺仪检测有误差,这里是使用(16384 - z_value)>500的判断语句,不超过此范围我认为开发板水平放置,否则开发板倾斜

2.蜂鸣器响声有嘶哑的声音

  • 19
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值