ARM接口技術

FS4412開發環境搭建

交叉編譯工具鏈搭建:

①:將交叉編譯工具鏈安裝到具體目錄下:新建了一個路徑,這裏是/home/linux_4412/toolchain/,然後將資源文件gcc-4.6.4.tar.xz移動到此路徑下,解壓到當前目錄(tar xvf gcc-4.6.4.tar.xz),解壓後的目錄為==gcc-4.6.4==

emmc總線和sd總線接口_emmc總線和sd總線接口

②:切換至該路徑下的bin目錄下。ls看一下:

emmc總線和sd總線接口_emmc總線和sd總線接口_02

畫紅框的就是我們所用的編譯工具arm-gcc

③:為了使用該編譯工具不每一次都切換到~/linux_4412/toolchain/gcc-4.6.4/bin絕對路徑下,我們需要做一下全局環境配置:

使用vi .banshrc打開全局環境配置文件,在最後一行輸入以下語句(在原有的PATH全局變量下追加新路徑)

emmc總線和sd總線接口_數據結構_03

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

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

emmc總線和sd總線接口_linux_04

如出現上述界面,説明已安裝成功

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

emmc總線和sd總線接口_emmc總線和sd總線接口_05

emmc總線和sd總線接口_數據結構_06

emmc總線和sd總線接口_emmc總線和sd總線接口_07

不同架構下的處理器,機器碼無法移植,試圖在x86處理器下運行arm-gcc編譯後生成的可執行文件,會顯示格式錯誤

loadb 0x40008000

將選中的程序下載到對應的地址

go 0x40008000

運行0x40008000為起始地址的地址

地址映射表

ARM體系架構中提到的6大類指令,除了load/store指令之外,均不會操作到cpu之外的東西。

每一個硬件控制器裏面都會有一些寄存器,CPU可以往這些寄存器裏寫入或讀取數據(STR、LDR指令),完成對硬件控制器的操控,進而控制硬件(硬件是由硬件控制器直接控制的)

emmc總線和sd總線接口_linux_08

硬件控制原理 :

CPU本身是不能直接控制硬件的,硬件一般是由其對應的控制器來控制, SOC中將各個硬件控制器的寄存器映射到了CPU地址空間中的一段範圍,這樣CPU就可以通過讀寫寄存器來間接控制硬件

注:這裏的寄存器在SOC中但在CPU之外,有地址,訪問方式與內存一樣,常用於控制硬件

再比如:STR,R1,[R2],與LDR,R3,[R4],對於32位處理器來説,寄存器都是32位的,所存放的地址是32位的,也就是説R2裏面存放的地址範圍是4G大小,即地址空間為4G大小,看下面的圖:

emmc總線和sd總線接口_開發語言_09

CPU的4G內存劃分,RAM、ROM?

emmc總線和sd總線接口_開發語言_10

.

這樣看來地址映射表中除了內存,還包含RECV保留空間以及硬件控制器寄存器地址映射空間(SFR特殊功能寄存器),這些共同組成了4G尋址空間

即:在一個處理器中,一般會將Flash、RAM、寄存器等存儲設備分別映射到尋址空間中的不同地址段,我們將這個映射關係成為這個處理器的地址映射表

RAM、ROM、硬盤及內存 - 知乎 (zhihu.com)

==此外注意:==

芯片上電後PC被自動置為0,也就是自動開始執行iROM裏的程序,執行完才會執行我們自己寫的程序


GPIO實驗

GPIO簡介:

GPIO(General-purpose input/output)

即通用型輸入輸出,GPIO可以控制連接在其之上的引腳實現信號的輸入和輸出

芯片的引腳與外部設備相連,從而實現與外部硬件設備的通訊、控制及信號採集等功能

emmc總線和sd總線接口_emmc總線和sd總線接口_11

實驗步驟 :

\1. 通過電路原理圖分析LED的控制邏輯

\2. 通過電路原理圖查找LED與Exynos4412的連接關係

\3. 通過數據手冊分析GPIO中哪些寄存器可以控制LED

\4. 通過程序去操控對應的寄存器完成對LED的控制

1.LED控制邏輯:

emmc總線和sd總線接口_數據結構_12

分析LED2一端(陽極)與DC33V(即直流3.3V)相連,另一端(陰極)直接連接到三極管的集電極,發射極接地,基級與一端接網絡標號為CHG_COK導線相連

基級導線上的電信號為高電平時,三極管導通,LED2的陰極接地,陽極接正極,LED2亮

---------------------為低電平時,三極管斷開,LED2的陰極浮空,陽極接正極,LED2滅

2.LED2與FS4412連接關係:

emmc總線和sd總線接口_開發語言_13

根據網絡標號CHG_COK進行查找,可以發現,LED2的另外一端其實連接到FS4412芯片的GPX2_7引腳上

(芯片內部GPIO硬件控制器與GPX2_7相連,GPX2_7可以輸出高低電平控制LED2的亮滅)

3.分析數據手冊中的與LED2相關的寄存器

emmc總線和sd總線接口_數據結構_14

GPX0,GPX1,GPX2,GPX3分別代表一組引腳,共32個引腳,每組8個引腳,這裏從芯片的電氣原理圖也能看出

我們直接找與GPX2引腳相關的寄存器:

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

emmc總線和sd總線接口_emmc總線和sd總線接口_15

emmc總線和sd總線接口_數據結構_16

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

下圖為每一個寄存器具體在地址映射表的位置

emmc總線和sd總線接口_emmc總線和sd總線接口_17

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

①:GPX2CON寄存器:

emmc總線和sd總線接口_emmc總線和sd總線接口_18

也就是説對於寄存器GPX2CON,配置最高的4位(31~28),可以將引腳7配置為不同的模式

對於其他的引腳,可以配置32位中的其他位,比如3~0位,可以配置引腳0

②:GPX2DAT寄存器

emmc總線和sd總線接口_數據結構_19

每四位控制一個引腳

引腳的狀態和配置的位相同,即第0位到第7位分別控制0~7這八個引腳

注意:GPX2DAT也是32位寄存器,只不過是其餘的位沒有使用,所以只寫了0~7位

4.程序實現LED2的控制:

實驗一:控制LED2熄滅(開發板上電默認點亮)

led-asm.s:

emmc總線和sd總線接口_emmc總線和sd總線接口_20

LED_CONFIG子程序實現將GPX2_7配置為輸出模式

LED_OFF子程序實現將所有的GPX2引腳輸出低電平(此開發板一上電默認點亮LED2)

編寫Makefile:

emmc總線和sd總線接口_emmc總線和sd總線接口_21

.elf只能放到linux上運行,不能直接放到開發板上跑,那麼怎麼觀察實驗結果呢?

要將.elf文件使用交叉編譯工具arm-none-linux-gnueabi-objcopy轉換為.bin二進制文件,然後將這個.bin文件拷貝至共享文件夾下,在使用SecureCRT軟件下載,觀察實驗現象

make:

emmc總線和sd總線接口_開發語言_22

將led-asm.bin文件拷貝至共享文件夾下(Share):cpled-asm.bin /mnt/hgfs/Share/

下載燒錄文件執行:

①:給開發板上電,serial-com3窗口出現提示信息方可,有時沒有出現提示信息,請檢查:

串口線是否鬆動、SD卡是否鬆動、與電腦相連接的usb數據線是否安插牢固,如果還不行,請將撥碼開關撥成EMMC模式

倒計時前按下回車鍵

②:使用loadb 0x40008000,準備將文件下載至0x40008000地址(該地址處於1G擴展內存0x40000000~0x80000000之內)

emmc總線和sd總線接口_linux_23

③:使用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億條指令,一個時鐘週期執行一條指令)

emmc總線和sd總線接口_數據結構_24

因此可以大致推測,下列代碼中標紅部分代碼塊執行一億次,要花費大概零點幾秒,即延時時間大概也就是零點幾秒左右


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


emmc總線和sd總線接口_開發語言_25

因為ARM採用滿減棧,因此要使每一個模式下的SP棧指針初始化為各自的棧的最高地址,從高往低壓棧

寄存器封裝:

一條C語句可能會被編譯器編譯成很多條彙編語句,所以延時時間要比彙編程序的延時短

因此,這裏的延時數1為百萬,彙編延時數為1億

1.宏定義封裝:

emmc總線和sd總線接口_數據結構_26

emmc總線和sd總線接口_開發語言_27

與不使用宏定義相比,所編譯生成的文件大小一樣都是8908字節,可見宏定義只是做了替換,不佔用文件長度

2.結構體封裝:

emmc總線和sd總線接口_linux_28

參與封裝成結構體的成員,必須具備以下兩個屬性:

①:控制的是同一種對象的屬性

②:存儲空間必須連續

?另外為什麼會少4個字節?

emmc總線和sd總線接口_開發語言_29

以後用到寄存器的時候,直接包含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

即通用異步收發器,是一種通用的串行、異步通信總線

該總線有兩條數據線,可以實現全雙工的發送和接收

在嵌入式系統中常用於主機與輔助設備之間的通信

並行通信:

emmc總線和sd總線接口_數據結構_30

可以一次性發送多個數據位

串行通信:

emmc總線和sd總線接口_linux_31

一次只能發送一個數據位

上圖有點問題:同一時刻,一根線只能發送一個數據位,怎麼可能一根線同時高低電平呢

單工與雙工通信:

emmc總線和sd總線接口_linux_32

單工:只有一根線,且只能單向通信

半雙工:同一時刻,只能一方發送、一方接收(==只有一根線)==,雙向通信

全雙工:同一時刻,雙方均可發送和接收(兩根線)

串行、並行對比:

並行總線速度比串行的快,但是卻需要很多數據線,浪費資源

再者,串行總線線與線之間可能存在信號干擾

波特率

波特率用於描述UART通信時的通信速度,其單位為

bps(bit per second)即每秒鐘傳送的bit的數量

UART幀格式:

emmc總線和sd總線接口_emmc總線和sd總線接口_33

空閒位:當不發送數據時,數據線上為高電平

起始位:將數據線置為低電平,起始位為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採用異步通信

異步通信:發送方與接收方時鐘不同步

硬件連接:

emmc總線和sd總線接口_開發語言_34

UART控制器

一般情況下處理器中都會集成UART控制器

我們使用UART進行通信時候只需對其內部的相關寄存器進行設置即可

(一般UART控制器會內部集成發送器、接收器)

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

Exynos_4412下的UART:

本次實驗我們使用絲印為CON7的串口,先來看電氣原理圖:

emmc總線和sd總線接口_數據結構_35

emmc總線和sd總線接口_開發語言_36

引腳的功能設置:

emmc總線和sd總線接口_emmc總線和sd總線接口_37

設置引腳功能的實質是讓引腳在芯片內部連接到某一個對應的控制器上

也就是説某個引腳可能被多個控制器所共用,即引腳複用,比如上面的:

emmc總線和sd總線接口_emmc總線和sd總線接口_38

可見這兩個引腳被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模式:

emmc總線和sd總線接口_開發語言_39

控制器組成與邏輯:

emmc總線和sd總線接口_開發語言_40

最後有更為詳細的通信過程圖解

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

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搬運至發送器

emmc總線和sd總線接口_數據結構_41

波特率設置方法:

emmc總線和sd總線接口_linux_42

小數位四捨五入即可

程序要寫一個死循環,防止結束後程序跑飛(因為沒有linux系統,沒有進程)

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

UART編程:

注意:

SecureCRT默認顯示字符

主頻SCLK_UART為100MHZ,用於作為波特率產生器的時鐘源

PART1:順序發送'A','B','C','D'

1.UART2初始化:主要是配置:發送接收引腳、傳輸幀格式、發送接收模式、波特率

emmc總線和sd總線接口_開發語言_43

2.發送數據:

配置好寄存器之後,就要開始發送數據了。數據的發送,主要是UTXH2這個寄存器(Transmit Holding Register),實際發送數據的時候,只需要往這個寄存器裏面寫入數據就可以了。寫入數據之後,UART相關的電路,自動發送數據給接收端。

emmc總線和sd總線接口_開發語言_44

emmc總線和sd總線接口_開發語言_45

但是從結果來看,不是按照ABCD的順序發送的,是隨機的,那麼究竟是什麼原因呢?

圖解:

emmc總線和sd總線接口_開發語言_46

根據上面的圖解:CPU工作頻率為1000MHZ,而波特率為115200bps,cpu寫入的速度遠大於發送數據位的速度,但是僅僅UTXH2寄存器為空時才可寫入。因此,‘A’寫入時,發送器還沒有發送完,cpu又企圖將'B'寫入,但是發現寫不進去,再寫'C'.,還是無法寫入..........,某一時刻,發送器完成了'A'的全部數據位的發送,此時UTXH2空了下來,此時cpu不一定執行到哪一條指令,即不一定寫哪個字符,因此發送的數據是隨機的

改進方法一:延時

emmc總線和sd總線接口_emmc總線和sd總線接口_47

emmc總線和sd總線接口_開發語言_48

延時的目的是給發送器足夠的時間發送數據,一段時間後再寫入,即可做到有序發送,但是這種方法延時時間不好把握,效率不高

改進方法二:UTRSTAT2狀態位判斷

emmc總線和sd總線接口_python_49

emmc總線和sd總線接口_數據結構_50

使用UTRSTAT2寄存器(發送接收狀態寄存器),當發送Buffer為空(即UTXH2為空)時UTRSTAT2位[1]會自動置為1,當接收Buffer有數據時(即URXH2有數據時),UTRSTAT2位[0]會置為1

emmc總線和sd總線接口_開發語言_51

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

是判斷UTRSTAT2的位[1]是否為1,即Buffer是否為空,等待為空時退出,寫入數據,非空時阻塞等待。

這樣就保證了緩衝區為空時,才會寫數據,且按順序發送,PART1實驗完成

PART2:通過電腦上的SecureCRT軟件,向開發板發送一個字符,然後開發板收到後,將此數據+1返回

程序:

emmc總線和sd總線接口_emmc總線和sd總線接口_52

結果:

emmc總線和sd總線接口_python_53

可以看到,鍵盤輸入123abcd後,SecureCRT軟件界面上顯示了每個字符ASCII碼加一後的結果。

而且:開發板剛上電要下載程序時,輸入loadb命令的實質就是,通過鍵盤將數據發送給開發板,開發板接收到數據後,不做處理原樣發送返回,於ScureCRT終端顯示,而不是輸入了直接就顯示出來輸入數據;對於該實驗也是,通過鍵盤輸入一字符,該字符並不會在SecureCRT顯示,而是通過串口發送到UART(接收器),再由CPU進行運算,然後UART(發送器)將運算後的數據返回到SecureCRT顯示

SecureCRT使用注意:

串口連接成功後,如下圖所示,其中在串口號的前面會有一個綠色的對勾。如果串口發送數據過來,在其界面的空白處就會顯示串口數據信息;如果需要發送串口信息,只要將光標定位到空白區域,然後輸入信息即可(默認情況下,輸入的信息不會顯示)。

emmc總線和sd總線接口_數據結構_54

即發送什麼數據直接通過鍵盤輸入即可,界面上不會顯示鍵盤輸入的信息,而是顯示通過串口接收到的數據信息

總結:

通信過程:

emmc總線和sd總線接口_emmc總線和sd總線接口_55

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;
•    }

}

emmc總線和sd總線接口_emmc總線和sd總線接口_56

下面我們再實現一個類似於庫函數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

emmc總線和sd總線接口_數據結構_57

對於老師提供的自定義printf:

vsprintf對參數進行解析,但不會輸出,將解析後的信息放到printfbuffer緩衝區,然後由puts輸出,當然這個puts函數也是自定義函數,位於common/src/

emmc總線和sd總線接口_emmc總線和sd總線接口_58

來看puts的原型:

puts調用putc函數,逐個字符發送,而putc函數具體實現是若串口2發送寄存器非空則等待,空則寫入數據,由發送器發送到串口輸出端

emmc總線和sd總線接口_emmc總線和sd總線接口_59

即如下圖:

庫函數printf定向到顯卡,自定義函數printf定向到串口

emmc總線和sd總線接口_emmc總線和sd總線接口_60

具體實現流程:printf調用puts,puts調用putc,putc再實現數據由串口發出

emmc總線和sd總線接口_數據結構_61

開發板的標準輸入輸出都是串口,下面是開發板剛上電時,SecureCRT的顯示:

emmc總線和sd總線接口_數據結構_62

作業:

若使用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復位,其本質是一個計數器

emmc總線和sd總線接口_數據結構_63

WDT原理:

emmc總線和sd總線接口_python_64

WDT工作邏輯:

emmc總線和sd總線接口_開發語言_65

使用是的PCLK為100MHZ,經過一級分頻(通過WTCON[15:8]設置分頻),分頻之後得到一個過程值,會再被分頻(通過 WDTCON[4:3]設置分頻,可選固定分頻),經二級分頻分頻之後,該頻率就會作為WTCNT工作頻率,WTCNT減到0後,會在不同模式產生不同信號:

①:普通定時模式,產生一箇中斷信號,通知CPU作某個處理;

②:看門狗模式:產生一個復位信號,讓CPU復位

可通過以下公式計算遞減週期(計數週期):

emmc總線和sd總線接口_數據結構_66

一級分頻所採用的8位預分頻器,容納的數據範圍是0-255,0不能做除數,因此實際的分頻數位1-256,即原來的預分頻值基礎上加1

WDT寄存器詳解:

WTCON:

emmc總線和sd總線接口_python_67

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,觀察結果

emmc總線和sd總線接口_linux_68

調試結果:

emmc總線和sd總線接口_emmc總線和sd總線接口_69

也就是説,當WTCON與WTCNT配置好之後,開始使能WDT,計數器開始遞減,同時進入死循環,由於沒有刷新計數值,計數器在遞減到0之後,就開始產生復位信號,開發板復位(重啓),於是就有了上面的結果

2.執行程序,不斷喂狗,觀察結果:

emmc總線和sd總線接口_開發語言_70

emmc總線和sd總線接口_開發語言_71

循環中第一句打印出WTCNT裏的當前計數值15260=3052*5,然後喂狗刷新數據,接着進行一段時間的延時(這裏不到1秒),然後開始遞減,又打印遞減後的數據,

接着又開始喂狗,計數值又會被更新,這樣的話就不會減到0,WDT就不會發生復位信號

同時注意:調用自定義的printf函數,不需要進行串口的初始化,因為引導程序已經初始化過【不然開發板一上電,SecureCRT也不會顯示硬件信息】

一定要注意,如果按位域配置寄存器時,合理的邏輯是:先配置完WTCON的部分與WTCNT位,最後再使能WDT位

另外,這樣寫也是可以的:

emmc總線和sd總線接口_emmc總線和sd總線接口_72

emmc總線和sd總線接口_linux_73

這樣做是先配置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;

}


按鍵電氣原理圖:

emmc總線和sd總線接口_開發語言_74

①:首先使用GPX1CON寄存器將GPX1_1引腳配置為輸入功能

emmc總線和sd總線接口_python_75

②:使用GPX1DAT寄存器判斷按鍵是否按下

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

GPIO中斷寄存器詳解:

emmc總線和sd總線接口_emmc總線和sd總線接口_76

①:GPX1CON配置GPX1_1為中斷功能

emmc總線和sd總線接口_linux_77

②:EXT_INT41_CON選擇中斷觸發方式:

emmc總線和sd總線接口_python_78

這裏選擇比較合適的下降沿觸發

③:EXT_INT41_FLTCON0用於電平濾波,這裏我們不用

emmc總線和sd總線接口_python_79

為什麼要濾波?

因為按鍵是機械硬件,按下時會產生抖動(不會立刻閉合),或者接觸不良也會產生雜波,濾除雜波可以較理想的達到合適的電平

④: EXT_INT41_MASK選擇使能或屏蔽中斷:

emmc總線和sd總線接口_開發語言_80

這裏我們選擇使能,注意0為使能

⑤: EXT_INT41_PEND掛起中斷請求:(系統會自動置1)

emmc總線和sd總線接口_開發語言_81

圖解

emmc總線和sd總線接口_開發語言_82

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])

emmc總線和sd總線接口_linux_83

中斷控制器的寄存器:

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

emmc總線和sd總線接口_linux_84

ICDISER_CPU: 設置160箇中斷中某個中斷的使能或禁止 ,那麼57號中斷使用的是 (ICDISER1_CPU0)進行配置------------9.5.16

emmc總線和sd總線接口_數據結構_85

ICDIPTR_CPU: 設置中斷的目標cpu:可以通過設置具體偏移地址的寄存器的某些位來選擇特定中斷的目標cpu,比如57號中斷使用的是偏移地址為0x0838的寄存器(ICDIPTR14_CPU0)的[15:8]

每一個寄存器管理四個中斷源,每一個字節管理一箇中斷源,向該字節寫數據可以為中斷選擇目標cpu

emmc總線和sd總線接口_linux_86

emmc總線和sd總線接口_python_87

ICCICR_CPUn: 打開我們想要中斷得到處理的中斷控制器與目標cpu之間的接口(開關)

emmc總線和sd總線接口_python_88

中斷控制器程序:


#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;

}


emmc總線和sd總線接口_emmc總線和sd總線接口_89


中斷處理:

1.工程模板代碼結構分析:

emmc總線和sd總線接口_linux_90

首先各目錄下的文件都被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清零,要記住==

③:為什麼按一次會打印這麼多?

emmc總線和sd總線接口_linux_91

需要清除中斷掛起標誌位,寫1清除

④:.cpu0如何對中斷信號進行區分,即到底是由哪一個硬件控制器產生的:

emmc總線和sd總線接口_開發語言_92

cpu無法區分,但是中斷控制器可以,GIC轉發給cpu時,會將低10位寫成中斷號,cpu處理之前直接讀取即可

⑤:.為什麼按多次只打印一次?

emmc總線和sd總線接口_數據結構_93

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.蜂鳴器響聲有嘶啞的聲音