跳转至

Telink B91系列芯片驱动SDK开发手册


驱动目录结构

Driver SDK目录结构如下:

Driver SDK目录

boot

该文件夹下是启动文件。

boot文件夹

common

该文件夹下是一些和驱动无关的共用文件。其中特殊说明两个文件夹:

bt_debug:是bt相关模块设置debug GPIO的接口函数。

compatibility_pack:是为了兼容以前的各个sdk的驱动接口而添加的相关文件,驱动中不会使用。

common文件夹

drivers

该文件夹下即是相关模块的驱动。

该文件夹用于存放link文件,根据不同的使用需求进行选择。

link文件夹

vendor

该文件夹下是所有模块的demo。

启动机制

a) 芯片上电/deep回来:会从Flash中先搬一段程序到RAM中,然后从RAM启动。

b) retention回来后直接从RAM启动。

可以看到,这里的启动位置是一样的,都是RAM。

Risc-V Platform SoC

a) 芯片上电/deep回来:不会从Flash中搬移代码到RAM中,而是跳转到Flash的起始地址(0x20000000)开始执行。(可以这样处理的原因是该系列的芯片支持直接从Flash取指执行。)

b) retention回来会从IRAM启动。

注意:

有两个RAM,一个是IRAM一个是DRAM,IRAM可以放程序和数据,DRAM只能放数据。

可以看到,这里的启动位置是不一样的,所以在link和S文件中可以看到有两个段:vectors和retention_reset段。vector段是在flash起始地址,retention段是在IRAM的起始地,他们都是启动代码部分,代码部分会有不同的处理,后面会详细说明。

使用组合

根据不同的使用场景,我们可以选择对应的组合方式。

使用场景 S文件 link文件
通用的从flash启动程序 cstartup_b91_flash.S flash_boot.link
测试性能(coremark和dhrystone)时使用 cstartup_b91_flash.S flash_boot_ramcode.link
用于load到ram中启动的程序,不能load到flash中执行 cstartup_b91_ram.S ram_boot.link

其中S文件的区别如下:

S文件 区别
cstartup_b91_flash.S 是从flash启动的程序需要用到的S文件
cstartup_b91_ram.S 是从ram启动的程序需要用到的S文件

link文件的区别:

link文件 区别
flash_boot.link 正常使用场景使用的
flash_boot_ramcode.link 和flash_boot.link文件的唯一区别是,它将除了启动需要的vector段,其他段都放到了ramcode段,所有的程序都在ram中跑,这样执行时间快。
ram_boot.link 该文件是用于load到ram中启动的程序,不能load到flash中执行,该link文件配合cstartup_b91_ram.S进行使用。

配置方法

可以通过如下配置选择需要使用的link文件。

link文件配置

S文件

S文件是通过如下宏定义进行区分选择的。

区分S文件的宏定义

可以通过如下方法定义宏,决定选择使用哪个S文件。

选择S文件的宏定义

objdump.txt

可以通过生成的objdump.txt看到VMA和LMA的相关分布。

下图是使用cstartup_b91_flash.S和flash_boot.link编译生成的一个objdump.txt文件。

其中vectors、text的VMA和LMA地址是一样的,这两个段是从flash中取指执行。

vectors、text的VMA和LMA地址

retention_reset、retention_data、ram_code他们的VMA和LMA的地址是不一样的,LMA的地址是flash地址,VMA的地址是IRAM地址,在S文件中,会对应的将这三个段从flash中搬移到IRAM中。

retention_reset、retention_data、ram_code的VMA和LMA地址

data段的VMA和LMA的地址是不一样的,LMA的地址是在flash中,VMA的地址是在DRAM中。

在S文件中,会对应的将data段从flash中搬移到DRAM中。

data段的VMA和LMA的地址

代码详解

以flash_boot.link文件为例:

//设置代码入口为_RESET_ENTRY
ENTRY(_RESET_ENTRY)
SECTIONS
{
//定义变量NDS_SAG_LMA_FLASH = 0x20000000
    NDS_SAG_LMA_FLASH = 0x20000000 ;
//指定当前地址为0x20000000(不加AT的涉及到的地址都是VMA),点即是代表当前地址
//LMA(Load Memory Address):装载地址,这里可以简单的理解为是flash中的地址。
    . = 0x20000000;
//定义变量BIN_BEGIN等于当前地址
    PROVIDE (BIN_BEGIN = .);
//定义vectors段,VMA地址和LMA的地址均为0x2000000,所以这里没有加AT指令。
//即vectors段的装载地址以及运行地址均是在0x20000000(这里0x20000000是flash的基地址,即vectors段是存储在flash的0x20000000,同时也是从这个地址取指运行的)
//keep相当于告诉编译器,这部分不要被垃圾回收。garbage collection就是删除不用的section,不输出到输出文件中,使用--gc-sections选项设置。但是可以使用KEEP来保留。如下面的vectors段是必须要保留的。
    .vectors: { KEEP(*(.vectors )) }
//指定当前位置为0x0(即IRAM的起始地址)
    . = 0x00000000;
//定义retention_reset段,VMA地址为0x0
//LMA地址= ALIGN(LOADADDR (.vectors) + SIZEOF (.vectors),8)
//LOADADDR (section):获取section的LMA的地址
//SIZEOF (section):获取section的大小
//AT(addr):定义本段的LMA的地址。
//当VMA和LMA不一致的时候,需要用AT指令设置LMA
    .retention_reset  : AT( ALIGN(LOADADDR (.vectors) + SIZEOF (.vectors),8))
     { KEEP(*(.retention_reset )) }
//retention_reset段的VMA、LMA的一些地址需要保存到变量中,S文件中会用到
    PROVIDE (_RETENTION_RESET_VMA_START = ADDR(.retention_reset));
    PROVIDE (_RETENTION_RESET_LMA_START = LOADADADDR(.retention_reset));
    PROVIDE (_RETENTION_RESET_VMA_END = .) .
//aes_data段只可以再IRAM的前64K地址,所以如果不清楚用法的情况下,请不要修改他的位置。
//设置当前地址= .按8对齐的地址
//如果没有对当前地址赋值(点),则所有的段的地址会顺次排列。
    . = ALIGN(8);
    PROVIDE (_AES_VMA_START = .) .
    .aes_data   (NOLOAD)  : { KEEP(*(.aes_data )) }
    PROVIDE (_AES_VMA_END = .) .
    . = ALIGN(8);
    .retention_data : AT( ALIGN(LOADADDR (.retention_reset) + SIZEOF (.retention_reset),8))
      { KEEP(*(.retention_data )) }
    PROVIDE (_RETENTION_DATA_VMA_START = ADDR(.retention_data));
    PROVIDE (_RETENTION_DATA_LMA_START = LOADADADDR(.retention_data));
    PROVIDE (_RETENTION_DATA_VMA_END = .) .

    . = ALIGN(8);
    .ram_code : AT( ALIGN(LOADADDR (.retention_data) + SIZEOF (.retention_data),8))
      { KEEP(*(.ram_code )) }
    PROVIDE (_RAMCODE_VMA_END = .) .
    PROVIDE (_RAMCODE_VMA_START = ADDR(.ram_code));
    PROVIDE (_RAMCODE_LMA_START = LOADADADDR(.ram_code));
    PROVIDE (_RAMCODE_SIZE = SIZEOF (.ram_code));
    . = ALIGN(LOADADADDR (.ram_code) + SIZEOF (.ram_code), 8);
    .text   : AT(ALIGN(LOADADADDR (.ram_code) + SIZEOF (.ram_code), 8))
       { *(.text .stub .text.* .gnu.linkonce.t.* ) KEEP(*(.text.*personality* )) *(.gnu.warning ) }
    . rodata   : AT(ALIGN(LOADADADDR (.text) + SIZEOF (.text), ALIGNOF(. rodata)))
       { *(. rodata . rodata.* .gnu.linkonce.r.* )}
//增加了eh_frame/eh_frame_hdr的段的分配,在使用puts函数时,如果不分配就会出现编译错误
    .eh_frame_hdr  : AT(ALIGN(LOADADADDR (. rodata) + SIZEOF (. rodata), ALIGNOF(.eh_frame_hdr)))
        { *(.eh_frame_hdr ) }
    . = ALIGN(0x20);
    .eh_frame : AT(ALIGN(LOADADDR (.eh_frame_hdr) + SIZEOF (.eh_frame_hdr), 32))
        { KEEP(*(.eh_frame )) }
//为指令压缩表分配内存空间.exec.itable
    .exec.itable : AT(ALIGN(LOADADDR (.eh_frame) + SIZEOF (.eh_frame), ALIGNOF(.exec.itable)))
        { KEEP(*(.exec.itable)) }

    . = 0x00080000;
    PROVIDE( __global_pointer$ = . + (4K / 2) );
//ALIGNOF(.data):返回data的VMA的对齐要求。
//如果section已分配,返回名为section的对齐字节。如果段还没被分配,链接器会报错。
    .data : AT(ALIGN(LOADADADDR (.exec.itable) + SIZEOF (.exec.itable), ALIGNOF(.data)))
        { *(.data .data.* .gnu.linkonce.d.* ) KEEP(*(.gnu.linkonce.d.*personality* )) SORT(CONSTRUCTORS)
            \*(.srodata.cst16 ) \*(.srodata.cst8 ) \*(.srodata.cst4 ) \*(.srodata.cst2 ) \*(. srodata . srodata.\* ) \*(. sdata . sdata.\* .gnu.linkonce.s.\* ) \*(.sdata2 .sdata2.\* .gnu.linkonce.s.\* )
        }
    PROVIDE (_DATA_VMA_END = .) .
    PROVIDE (_DATA_VMA_START = ADDR(.data));
    PROVIDE (_DATA_LMA_START = LOADADDR(.data));
//BIN_SIZE = data段的LMA地址 + data段的大小 - BIN_BEGIN
//bin文件其实就是所有段的LMA拼接在一起组成的,一般都会按照前后顺序依次排列(AT指令中的值是按照这个规则来设置的)
    PROVIDE (BIN_SIZE = LOADADDR(.data) + SIZEOF(.data) - BIN_BEGIN);
//每个输出section可以有一个类型,类型是用圆括号括起来的关键字。
//NOLOAD,section 被标记为不可加载类型,程序运行时不会载入到内存。
//bss段不需要载入,S文件会将bss段的VMA地址空间清零。
    . = ALIGN(8);
    PROVIDE (_BSS_VMA_START = .) .
    . sbss (NOLOAD) : { \*(. dynsbss ) \*(. sbss . sbss.\* .gnu.linkonce.sb.* ) \*(. scommon . scommon.\* ) }
    . bss  (NOLOAD) : { \*(. dynbss ) \*(. bss . bss.\* .gnu.linkonce.b.\* ) \*(COMMON ) . = ALIGN(8); }
    PROVIDE (_BSS_VMA_END = .) .

    . = ALIGN(8);
//_end是堆的起始地址,堆是向上增长的,一般我们设置在bss后面不用的空间
//调用了sprintf/malloc/free之类的函数,这些函数会调用_sbrk函数进行堆内存的分配,_sbrk会通过_end符号确定从哪里开始分配堆空间(一般都是.bss段的末尾),否则会出现链接错误
    _end = .
    PROVIDE (end = .) .
//定义栈的起始地址,这里定义的是DRAM的结尾位置
    PROVIDE (_STACK_TOP = 0x00a0000);
    PROVIDE (FLASH_SIZE = 0x0100000);
}

ASSERT((BIN_SIZE)<= FLASH_SIZE, "BIN FILE OVERFLOW");

对齐

Link文件中会涉及到一些对齐规则,如果涉及到多个段一起搬移,需要注意这几个段的对齐方式,LMA和VMA起始地址不一致的时候,多个段一起搬移,很可能出现搬移出错的情况。

下面是错误的一个例子:

S文件中的代码是想要将sdata和data段从LMA搬移到VMA中,VMA的起始地址是0x00080000,LMA的起始地址和前面的代码相关,并且是按照sdata的VMA的对齐规则进行对齐的。

link文件对齐规则的错误例子

下图是用上述link文件编译生成的一个会出错的lst文件。

出错的lst文件

修改方法有以下几种:

(1) 一个段一个段去搬移,这样就不会有上面的问题。

(2) 合并sdata和data段。

S文件详解

以cstartup_b91_flash.S文件为例。

代码详解

//定义vector段,这句后面开始的代码属于vectors段,直到遇到下一个段名,或者文件结束。
//其中“ax”代表该section可分配并且可执行。
//`a' section is allocable `x' section is executable
.section .vectors, "ax" 
.option push        //push保存当前的.option配置
.option norelax    //设置为norelax
.org 0x0          //伪指令.org是告诉编译器下一条指令的偏移地址。设置偏移地址为0
// .global 伪指令用于定义一个全局的符号,使得链接器能够全局识别它,即一个程序文件中定义的符号能够被所有其他程序文件可见。
.global _RESET_ENTRY 
//.type伪指令用于定义符号的类型。下面是将_RESET_ENTRY定义为一个函数。
.type _RESET_ENTRY,@function
//.align伪指令用于将当前PC地址推进到”2的integer次方个字节”对齐的位置。下面是将当前PC地址推进到4个字节对齐的位置处。
.align 2
//标号_RESET_ENTRY为程序的入口地址。
_RESET_ENTRY:
//程序的入口地址必须是可执行指令,这里是一个跳转指令,跳转到标号_START。
    j     _START
//设置偏移地址为0x18的位置存放BIN_SIZE(4个字节)BIN_SIZE在link文件中有定义。
    .org 0x18
    .word (BIN_SIZE)
//设置偏移地址为0x20的位置存放关键字,这个位置必须是这个值,否则程序跑不起来。
    .org 0x20
    .word ('T'<<24 | 'L'<<16 | 'N'<<8 | 'K')
//设置偏移地址为0x26的位置存放flash配置,后面flash取指会根据这个配置决定走哪个协议。
//支持以下6种,可以根据flash型号进行选择。
     .org 0x26
     //.short (0x0003) //READ: cmd:1x, addr:1x, data:1x, dummy:0
    //.short (0x070B) //FREAD: cmd:1x, addr:1x, data:1x, dummy:8
    .short (0x173B) //DREAD: cmd:1x, addr:1x, data:2x, dummy:8
    //.short (0x53BB) //X2READ: cmd:1x, addr:2x, data:2x, dummy:4
    //.short (0x276B) //QREAD: cmd:1x, addr:1x, data:4x, dummy:8
    //.short (0x65EB) //X4READ: cmd:1x, addr:4x, data:4x, dummy:6
//使用pop恢复.option配置
.option pop 
//4个字节对齐
    .align 2

_START:
//此处是为了debug使用的,会将PB4输出高,一般用来看状态的,默认关闭。
#if 0
    lui   t0,0x80140        //0x8014030a
    li    t1, 0xef
    li    t2, 0x10
    sb    t1 , 0x30a(t0)   //0x8014030a PB oen = 0xef
    sb    t2 , 0x30b(t0)   //0x8014030b PB output = 0x10
#endif
//初始化全局指针gp寄存器,__global_pointer$在link文件中定义。
    .option push
    .option norelax
    la gp, __global_pointer$
    .option pop
//初始化全局指针sp寄存器,__global_pointer$在link文件中定义。
     la t0, _STACK_TOP
    mv sp, t0

#ifdef __nds_execit
//设置指令压缩表的及地址
    la t0, _ITB_BASE_
    csrw uitb , t0
#endif
//设置FS为0b11,清fscsr(是浮点运算之前需要做的处理)
#ifdef __riscv_flen
    /* Enable FPU */
    li t0, 0x00006000
    csrrs t0, mstatus , t0
    /* Initialize FCSR */
    fscsr zero
#endif
//设置中断入口基地址(base)
    la t0, __vectors
    csrw mtvec, t0
//使能中断的vector模式(需要使能两个地方,这里不可以修改)
    /* Enable vectored external plic interrupt */
    csrsi mmisc_ctl, 2

    /*vector mode enable bit (VECTORED) of the Feature Enable Register */
    lui    t0, 0xe4000
    li     t1, 0x02
    sw     t1, 0x0(t0)   //(*(volatile unsigned long*)(0xe4000000))= 0x02
//使能I/D-Cache
    csrr t0, mcache_ctl
    ori t0, t0, 1 #/I-Cache
    ori t0, t0, 2 #/D-Cache
    csrw mcache_ctl, t0
    fence.i
//将retention_reset段从flash中搬到ram中

_RETENTION_RESET_INIT:
    la t1, _RETENTION_RESET_LMA_START
    la t2, _RETENTION_RESET_VMA_START
    la t3, _RETENTION_RESET_VMA_END
_RETENTION_RESET_BEGIN:
    bleu t3, t2, _RETENTION_DATA_INIT
    lw t0, 0(t1)
    sw t0, 0(t2)
    addi t1, t1, 4
    addi t2, t2, 4
    j _RETENTION_RESET_BEGIN
//将retention_data段从flash中搬到ram中
_RETENTION_DATA_INIT:
    la t1, _RETENTION_DATA_LMA_START
    la t2, _RETENTION_DATA_VMA_START
    la t3, _RETENTION_DATA_VMA_END
_RETENTION_DATA_INIT_BEGIN:
    bleu t3, t2, _RAMCODE_INIT
    lw t0, 0(t1)
    sw t0, 0(t2)
    addi t1, t1, 4
    addi t2, t2, 4
    j _RETENTION_DATA_INIT_BEGIN
//将ram_code段从flash中搬到ram中
_RAMCODE_INIT:
    la t1, _RAMCODE_LMA_START
    la t2, _RAMCODE_VMA_START
    la t3, _RAMCODE_VMA_END
_RAMCODE_INIT_BEGIN:
    bleu t3, t2, _DATA_INIT
    lw t0, 0(t1)
    sw t0, 0(t2)
    addi t1, t1, 4
    addi t2, t2, 4
    j _RAMCODE_INIT_BEGIN
//将data段从flash中搬到ram中
_DATA_INIT:
    la t1, _DATA_LMA_START
    la t2, _DATA_VMA_START
    la t3, _DATA_VMA_END
_DATA_INIT_BEGIN:
    bleu t3, t2, _ZERO_BSS
    lw t0, 0(t1)
    sw t0, 0(t2)
    addi t1, t1, 4
    addi t2, t2, 4
    j _DATA_INIT_BEGIN
//将bss段清零
_ZERO_BSS:
    lui t0, 0
    la t2, _BSS_VMA_START
    la t3, _BSS_VMA_END
_ZERO_BSS_BEGIN:
    bleu t3, t2, _ZERO_AES
    sw t0, 0(t2)
    addi t2, t2, 4
    j _ZERO_BSS_BEGIN
//将AES段清零
_ZERO_AES:
    lui t0, 0
    la t2, _AES_VMA_START
    la t3, _AES_VMA_END
_ZERO_AES_BEGIN:
    bleu t3, t2, _FILL_STK
    sw t0, 0(t2)
    addi t2, t2, 4
    j _ZERO_AES_BEGIN
//将堆栈区域均初始化为0x55,默认该段代码是不打开的,因为ram比较大,会比较费时,如果debug需要,可以打开使用。
_FILL_STK:
#if 0
    lui t0, 0x55555
    addi t0, t0, 0x555
    la t2, _BSS_VMA_END
    la t3, _STACK_TOP
_FILL_STK_BEGIN:
    bleu t3, t2, _MAIN_FUNC
    sw t0, 0(t2)
    addi t2, t2, 4
    j _FILL_STK_BEGIN
#endif
//跳转到main函数
_MAIN_FUNC:
    nop
//使用j或者jal只能跳转到[-524288,524287],超出这个范围就只能使用jalr来实现,为避免出现此类问题,统一使用jalr来进行跳转。
    la t0, main
    jalr t0

    nop
    nop
    nop
    nop
    nop
_END:
j _END 

//定义一个宏,名字是INTERRUPT,参数是num,.endm时这个宏的结束。
.macro INTERRUPT num
//弱定义
.weak entry_irq\num
.set entry_irq\num, default_irq_entry
.long entry_irq\num
.endm
//中断源一共有64个
#define VECTOR_NUMINTRS 63
.section .ram_code, "ax"
//定义ram_code段,所有的中断入口地址都应该在ram_code,这样才能快速进入中断。
.global __vectors
//共有64个中断源,这里需要设置的对齐的计算公式为:2ceiling(log2(N))+2,N为64,所以这里应该设置为256对齐。
.balign 256

__vectors:
    .long trap_entry
//实际这里是一个for循环,是在定义除了trap中断以外的63个中断入口地址
//展开之后是:
//.weak entry_irq1
//.set entry_irq1, default_irq_entry
//.long entry_irq1
//.........
//.weak entry_irq63
//.set entry_irq63, default_irq_entry
//.long entry_irq63
//我们使用vector mode,发生中断后,pc会指向地址__vectors +4*中断ID,如果中断ID为2,则会跳转到entry_irq2
.altmacro
.set irqno, 1
.rept VECTOR_NUMINTRS/* .rept .endr */
INTERRUPT %irqno
.set irqno, irqno+1
.endr

vectors和retention_reset段的差异

retention_reset是retention回来会跑的启动代码。

目前retention_reset的处理是不会从flash中搬这些段(retention_reset、retention_data、ram_code)到ram,因为retention阶段这些段是保留的不会丢失的。这个处理的条件是,retention ram大小是比这些段大的,如果有超过,则需要做搬移动作。

retention_reset启动部分比vectors需要多加的处理包括:flash唤醒、多地址寄存器恢复。这些是retention回来必须做的处理。

其他

(1) .org的使用注意事项

目前的编译环境下,.org和优化选项中的Link Time Optimization(-flto)不能同时使用,但是为了编译出来的bin文件小,(-flto)是一定会选择的,所以在S文件中如果想使用.org,需要按照如下方法使用:

优化选项中的Link Time Optimization(-flto)

.option push       //push保存当前的.option配置
.option norelax   //设置为norelax
.org 0x0         //设置.org
.........
.option pop     //使用pop恢复.option配置

.option说明如下:

.option伪指令用于设定某些架构特定的选项,使得汇编器能够识别此选项并按照选项的定义采取相应的行为。

push、pop用于临时性的保存或者恢复.option伪指令指定的选项:

“.option push”伪指令暂时将当前的选项设置保存起来,从而允许之后使用.option伪指令指定新的选项;而“.option pop” 伪指令将最近保存的选项设置恢复出来重新生效。

通过“.option push”和“.option pop”的组合,便可以在汇编程序中在不影响全局选项设置的情况下,为其中嵌入的某一段代码特别地设置不同的选项。

(2) 压缩指令

RISC-V的C Extension,指的是用16bit的指令替换32bit指令,而Andes的CoDense(Code Dense)技术,是把32位的指令放到指令表里面,在原来32位指令出现的地方,用16 EXEC.IT 0xxxxx来代替。

宏__nds_execit在编译器中默认设置为打开状态,同时_ITB_BASE_会设置为.exec.itable的首地址。

在S文件中将_ITB_BASE_设置到寄存器uitb中,作为指令表的基地址。压缩指令会放到.exec.itable段中。

(3) FPU使能

宏__riscv_flen在编译器中默认设置为打开状态。

在执行浮点指令前,需要将mstatus<14:13>FS字段改为非0的值,否则会抛指令异常,所以S文件中将FS设置为了0b11,初始化浮点的控制状态寄存器FCSR为0。

Debug Demo

Driver里没有重新定义printf接口,直接使用了toolchain自带的printf接口,但是我们对其进行了重定向,driver里分别实现了两种重定向,一种是将数据重定向到GPIO(通过GPIO模拟串口时序),一种是重定向到USB。可选择任意一种进行debug info输出。

可以在printf.h中选择,使用GPIO还是USB打印。

#define DEBUG_IO         0
#define DEBUG_USB        1
#define DEBUG_BUS        DEBUG_IO

GPIO口模拟串口输出

GPIO口相关配置在printf.h中配置选择,包括波特率等(只需要接RX和GND)。硬件接法如下:

GPIO口硬件接法

下图为串口助手接收到的打印输出信息:

串口助手接收到的打印输出信息

USB打印输出

配置为USB printf,需要使用BDT工具来查看输出信息,使用USB打印时需要注意的一点是,USB使用的是固定48M时钟,在时钟初始化默认已经配置好了,但是如果使用的PLL clock不能整除得到48M的话,USB可能不能正常使用。

使用USB printf时可以配置阻塞和非阻塞模式,可通过printf里的相关宏定义选择,默认非阻塞,如下:

#define BLOCK_MODE         0

如下为BDT配置usb_log方法和实验现象:

BDT配置usb_log方法

实验现象

中断

PLIC (Platform-Level Interrupt Controller)兼容RISC-V PLIC,具有两大功能:中断向量和中断优先级。

中断概述

中断机制,处理器在顺序执行程序指令流过程中突然被别的请求打断而中止执行当前的程序,转而去处理别的事情 ,待其处理完了别的事情,然后重新回到之前程序中断的点继续执行之前的程序指令流,而“别的请求”便称之为中断请求,别的“请求来源”称之为中断源,中断源通常来自于外围设备。处理器转而去执行“别的事情”便称之为中断处理程序。

中断类型

RISV-V的架构模式有三种工作模式:机器模式(Machine Mode)、用户模式(User Mode)和监督模式(Supervisor Mode)。我们的使用的是机器模式,对应的机器模式下有三种中断类型:软件中断(Software Interrupt)、计时中断(Timer Interrupt)和外部中断(External Interrupt)。外部中断指CPU外部中断,例如UART、GPIO等产生的;计时中断是指来自计时器的中断,软件中断是来自软件自己触发的中断。

注意:

  • 中断有两种模式,Vector和regular模式,driver默认的是vector模式。

外部中断

中断使能

驱动中断接口的使用,如果想打开中断,分下面的三个层级:

第一层

core_enable_interrupt()

使能RISC-V core中CSR寄存器中的对应BIT,这个是总的中断开关。

第二层

plic_interrupt_enable()

使能plic模块中对应的模块的BIT,这个是模块中断控制开关。

第三层

rf_set_irq_mask(FLD_ZB_RX_IRQ)

使能对应模块的mask,以rf模块为例,需要用哪个,设置哪个即可。

Vector模式下外部中断处理函数

在plic驱动中已经定义好了相应的中断处理函数,对应如下:

中断向量 中断处理函数
IRQ0_EXCEPTION except_handler
IRQ1_SYSTIMER stimer_irq_handler
IRQ2_ALG analog_irq_handler
IRQ3_TIMER1 timer1_irq_handler
IRQ4_TIMER0 timer0_irq_handler
IRQ5_DMA dma_irq_handler
IRQ6_BMC bmc_irq_handler
IRQ7_USB_CTRL_EP_SETUP usb_ctrl_ep_setup_irq_handler
IRQ8_USB_CTRL_EP_DATA usb_ctrl_ep_data_irq_handler
IRQ9_USB_CTRL_EP_STATUS usb_ctrl_ep_status_irq_handler
IRQ10_USB_CTRL_EP_SETINF usb_ctrl_ep_setinf_irq_handler
IRQ11_USB_ENDPOINT usb_endpoint_irq_handler
IRQ12_ZB_DM rf_dm_irq_handler
IRQ13_ZB_BLE rf_ble_irq_handler
IRQ14_ZB_BT rf_bt_irq_handler
IRQ15_ZB_RT rf_irq_handler
IRQ16_PWM pwm_irq_handler
IRQ17_PKE pke_irq_handler
IRQ18_UART1 uart1_irq_handler
IRQ19_UART0 uart0_irq_handler
IRQ20_DFIFO audio_irq_handler
IRQ21_I2C i2c_irq_handler
IRQ22_SPI_AHB hspi_irq_handler
IRQ23_SPI_APB pspi_irq_handler
IRQ24_USB_PWDN usb_pwdn_irq_handler
IRQ25_GPIO gpio_irq_handler
IRQ26_GPIO2RISC0 gpio_risc0_irq_handler
IRQ27_GPIO2RISC1 gpio_risc1_irq_handler
IRQ28_SOFT soft_irq_handler
IRQ29_NPE_BUS0 npe_bus0_irq_handler
IRQ30_NPE_BUS1 npe_bus1_irq_handler
IRQ31_NPE_BUS2 npe_bus2_irq_handler
IRQ32_NPE_BUS3 npe_bus3_irq_handler
IRQ33_NPE_BUS4 npe_bus4_irq_handler
IRQ35_USB_RESET usb_reset_irq_handler
IRQ36_NPE_BUS7 npe_bus7_irq_handler
IRQ37_NPE_BUS8 npe_bus8_irq_handler
IRQ42_NPE_BUS13 npe_bus13_irq_handler
IRQ43_NPE_BUS14 npe_bus14_irq_handler
IRQ44_NPE_BUS15 npe_bus15_irq_handler
IRQ46_NPE_BUS17 npe_bus17_irq_handler
IRQ50_NPE_BUS21 npe_bus21_irq_handler
IRQ51_NPE_BUS22 npe_bus22_irq_handler
IRQ52_NPE_BUS23 npe_bus23_irq_handler
IRQ53_NPE_BUS24 npe_bus24_irq_handler
IRQ54_NPE_BUS25 npe_bus25_irq_handler
IRQ55_NPE_BUS26 npe_bus26_irq_handler
IRQ56_NPE_BUS27 npe_bus27_irq_handler
IRQ57_NPE_BUS28 npe_bus28_irq_handler
IRQ58_NPE_BUS29 npe_bus29_irq_handler
IRQ59_NPE_BUS30 npe_bus30_irq_handler
IRQ60_NPE_BUS31 npe_bus31_irq_handler
IRQ61_NPE_COMB npe_comb_irq_handler
IRQ62_PM_TM pm_irq_handler
IRQ63_EOC eoc_irq_handler
__attribute__((section(".ram_code"))) void default_irq_handler(void)
{

}
void stimer_irq_handler(void) __attribute__((weak, alias("default_irq_handler")));

默认状态下,所有的中断处理函数均被弱定义为default_irq_handler一个空函数。

弱函数

理论上,一个工程中是不允许有两个相同名称的函数的,这里使用_weak指明其中一个是弱函数即可。

程序在编译的时候,如果发现有两个相同名字的函数,而且其中一个是弱函数,就会忽略弱函数,使用正常的函数进行编译,如果发现只有一个弱函数,那还是会使用弱函数参与编译。

上层使用的时候,只要再定义一个一模一样的函数,但是不需要指明是弱函数了,然后在函数中加入一些用户代码即可。

中断现场保存与恢复

在中断处理中没有发现中断保存和恢复,但是功能正常,原因如下:

_attribute_ ((interrupt ("machine"), aligned(4)));

attribute里有中断的声明,编译器看到会插入修改保护寄存器的代码。

外部中断内的优先级

多个中断源同时向处理器发起请求时,需要对这些中断源仲裁,哪些中断源被优先处理,就存在中断优先级的概念。当中断优先级不使能的情况下,新的中断不会打断正在处理的中断,等完成中断服务函数后才能响应新的中断请求。默认中断抢占功能是不打开的,如果需要使用中断抢占功能需要三个步骤:

a) 设置plic_set_threshold().只有中断优先级大于阈值(priority> threshold)才能产生中断,threshold默认为0,priority默认为1.

b) plic_preempt_feature_en();//使能中断抢占功能

c) plic_set_priority();//设置中断优先级

注意:

  • SoC有4个中断优先级0-3,没有设置中断优先级的中断源,默认优先级为1,数字越大优先级越高,设置为0优先级不会产生中断(threshold默认0,不满足条件:priority> threshold),高优先级的中断源可以打断比它低的中断源,不能打断同级中断。

Demo里配置了3个中断,stimer中断、timer0中断和rf_tx中断。

a) 开中断

core_interrupt_enable();

b) 使能中断抢占,设置优先级,默认threshold为0

plic_preempt_feature_en();  //使能中断抢占
plic_set_priority(IRQ1_SYSTIMER,IRQ_PRI_LEV3);  //设置stimer 优先级
plic_set_priority(IRQ4_TIMER0, IRQ_PRI_LEV2);   //设置timer0 优先级
plic_set_priority(IRQ15_ZB_RT, IRQ_PRI_LEV1);  //设置rf 优先级

在中断处理函数延时前后,设置了观测IO,延时前拉高,延时后拉低。Demo中,stimer延时250us,timer0延时600us,在rf_tx延时800us。

_attribute_ram_code_sec_noinline_ void irq_handler(void)
{ ...
    gpio_set_high_level(LED3);
    delay_us();
    gpio_set_low_level(LED3);
}

结果观测

如下图,中断嵌套打开,rf_tx间里被优先级高的timer0打断,以此类推Time0中断处理函数被优先级更高的stimer打断。

中断嵌套打开

如下图,中断嵌套未打开,可以对比发现三个中断源默认的优先级都是1,执行完前一个中断处理函数,再去处理下个中断处理函数,相互之间不会被打断。

中断嵌套未打开

GPIO

SoC的GPIO驱动实现了对GPIO的输入输出、上下拉、中断等功能的配置。

中断

GPIO可以配置用于产生中断,中断硬件结构如下图所示,每个GPIO通过配置可以产生GPIO_IRQ、GPIO2RISC0_IRQ、GPIO2RISC1_IRQ三种类型的中断。GPIO_IRQ是最基本的GPIO中断,GPIO2RISC0_IRQ和GPIO2RISC1_IRQ除了包含GPIO_IRQ功能外,还可以在Timer(定时器外设)的应用中产生计数或者控制信号。

中断硬件结构

机制说明

如下图,GPIO设置为上升沿触发,MCU的机制是:将GPIO的电平信号作为产生中断的信号,上升沿触发中断。

GPIO设置为上升沿触发

如下图,GPIO设置为下降沿触发,MCU的机制是:将GPIO的电平信号取反,然后将取反后的信号作为中断产生的信号,上升沿触发中断。

GPIO设置为下降沿触发

如下图,若两个GPIO设置为一种中断,上升沿触发,MCU的机制是:将两个GPIO的电平信号做或运算,然后用得到的信号作为产生中断的条件,上升沿触发中断。图中只有GPIO0触发了中断。

两个GPIO设置为一种中断,上升沿触发

如下图,若两个GPIO设置为一种中断,下降沿触发,MCU的机制是:分别将两个GPIO的电平信号取反,然后将取反信号做或运算,用得到的信号作为产生中断的条件,也是上升沿触发中断,即最终MCU均是用最终信号进行上升沿触发中断。图中只有GPIO0触发了中断。

两个GPIO设置为一种中断,下降沿触发

如下图,若GPIO0设置上升沿触发,GPIO1设置下降沿触发,MCU的机制是:将GPIO1的电平信号取反,然后将GPIO0与(!GPIO1)做或运算,用这个得到的信号作为产生中断的条件,同样也是上升沿触发中断。即最终MCU均是用最终信号进行上升沿触发中断。图中只有GPIO1触发了中断。

GPIO0设置上升沿触发,GPIO1设置下降沿触发

结论

两个或多个GPIO设置成一种中断,根据输入GPIO的时序,触发中断的情况是不确定的,不推荐使用。但不同的GPIO中断的机制相互独立,一个GPIO设置成一种中断,两个GPIO的中断就都可以触发了。

比如GPIO0设置成GPIO_IRQ中断,GPIO1设置成GPIO_IRQ_RSIC0中断,均配置成上升沿触发。如下图,信号0和1分别是GPIO0的输入时序和中断处理函数内设置的观测GPIO0中断的toggle信号,信号2和3分别是GPIO1的输入时序和中断处理函数内设置的观测GPIO1中断的toggle信号。图中GPIO1和GPIO2均触发了中断。

两个或多个GPIO设置不同中断

注意:

  • 因为GPIO中断只有GPIO_IRQ、GPIO_IRQ_RSIC0、GPIO_IRQ_RSIC1三种,因此最多可以同时设置三个GPIO中断。

注意事项

如设置触发类型为高电平或者上升沿触发,应该设置下拉电阻,设置触发类型为低电平或者下降沿触发,应该设置上拉电阻。

另外两个不需要应用层关心的问题,也在这里简单说明下(驱动接口中已处理)。

设置下降沿触发时,需要在设置gpio的极性后清掉中断位,再使能mask。否则gpio设置下降沿触发时,在使能gpio中断时刻产生一次非下降沿导致的中断触发。此部分已经在gpio对应的中断使能函数中处理,不需要应用层关心。

GPIO复用功能切换注意事项:

(1) 开始就是GPIO功能,那么需要先将所需要的功能MUX配置好后,再将GPIO功能disable。

(2) 开始是功能IO,需要改成GPIO output,先设置对应的IO的output值和OEN,再enable GPIO功能。

(3) 开始是功能IO,需要改成GPIO input。

需要该IO上拉:

情况1(数字上拉):先将output设置成1,OEN设置为1;

情况2(模拟上拉):将pullup设置为1。

不需要上拉:

情况1(数字上拉): 将output设置为0,OEN设置为1;

情况2(模拟上拉): 将pullup 设置为0。

最后enable GPIO功能。

Clock

简述

Risc-V Platform SoC的clock相对比较复杂,如下图所示为部分时钟的时钟树(datasheet有完整的时钟树结构)。这里介绍几个比较重要的时钟,pll_clk/cclk/hclk/pclk/mspi_clk。

pll_clk: 即图中的PLL,它是很多模块时钟源的源头,包括sys_clk一般使用的也是从PLL分频过来的。

cclk:即cpu clk,程序运行的速度是有该clock决定的,同时它也是hclk和pclk的唯一时钟源头。

hclk:所有挂在AHB总线上的模块均使用hclk。

pclk:所有挂在APB总线上的模块均使用pclk。

mspi_clk:mspi连接flash,进行flash的相关操作,包括取指,读写flash等,均是受该时钟控制。

时钟树

clock_init

Driver提供了clock_init接口可以配置如上的5个时钟。

注意:

  • 这个函数除了设置参数中的几个clock,还会设置USB的clock,USB的clock固定使用48M时钟。

目前支持的最大clock如下表所示:

clk cclk hclk pclk mspi
fre_max 96M 48M 24M 外挂flash:48M;内置flash:64M

PLL_CLK

可使用时钟较多,但如果没有特殊需求,需统一使用192M。

注意:

  • USB使用固定48M时钟,配置PLL_CLK时需要注意如果时钟不能分频得到48M的话可能导致USB不能正常使用。
  • 另外Audio驱动中的一些频率均是按照192M来设置的,如果修改PLL_CLOCK会导致Audio出错。

CCLK

cclk可选的时钟源有4种,分别为RC_24M、PAD_24M、PAD_PLL、PAD_PLL_DIV,其中PAD_PLL_DIV可选的分频比有2-15。

HCLK

hclk由cclk分频而来,可以1、2分频。

PCLK

pclk由hclk分频而来,可以1、2、4分频。

注意:

  • 当hclk = 1/2 cclk时,pclk不能设置4分频,这个和硬件相关,接口里已经做了处理,如果这样设置的话,程序会停住,不会往下执行。
  • 另外,在使用REBOOT或者PM的情况下如果hclk选择2分频时有概率导致死机。

MSPI_CLK

mspi clock可选两种时钟源,一种是等于cclk,另一种是由PLL_clock分频得到。

AES

支持硬件AES128加密,所有接口中使用的buffer数据,均使用的小端模式。

注意:

  • 因为AES计算时使用的数据的存放地址是有限制要求的——必须是在base_address+64K的地址空间内。
  • 目前驱动中处理方式如下: 定义base_address为0xc0000000(即IRAM的起始地址,),在IRAM的前64K的范围aes_data段(可以在link文件中看到),AES计算时需要处理的数据均放到aes_data段进行处理。
  • 如果不改变link文件中aes_data段的位置,可以不关心上述流程,直接使用。
  • 如果不满足需求,也可以根据需求自行调整,驱动提供aes_set_em_base_addr接口来修改base_address。
  • 另外该地址和BT共用,修改该地址还需要确保BT使用的数据也在这个地址空间内。

EMI

EMI例程是配合EMI_Tool” 和 “Non_Signaling_Test_Tool”工具使用的,本文档主要介绍EMI测试例程中相关功能和注意事项。

协议

通信协议请参考《Telink SoC EMI Test User Guide》。

程序说明

Eagle中EMI测试支持carrieronly模式、continue模式、burst模式、收包模式。

支持的无线通信方式包括Ble1M、Ble2M、Ble125K、Ble500K、Zigbee250K。

CarrierOnly模式

功能: CarrierOnly模式用于产生单通信号,该模式下可设定单通信号的通道(channel)与 能量值(power)以及通信方式(rf mode)。

实例说明:

测试工具:选用EMI_Tool

无线通信方式设定:Ble1M/Ble2M/Ble125K/Ble500K/Zigbee250K

能量设定:XXdB

通道设定:2402~2480MHz

CarrierOnly模式

Continue模式

功能:Continue模式用于产生连续信号,该模式下可设定通道(channel)与 能量值(power)以及通信方式(rf mode)。

连续模式下发送包(Payload)数据分为了Prbs9、0x0f、0x55三种,并可设置跳频(hop)。

实例说明:

测试工具:选用EMI_Tool

无线通信方式设定:Ble1M/Ble2M/Ble125K/Ble500K/Zigbee250K

能量设定:XXdB

通道设定:2402~2480MHz

注意:

  • 目前EMI_Tool在continue模式下只支持发送prbs9数据包。

Continue模式

Burst模式

功能:Burst模式下可设定Burst信号的通道(channel)与 能量值(power)以及通信方式(rf mode)。

Burst模式下发送包(Payload)数据分为了Prbs9、0x0f、0x55三种。

实例说明:

测试工具:选用Non_Signaling_Test_Tool

无线通信方式设定:Ble1M/Ble2M/Ble125K/Ble500K/Zigbee250K

能量设定:XXdB

通道设定:2402~2480MHz

注意:

  • Burst模式下由于信号不连续,可使用频谱分析仪的Single Sweep和MaxHold设置进行信号抓取。

下面前三幅图为Single Sweep设置抓取的结果,第四幅图为MaxHold设置抓取的结果

Burst模式1

Burst模式2

Burst模式3

Burst模式4

RX模式

功能:收包模式下可设定通信方式进行收包,具体可配合非信令测试工具“Non_Signaling_Test_Tool”获取收包数和RSSI。

实例说明:

测试工具:选用Non_Signaling_Test_Tool

无线通信方式设定:Ble1M/Ble2M/Ble125K/Ble500K/Zigbee250K

通道设定:2402~2480MHz

Timer

SoC支持2个定时器:Timer0、Timer1。

支持以下四种模式:System Clock Mode、GPIO Trigger Mode、GPIO Pulse Width Mode、Tick Mode。

还有一个watchdog timer,配置为“watchdog”来监测系统运行,如果出现异常,则重启。

功能说明

当使用Timer0、Timer1定时器时,需要指定哪种模式,通过timer_set_mode函数接口设置:

void timer_set_mode(timer_type_e type, timer_mode_e mode,unsigned int init_tick, unsigned int cap_tick).

当使用Timer0、Timer1的GPIO Trigger Mode,GPIO Pulse Width Mode时,Timer的时钟源是由GPIO提供的,需要通过如下接口进行设置,可以选择gpio以及极性,同时该接口中还会将对应的GPIO的相关中断mask也设置好,以便可以正常的产生timer中断。

void timer_gpio_init(timer_type_e type, gpio_pin_e pin, gpio_pol_e pol ).

System Clock Mode

时钟源 功能 机制说明
pclk 定时产生中断 设置为该模式后,每当检测到pclk的上升沿,则定时器的计数寄存器加1,直到达到capture value,产生中断,同时会自动装载initial_tick,重新计数,当达到capture value,再次进入中断,timer使能不关,这样的操作就会一直循环进行。

设置步骤(如下代码所示):

timer_set_mode(TIMER0, TIMER_MODE_SYSCLK, 0, 50*sys_clk.pclk*1000);
timer_start(TIMER0);

GPIO Trigger Mode

时钟源 功能 机制说明
由GPIO提供的 指定个数的GPIO上升沿/下降沿触发中断 设置为该模式后,GPIO每发生一个上升沿/下降沿,定时器就会计数加1,当定时器的计数值达到指定的设定个数,就会进行中断,定时器清零重新开始计数,定时器使能中断不关,这样的操作就会一直循环进行。

设置步骤(如下代码所示):

timer_gpio_init(TIMER0, SW1,POL_RISING);
timer_set_mode(TIMER0, TIMER_MODE_GPIO_TRIGGER,0,TIMER_MODE_GPIO_TRIGGER_TICK);
timer_start(TIMER0);

GPIO Pulse Width Mode

时钟源 功能 机制说明
pclk 捕获脉冲宽度 设置为该模式后,设置的GPIO如果检测到上升沿/下降沿,就会触发定时器定时,每一个pclk,定时器就会计数加1,当检测到该GPIO电平再次翻转就会进入中断,定时器计数停止,此时可以读取当前的tick计数值,去计算gpio脉冲的宽度,此中断触发一次,不会一直操作自动循环。

设置步骤(如下代码所示):

timer_gpio_init(TIMER0, SW1, POL_FALLING);
timer_set_mode(TIMER0, TIMER_MODE_GPIO_WIDTH,0,0);
timer_start(TIMER0);

例子说明:如果极性设置为POL_FALLING,则是下降沿触发计时,上升沿产生中断。

Tick Mode

时钟源 功能 机制说明
pclk 可作为时间指示器来用,该模式不会产生中断 设置为该模式后,每当检测到system clock的上升沿,定时器的计数器加1,可以判断定时器的计数值是否达到指定的设定时间,手工将其定时器的初始计数值设为0,定时器就会又从0重新开始定时。如果没有在指定的时间将定时器的初始计数值设为0,那么定时器就会一直加1,直到定时器计数溢出,自动将定时器的初始计数值设为0,又开始计时,定时器就像钟表一样一直循环计时。

设置步骤(如下代码所示):

timer_set_mode(TIMER0, TIMER_MODE_TICK,0,0);
timer_start(TIMER0);

Watchdog Mode

时钟源 功能 机制说明
pclk 在设定的时间内没有“喂狗”就会复位 设置为该模式后,看门狗开始计时,如果在指定的时间内没有喂狗,程序就会复位,喂狗函数为:wd_clear_cnt,该函数会清除计时,重新开始定时。如果不使用看门狗需要将看门狗关闭,以免程序复位。

设置步骤(如下代码所示):

wd_set_interval_ms(1000,sys_clk.pclk*1000);
wd_start();

Demo说明

通过Timer_Demo/app_config.h中的宏TIMER_MODE选择使用哪一种模式。

#define TIMER_SYS_CLOCK_MODE      1
#define TIMER_GPIO_TRIGGER_MODE   2
#define TIMER_GPIO_WIDTH_MODE     3

GPIO System Clock Mode

Demo设置:

Timer0,设置为system clock mode,设置initial_tick=0,capture value=50ms,使能timer0。中断中会用LED2进行翻转。

执行结果:

LED2会每50ms进行反转,结果如下:

通道1(LED2)是中断标识GPIO,大约50ms左右,反转一次。

GPIO System Clock Mode

GPIO Trigger Mode

Demo设置:

Timer0,设置为gpio trigger mode,初始化GPIO,将SW1配置为timer0的时钟源,上升沿触发,设置initial_tick=0,capture value=0xf,使能timer0。将GPIO_PA2和SW1两个引脚互连,GPIO_PA2每500ms产生一个上升沿,达到capture value,进入中断。中断中会用GPIO_PB5进行翻转。

执行结果:

GPIO_PA2每产生15个上升沿,LED2反转一次,结果如下:

通道1(LED2):是中断标识GPIO,GPIO_PA2每产生15个上升沿,LED2产生一次翻转。

通道0(GPIO_PA2):是触发信号引脚GPIO_PA2。

GPIO Trigger Mode

接下来,对红框进行详细说明:

红框1

红框2

从红框1、2可以看出,当上一次中断的第15个上升沿发生以后,大约时延4.25us,就会进入当前中断,LED2翻转,当前LED2持续15个上升沿之后,大约时延4.25us,就会进入下一次中断。

GPIO Pulse Width Mode

Demo设置:

Timer0,设置为gpio pulse width mode,初始化GPIO,将SW1配置为timer0的触发源,下降沿触发,设置initial_tick=0,capture value=0,使能timer0。将GPIO_PA2和SW1两个引脚互连,GPIO_PA2产生下降沿,触发定时器计时,延时250ms,GPIO_PA2产生上升沿,进入中断。中断中会用LED2进行翻转。

执行结果:

GPIO_PA2产生上升沿时,LED2反转,结果如下:

通道0(GPIO_PB4):是触发源GPIO_PA2的波形。

通道1(LED2):是中断标识GPIO,检测到GPIO_PA2产生上升沿,发生中断,LED2产生翻转。

GPIO Pulse Width Mode

红框说明:

红框说明

产生中断的时延4.75us,CPU进入中断需要一定的软件和硬件处理的时间。

读取定时器的计时寄存器,结果如下:

计时寄存器

对0x005b8e01十六进制转化为十进制,结果是6000129,使用的是系统时钟,频率为24M,每1/24M秒计数一次,6000129*(1/24M)计算得到大约是250ms。

Tick Mode

Demo设置:

Timer0,设置为tick mode,设置initial_tick=0,capture value=0,使能timer0。每隔500ms,手工设置定时器计时从头开始,LED2翻转一次。

执行结果:

LED2会每500ms进行反转,结果如下:

通道1(LED2):是tick mode标识GPIO,大约500ms左右,反转一次。

Tick Mode

Watchdog Mode

(1) 喂狗测试

Demo设置:

设置看门狗时间,reg_wt_target=1000ms。每隔990ms,喂狗,LED2翻转一次。

执行结果:

LED2会每隔990ms进行反转,结果如下:

通道1(LED2):是喂狗测试标识GPIO,每隔990ms左右,反转一次。

喂狗测试

(2) 不喂狗测试

如果在看门狗设定的时间内没有喂狗:

Demo设置:

delay_ms(990);

//wd_clear_cnt(); //将喂狗操作取消

gpio_toggle(LED2);

执行结果:

LED2会周期性输出低电平991.3847ms,高电平10.0074ms的波形,结果如下:

通道1(LED2):是不喂狗测试标识GPIO。

不喂狗测试

不喂狗测试

结果说明:看门狗设定的时间为1000ms,延时990ms之后,翻转LED2,由于没有喂狗,LED2高电平状态维持10ms之后,达到看门狗设定的时间,程序复位,一直重复以上的操作。

Analog

analog驱动是用于读写模拟寄存器的,支持单个Byte/Hword/Word,数据Buffer、DMA通道读写的功测试结果。

注意事项

使用DMA从模拟寄存器读数据到Buffer时,对应的目的地址的Buffer大小一定要是4的倍数。

原因:每次DMA都会送到Buffer 4个Byte。即使配置读取长度不够4,也会往目的地址写4个Byte。

例如:定义数组Buffer大小为5Byte,配置DMA从模拟寄存器读取5Byte到Buffer,这时候DMA实际上传送了2次共8个Byte到Buffer,多的3个Byte数据会从数组溢出,溢出的数据会覆盖别的变量。这时如果配置数组大小为8Byte,多的3个Byte数据就会存放在数组中,不会溢出,避免了潜在的风险。

速度测试

在cclk、pclk、hclk 都设置为24MHz条件下,加载各接口函数到RAM中运行,分别测得各模式读写4byte和8byte花费的时间如下表:

Mode Write 4byte时间 Read 4byte时间
ALG_WORD_MODE 6us 12.9us
ALG_DMA_WORD_MODE 8.4us 10.2us
ALG_DMA_BUFF_MODE 8.4us 12.4us
ALG_BUFF_MODE 10us 12us
ALG_HWORD_MODE 12.8us 17.6us
ALG_DMA_ADDR_DATA_MODE 13.1us 不支持
ALG_BYTE_MODE 22.6us 26.8us
Mode Write 8byte时间 Read 8byte时间
ALG_DMA_BUFF_MODE 11.2us 15.5us
ALG_BUFF_MODE 12.8us 16.6us
ALG_WORD_MODE 15.8us 19.3us
ALG_DMA_ADDR_DATA_MODE 18.9us 不支持
ALG_DMA_WORD_MODE 23.5us 20.1us
ALG_HWORD_MODE 24.8us 32.9us
ALG_BYTE_MODE 44.1us 53.9us

Flash

Flash是一种非易失性(Non-Volatile)内存,在没有电流的供应下也能长久的保持数据,其存储特性相当于硬盘。Flash可以对称为块的存储器单元进行擦写和再编程。任何flash器件的写入操作只能在空或者已擦除的单元内进行,所以在进行写入操作之前必须先执行擦除。

读操作

注意:

  • 当读取地址超过Flash的最高地址,还是可以读到值,但是读的地址是按照有效地址位进行计算的。如flash大小是1M bytes,则最大地址是0xfffff,即低20bit是有效地址位,如果读取0x100000地址的值,则是读取0x0地址的值。

写操作

注意:

  • 写之前必须要先擦除,最新的flash_write_page函数支持跨page写。

BQB

该章节介绍BQB 测试Demo的使用方法。

功能描述

  • 支持2-wire模式;

  • 支持BLE1M、BLE2M、BLE_LR_S2/S8;

  • 支持长包(payload为255);

  • 支持通过写Flash来手动调整频偏值;

  • 支持通过写Flash来手动调整串口的配置。

频偏值设置

Flash地址0x7e000(512K)、0xfe000(1M)、0x1fe000(2M)用来设置频偏值,写完频偏值之后通过复位来使设置生效。如果不写值(0xff)则为默认的频偏值。

通信验证

串口配置完成之后可以通过以下方式验证串口是否通信正常:

打开电脑端的串口工具,通过串口工具以16进制的格式向Eagle开发板发送“C0 00”,如果能够接受到开发板返回的“80 00”则说明通信正常。

PWM

PWM简介

SoC共有6路PWM:PWM0~PWM5。

PWM支持的相关模式介绍如下:

PWM0支持的模式:

  • continuous mode
  • counting mode
  • IR mode
  • IR FIFO mode
  • IR DMA FIFO mode

PWM1~PWM5支持的模式:

  • continuous mode

时钟

PWM的时钟源是有两路,可以选择pclk,也可以选择32K,如下图所示:

PWM的时钟源

pclk:

功能:可以进行分频,然后分频后的时钟作为PWM使用的时钟源。

接口配置:static inline void pwm_set_clk(unsigned char pwm_clk_div);

其中:pwm_clk_div = pclk_frequency /pwm_frequency -1;(pwm_clk_div:0~255)

32K:

功能:不支持分频,并且只支持continuous mode、counting mode。该配置主要是为了实现suspend模式下也能发PWM波形。

接口配置:static inline void pwm_32k_chn_en(pwm_clk_32k_en_chn_e pwm_32K_en_chn);

注意:

  • 所有通道默认pclk时钟源,如果想使用32K时钟源,调用pwm_32k_chn_en使能对应通道,没有使能的通道仍是pclk时钟源。
  • 32K时钟源, PWM设计的时候只考虑了suspend场景,在continuous mode、counting mode下使用中断,会提前一个32K时钟周期进入中断,在这32K时钟周期,会出了中断又会进入中断。使用32K PWM时如果需要中断,建议可以用GPIO中断来实现。
  • 32K时钟源时,在运行过程中如果需要更新占空比,只调用设置占空比的函数不会生效,必须设置好占空比后,再调用如下函数后,才会生效。

具体函数接口如下:

static inline void pwm_32k_chn_update_duty_cycle(void);

占空比

PWM的一个signal frame由两个部分组成,分别为Count status(高电平时间)和Remaining status(低电平时间),一个信号帧的具体波形如下,其中tmax是周期时间。

信号帧的具体波形

驱动中设置signal frame周期和占空比的函数均使用的是tcmp和tmax作为参数。

a) 通用的设置占空比的函数接口(支持所有通道):

static inline void pwm_set_tcmp(pwm_id_e id, unsigned short tcmp);
static inline void pwm_set_tmax(pwm_id_e id, unsigned short tmax);

pwm_set_tcmp:

id:选择哪个PWM通道;

tcmp:设置高电平持续时间。

pwm_set_tmax:

id:选择哪个PWM通道;

tmax:设置周期。

注意:

  • 在pwm_set_tmax函数的第二参数设置PWM的周期,参数类型是short型,tmax的最小取值为1,不能为0,如果为0,则pwm处于不工作的状态,所以tmax的取值范围为:1~65535。
  • 在pwm_set_tcmp函数的第二参数设置PWM的占空比,参数类型是short型,tcmp的最小取值可以为0,当为0的时候,这时pwm的波形一直是低电平,最大取值可以为tmax,这时pwm的波形一直是高电平,所以tcmp的取值范围为:0~tmax。

b) 当使用PWM0的IR FIFO Mode、IR DMA FIFO Mode时,还会使用到另外一个函数接口:

static inline void pwm_set_pwm0_tcmp_and_tmax_shadow(unsigned short tmac_tick, unsigned short cmp_tick);

注意:

  • 在pwm_set_pwm0_tcmp_and_tmax_shadow函数中,参数tmac_tick设定pwm0的周期,参数cmp_tick设定pwm0的高电平持续时间,tmac_tick的取值范围:1 \~ 65536,cmp_tick的取值范围:0 \~ cycle_tick。

c) 当使用PWM0的counting mode、IR mode时,pwm0需要设定脉冲的输出个数功能,使用到的函数接口:

static inline void pwm_set_pwm0_pulse_num(unsigned short pulse_num);

pulse_num:脉冲的个数。

d) 当pwm0向fifo中写入cfg data,使用到的函数接口:

static inline void pwm_set_pwm0_ir_fifo_cfg_data(unsigned short pulse_num, unsigned char use_shadow, unsigned char carrier_en);

use_shadow:

1:使用pwm_set_pwm0_tcmp_and_tmax_shadow函数下设置的周期和占空比。

0:使用pwm_set_tmax、pwm_set_tcmp函数下设置的周期和占空比。

carrier_en:

1:按照参数pulse_num、use_shadow的设置输出pulse。

0:输出低电平,持续时间按照参数pulse_num、use_shadow进行计算。

Invert/polarity

(1) 通过 pwm_set_tcmp和 pwm_set_tmax设定的波形,默认情况下先输出高电平Count status,后输出低电平Remaining status。

(2) 如果使用PWM*_PIN,输出的波形与通过 pwm_set_tcmp和 pwm_set_tmax设定的波形一致。

(3) 如果使用PWM*_N_PIN,输出的波形与PWM*_PIN波形相反。

(4) 如果使用pwm_invert_en使能PWM*通道的invert功能,将会翻转PWM*_PIN波形。

(5) 如果使用PWM*通道的invert功能,将会翻转PWM*_N_PIN波形。

(6) 如果使用pwm_n_invert_en使能PWM*通道的polarity功能,所有的PWM_PIN都会按照如下规则输出:Count status输出低,Remaining status输出高。

功能说明

Continuous mode

该模式会一直持续按照设置的占空比发送signal。如果想停止则设置stop,设置之后则立刻停止。在发送期间,可以更新占空比,占空比后会在下一个frame生效。

Counting Mode

发送设置数量的signal frame则停止。该模式下如果stop会立刻停下。该模式下发送过程中,修改占空比,不会改变占空比。

IR Mode

IR mode会连续发送pulse groups。中间可以改变占空比,会在下一个pulse group生效。如果想立刻停止,可以直接stop。IR mode与count的区别,count发送一个pulse groups就停止不再发送,而IR mode会持续不断地发送给pulse groups。

如果想停止IR mode并想完成当前的pulse group,则可以切换到counting mode。

如果想立刻停止,可以直接stop。

注意:

  • 如果想停止IR mode并想完成当前的pulse group,则在中断中可以切换到counting mode,但在切换的过程中,会在当前IR mode模式下的pulse group发完,才会切换过来。

IR FIFO mode

在没有MCU的干预下,可以发送长代码模式,IR的载波频率是由系统时钟分频得到的,可以支持通用频率,“Fifo cfg data”这个element作为IR波形的基础单元,硬件会解析cfg信息发出对应的signal。

IR FIFO Mode依次取出FIFO中的cfg data并发出对应signal,直到fifo为空为止。在该模式下,可以使用stop,但只是停止当前cfg data的执行,不影响fifo后面的cfg data执行。

注意:

  • 在IR FIFO模式下,只要fifo有数据就会一直往外发送(自动发送),不需要start信号,同时IR DMA FIFO Mode也不需要start信号,但在其他模式下,都需要pwm_start信号。
  • 每调用函数pwm_set_pwm0_ir_fifo_cfg_data,则FIFO的cnt会加1(如果这时FIFO为满,就会等待,直到FIFO不满,就会写入),硬件从FIFO中取出一个,则FIFO中的cnt会减1。FIFO的深度为8bytes。数据从FIFO中取出后,执行发送signal动作,只有当前signal执行完之后,才会从FIFO中取出下一个。

IR DMA FIFO mode

IR DMA FIFO模式,与IR FIFO模式相似,只是配置不是直接由MCU写在FIFO中,而是通过DMA写到FIFO中。

注意:

  • 在中断里面需要更新DMA的部分配置:源地址的更新,DMA触发等。
  • 在该模式下,与 IR FIFO不同的地方,并不是FIFO中的cfg data数量为空的时候触发中断,而是将fifo中的配置pwm信号帧全部执行完才会触发中断。

中断

PWM支持的中断设置说明如下(硬件不会自动清除中断标志位,需要软件手工清除)。

PWM0支持的中断:

a) FLD_PWM0_FRAME_DONE_IRQ:每个signal frame完成,会产生中断。

b) FLD_PWM0_PNUM_IRQ:每发完一个pulse groups,则会产生中断。

c) FLD_PWM0_IR_FIFO_IRQ:当FIFO里面的cfg data小于(不包括等于)设置的值(trigger_level)时,进入中断。

d) FLD_PWM0_IR_DMA_FIFO_IRQ:当FIFO执行完DMA发送的cfg data之后,进入中断。

PWM1支持的中断:

a) FLD_PWM1_FRAME_DONE_IRQ:每个signal frame完成,会产生中断。

PWM2支持的中断:

a) FLD_PWM2_FRAME_DONE_IRQ:每个signal frame完成,会产生中断。

PWM3支持的中断:

a) FLD_PWM3_FRAME_DONE_IRQ:每个signal frame完成,会产生中断。

PWM4支持的中断:

a) FLD_PWM4_FRAME_DONE_IRQ:每个signal frame完成,会产生中断。

PWM5支持的中断:

a) FLD_PWM5_FRAME_DONE_IRQ:每个signal frame完成,会产生中断。

一个脉冲组(pluse groups)包含几个frame可以通过pwm_set_pwm0_pulse_num函数接口进行配置:

static inline void pwm_set_pwm0_pulse_num(unsigned short pulse_num).

IR FIFO模式trigger_level的值可以通过pwm_set_pwm0_ir_fifo_irq_trig_level函数接口进行配置:

static inline void pwm_set_pwm0_ir_fifo_irq_trig_level(unsigned char trig_level).

注意:

  • 响应中断时,存在时延,时间大约在2~4us左右。

Continuous mode

功能说明

该demo实现的功能如下:LED1会持续发送signal frame(高电平时间为50us,周期为100us),同时,每产生一次中断,LED4会翻转一次(该GPIO是为了做中断测试设置的一个标识信号)。

实例结果

Continuous mode实验结果

上图为使用逻辑分析仪抓到的实验结果:

通道0(LED1):是PWM输出信号。

通道1(LED4):是中断标识GPIO,每发送一个信号帧,产生一次中断。

红框说明:产生中断的时延,CPU进入中断需要一定的软件和硬件处理的时间。

其他验证结果

(1) Stop

使用下面实验验证continuous mode下,执行stop后,signal会立刻停止。

代码实现如下,在stop之后,反转LED3的状态。

pwm_start(PWM_ID);
delay_ms(1);
pwm_stop(PWM_ID);
gpio_toggle(LED3);

使用逻辑分析仪抓到的实验结果:

通道0(LED1):是PWM输出信号。

通道1(LED3):是stop标识GPIO。

由图中可以看到,LED3翻转后,PWM的signal立刻停止。

其他验证结果

(2) 占空比

使用下面实验验证continuous mode下,在发送期间,可以更新占空比,更新占空比后会在下一个frame生效。代码实现如下,在修改占空比的状态之后,反转LED3的状态。

pwm_start(PWM_ID);
delay_ms(1);
pwm_set_tcmp(PWM_ID,10 * CLOCK_PWM_CLOCK_1US);
gpio_toggle(LED3);

使用逻辑分析仪抓到的实验结果:

通道0(LED1):是PWM输出信号。

通道1(LED3):是占空比更新标识GPIO。

由图中可以看到,LED3翻转后,PWM修改占空比后,会在下一个frame生效。

更新占空比

Counting mode

通过使用app_pwm_count.c中的宏选择哪一种中断方式。

#define COUNT_FRAME_INIT 1
#define COUNT_PNUM_INIT 2
#define SET_COUNT_INIT_MODE COUNT_FRAME_INIT

COUNT_FRAME_INIT

功能说明:

该demo实现的功能如下,LED1会输出数量为16的signal frame(高电平时间为50us,周期为100us),同时,每发送一个信号帧,产生一次中断,LED4会翻转一次(该GPIO是为了做中断测试设置的一个标识信号)。

实例结果:

COUNT_FRAME_INIT实例

通道0(LED1):是PWM输出信号。

通道1(LED4):是中断标识GPIO,每发送一个信号帧,产生一次中断。

红框说明:产生中断的时延,CPU进入中断需要一定的软件和硬件处理的时间。

COUNT_PNUM_INIT

功能说明:

该demo实现的功能如下: LED1会输出数量为16的signal frame(高电平时间为50us,周期为100us),当发送完指定的脉冲个数之后,产生中断,LED4翻转(该GPIO是为了做中断测试设置的一个标识信号)。

实例结果:

COUNT_PNUM_INIT实例

红框的具体情况如下:

红框具体情况

通道0(LED1):是PWM输出信号。

通道1(LED4):是中断标识GPIO,指定脉冲个数发完,产生中断。

红框说明:比50us多3us,说明进入中断有一定的时延,CPU进入中断需要一定的软件和硬件处理的时间。

其他验证结果

(1) Stop

使用下面实验验证counting mode下,执行stop后,signal会立刻停止。

代码实现如下,在stop之后,反转LED3的状态。

pwm_start(PWM_ID);
delay_ms(1);
pwm_stop(PWM_ID);
gpio_toggle(LED3);

使用逻辑分析仪抓到的实验结果:

通道0(LED1):是PWM输出信号。

通道1(LED3):是stop标识GPIO。

由图中可以看到,LED3翻转后,PWM的signal立刻停止。

其他验证结果

(2) 占空比

使用下面实验验证counting mode下,在发送期间,更改占空比,占空比不可以改变。

代码实现如下,在修改占空比的状态之后,反转LED3的状态。

pwm_start(PWM_ID);
delay_ms(1);
pwm_set_tcmp(PWM_ID,10 * CLOCK_PWM_CLOCK_1US);
gpio_toggle(LED3);

使用逻辑分析仪抓到的实验结果:

通道0(LED1):是PWM输出信号。

通道1(LED3):是占空比更新标识GPIO。

由图中可以看到,LED3翻转后,PWM的signal没有发生改变。

实验结果如下:

更改占空比

IR mode

功能说明:

该demo实现的功能如下,LED1会输出6个脉冲组(每个脉冲组脉冲个数为4,高电平时间为50us,周期为100us),同时,每发完一个pulse groups,产生一次中断,LED4会翻转一次(该GPIO是为了做中断测试设置的一个标识信号)。

实例结果:

IR mode实例

上图为使用逻辑分析仪抓到的实验结果:

通道0(LED1):是PWM输出信号。

通道1(LED3):是中断标识GPIO,一个脉冲组发完翻转一次。

红框说明:产生中断的时延,CPU进入中断需要一定的软件和硬件处理的时间。

其他验证结果

(1) Stop

使用下面实验验证IR mode下,执行stop后,signal会立刻停止。

代码实现如下,在stop之后,反转LED3的状态。

pwm_start(PWM_ID);
delay_ms(1);
pwm_stop(PWM_ID);
gpio_toggle(LED3);

使用逻辑分析仪抓到的实验结果:

通道0(LED1):是PWM输出信号。

通道1(LED3):是stop标识GPIO。

由图中可以看到,LED3翻转后,PWM的signal立刻停止。

其他验证结果

(2) 占空比

使用下面实验验证IR mode下,中间可以改变占空比,但会在当前pulse group执行完生效。

代码实现如下:

pwm_start(PWM_ID);
delay_ms(1);
pwm_set_tcmp(PWM_ID,10 * CLOCK_PWM_CLOCK_1US);
gpio_toggle(LED3);

使用逻辑分析仪抓到的实验结果:

通道0(LED1):是PWM输出信号。

通道1(LED3):是占空比更新标识GPIO。

由图中可以看到,LED3翻转后,pwm的signal并没有立刻改变,而是在当前pulse group执行完生效。

更改占空比

IR FIFO Mode

功能说明:

IR FIFO MODE的demo的大致流程介绍:一开始先向FIFO写入两组cfg data1, cfg data2, 当FIFO里面的cfg data小于(不包括等于)设置的值(trigger_level为1)进入中断,在中断配置了相同的两组cfg data1, cfg data2。

该demo实现的功能如下,LED1以两组脉冲组为单位依次不断地发送,两组脉冲组的设置如下:

(1) cfg data1, high level time of 50us, period of 100us

(2) cfg data2, high level time of 100us, period of 200us

每产生一次中断,LED4会翻转一次(该GPIO是为了做中断测试设置的一个标识信号)。

实例结果:

IR FIFO mode实例

通道0(LED1):是PWM输出信号。

通道1(LED4):是中断标识GPIO,当FIFO里面的cfg data小于(不包括等于)设置的值(trigger_level为1)的时候翻转一次(执行完cfg data1,从FIFO取出cfg data2时,cnt的值为0,小于trigger level(值为1),进入中断)。

红框说明:产生中断的时延,CPU进入中断需要一定的软件和硬件处理的时间。

其他验证结果

(1) Stop

使用下面实验验证IR FIFO mode下,执行stop后,只是停止当前cfg data的执行,不影响fifo后面的cfg data执行。

代码实现如下,在stop之后,反转LED3的状态。

delay_ms(10);
pwm_stop(PWM_ID);
gpio_toggle(LED3);

使用逻辑分析仪抓到的实验结果:

通道0(LED1):是PWM输出信号。

通道1(LED3):是stop标识GPIO。

在IR FIFO MODE功能说明中:

cfg data1: 脉冲个数为5,高电平时间为50us,周期为100us。

cfg data2: 脉冲个数为6,高电平时间为100us,周期为200us。

由图中可以看到,执行stop后,LED3反转,停止当前cfg data1的执行,不影响fifo后面的cfg data2的执行。

其他验证结果

DMA FIFO mode

通过使用app_pwm_ir_dma.c中的宏选择工作方式。

#define     PWM_IR_FIFO_DMA     1
#define     PWM_CHAIN_DMA       2
#define     SET_PWM_DMA_MODE    PWM_IR_FIFO_DMA

PWM_IR_FIFO_DMA

在IR DAM FIFO Mode中,需要不断触发中断请求DMA将cfg data发送到FIFO中。

IR DMA FIFO Mode程序大致流程:在主程序中通过DMA请求,向FIFO中传送三组cfg data,分别为cfg data1、cfg data2、cfg data3,当FIFO中的cfg data全部执行完之后,进入中断,在中断通过DMA请求,向FIFO中传送两组cfg data,分别为cfg data4、cfg data5。

功能说明:

该demo实现的功能如下,LED1一开始会发送一个三组脉冲组:

cfg data1:脉冲个数为5,高电平时间为50us,周期为100us。

cfg data2:脉冲个数为4,高电平时间为50us,周期为100us。

cfg data3:脉冲个数为6,高电平时间为100us,周期为200us。

接下来,会以两组脉冲为单位不断发送两组脉冲组:

cfg data4:脉冲个数为4,高电平时间为50us,周期为100us。

cfg data5:脉冲个数为4,高电平时间为50us,周期为100us。

同时,每产生一次中断,LED4会翻转一次(该GPIO是为了做中断测试设置的一个标识信号)。

实例结果:

PWM_IR_FIFO_DMA实例

通道0(LED1):是PWM输出信号。

通道1(LED4):是中断标识GPIO。

从上图可以看出,在该模式下,当FIFO的cfg data数据全部执行完才触发中断,与IR FIFO Mode的中断机制不一样的。

下图为上图中红色框的放大框,从图中可以看出cfg data3的最后一个低电平的维持时间是105us(并不是cfg data3设置的100us),所以可以看出,在这种使用方法下,发完第一组DMA数据后,中断中重新触发DMA,在signal上是有一段时延的。(这个实例相当于实际输出是这样的:cfg1+cfg2+cfg3+delay(低电平5us左右)+cfg4+cfg5),这5us的时间包括进入中断以及重新设置相关设置的时间。

PWM_IR_FIFO_DMA实例放大框

PWM_CHAIN_DMA

SoC芯片的DMA有一种链表形式,可以结合使用到DMA FIFO mode中,这个的好处就是,可以没有MCU干预,一直反复的发送想要的signal。在前面的IR DAM FIFO Mode例子中,需要不断触发中断请求DMA将cfg data发送到FIFO中,使用链表的方法在不需要中断参与的情况下,也可以完成连续发送的功能。

在pwm_chain_dma的例子中,链表结构如下:

PWM_CHAIN_DMA链表结构

先建立头结点head_of_list,然后向循环链表添加结点,在demo中,添加了两个循环链表结点,分别是tx_dma_list[0]和tx_dma_list[1]。

注意:

  • 头结点需要配置DMA源地址、DMA长度、下一个结点配置所在地址,不能仅仅配置下一个结点配置所在的地址。

从流程图可以看出,先开始执行头指针,然后执行tx_dma_list[0],然后执行tx_dma_list[1],再执行tx_dma_list[0],依次循环执行,直到LLP设置为0才会停止。

这只是实现两个循环链表通过DMA实现不断连续发送,如果想实现更多数组循环发送,可以按照以上的结构添加相对应的指针结构体即可。

具体DMA链表配置如下:

pwm_set_dma_config(DMA_CHN);
pwm_set_dma_chain_llp(DMA_CHN,(u16*)(&CHIAN_DMA_Buff[0]),MIC_BUFFER_SIZE,&tx_dma_list[0]);
pwm_set_tx_dma_add_list_element(DMA_CHN,&tx_dma_list[0],&tx_dma_list[1],(u16*)(&CHIAN_DMA_Buff[0]),CHAIN_BUFFER_SIZE);
pwm_set_tx_dma_add_list_element(DMA_CHN,&tx_dma_list[1],&tx_dma_list[0],(u16*)(&CHIAN_DMA_Buff[1]),CHAIN_BUFFER_SIZE)
pwm_ir_dma_mode_start(DMA_CHN);

DMA链表与PWM_IR_FIFO_DMA的不同在于:

pwm_set_dma_chain_llp函数和pwm_set_tx_dma_add_list_element函数。

通过pwm_set_dma_chain_llp函数设置链表头:

void pwm_set_dma_chain_llp(dma_chn_e chn,u16 * src_addr, u32 data_len,dma_chian_config_t * head_of_list)

chn: DMA配置。

src_addr:DMA源地址,即cfg data数组。

data_len:DMA长度。

head_of_list:下一个结点配置的所在地址。

每个结点中包括的内容:DMA配置、当前结点配置的所在地址、下一个结点配置所在地址、DMA源地址,DMA长度,通过pwm_set_tx_dma_add_list_element函数设置循环链表的结点:

void pwm_set_tx_dma_add_list_element(dma_chn_e chn, dma_chian_config_t *config_addr, dma_chian_config_t *llponit , u16 * src_addr, u32 data_len)

chn:DMA配置。

config_addr:当前结点配置的所在地址。

llponit:下一个结点配置的所在地址。

src_addr:DMA源地址,即cfg data数组。

data_len:DMA长度。

功能说明:

该demo实现的功能如下,LED1一开始会先发送头结点配置的cfg data数组,然后会以结点1配置的cfg data数组和结点2配置的cfg data数组交替循环执行不断发送。

结点1配置的cfg data情况如下:

cfg data1: 脉冲个数为5,高电平时间为100us,周期为200us。

cfg data2: 脉冲个数为4,高电平时间为100us,周期为200us。

cfg data3: 脉冲个数为6,高电平时间为100us,周期为200us。

cfg data4: 脉冲个数为3,高电平时间为100us,周期为200us。

结点2配置的cfg data情况如下:

cfg data 5: 脉冲个数为5,高电平时间为50us,周期为100us。

cfg data6: 脉冲个数为4,高电平时间为50us,周期为100us。

cfg data7: 脉冲个数为6,高电平时间为50us,周期为100us。

cfg data8: 脉冲个数为3,高电平时间为50us,周期为100us。

实例结果:

PWM_CHAIN_DMA实例

通道0(LED1):是PWM输出信号。

对红框进行详细说明,指针1和指针2的cfg data在切换的过程中,没有时延的产生。

红框详细说明1

红框详细说明2

由图可得,没有延时的产生,所以使用链表的方式可以发出连续的PWM波形。

I2C

简介

I2C,由数据线SDA和时钟SCL构成的串行总线,可发送和接收数据,是半双工通信方式。时钟是由master端提供控制的。I2C通信协议具体如下:

I2C通信协议

I2C通信协议具体介绍如下:

状态 过程
空闲状态 I2C总线的SDA和SCL两条信号线同时处于高电平时,规定为总线的空闲状态。
开始信号 当SCL为高期间,SDA由高到低的跳变。
停止信号 当SCL为高期间,SDA由低到高的跳变。
应答信号 对于反馈有效应答位ACK的要求是,接收器在第9个时钟脉冲之前的低电平期间将SDA线拉低,并且确保在该时钟的高电平期间为稳定的低电平。如果接收器是主控器,则在它收到最后一个字节后,发送一个NACK信号,以通知被控发送器结束数据发送,并释放SDA线,以便主控接收器发送一个停止信号P。
数据的有效性 I2C总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。即:数据在SCL的上升沿到来之前就需准备好。并在在下降沿到来之前必须稳定。
数据传输 在I2C总线上传送的每一位数据都有一个时钟脉冲相对应(或同步控制),即在SCL串行时钟的配合下,在SDA上逐位地串行传送每一位数据。

中断

I2C模式下的中断相关介绍:

I2C no-DMA模式:

相关中断:

a) I2C_RX_BUF_MASK:与rx_irq_trig_lev设置相关,当fifo_data_cnt>=rx_irq_trig_lev产生中断。

b) I2C_RX_DONE_MASK:当接收完发送的数据之后,产生中断。

是否手动清除中断标志位:

I2C_RX_BUF_MASK、I2C_RX_DONE_MASK无需手动清除中断标志位,当不满足条件的时候就会自动清除。

I2C DMA模式:

相关中断:

a) TC_MASK:当DMA传输完接收的数据之后,DMA的TC中断就会起来。

b) I2C_TX_DONE_MASK:当发送完数据之后,产生中断。

是否手动清除中断标志位:

TC_MASK、I2C_TX_DONE_MASK需要手动清除中断标志位,否则就会一直进中断出中断。

注意:

  • 非DMA模式下,slave端使用中断接收数据时,为了兼容所有长度可能,slave必须同时使用I2C_RX_BUF_MASK和I2C_RX_DONE_MASK才能判断收完一帧。原因是:
    (1)I2C_RX_DONE_MASK 如果数据长度是trigger level的倍数的时候,捕捉不到;
    (2)I2C_RX_BUF_MASK在数据长度不是trigger level倍数的时候,尾包中断检测不到。
  • 在DMA模式下,I2C_RX_DONE_MASK中断标志位捕捉不到,使用DMA的TC_MASK中断代替。
  • DMA的所有中断MASK默认都是打开的,需要将其他不用的MASK关掉,以免影响实验的结果。
  • 使用I2C_TX_DONE_MASK中断之前,需要手动清除I2C_TX_DONE_CLR状态,不清除的话,会一直进中断。
  • I2C_TX_DONE_MASK中断不代表一帧发送完毕(只是表示数据部分发送完毕,没有等stop信号),产生I2C_TX_DONE_MASK中断后,再查询busy信号,直到IDLE才代表结束。

I2C mode

芯片支持作为master也支持作为slave。如果作为master,需要进行master初始化,并且需要设置时钟信号,接口如下:

void i2c_master_init(void);
void i2c_set_master_clk(unsigned char clock);

如果作为slave,则调用如下接口进行初始化,每个设备有唯一的地址(id):

void i2c_slave_init(unsigned char id).

I2C no-DMA mode

(1) Master

master发送、接收数据的相关接口配置如下:

unsigned char i2c_master_write(unsigned char id, unsigned char *data, unsigned char len);
unsigned char i2c_master_read(unsigned char id, unsigned char *data, unsigned char len);

i2c_master_write、i2c_master_read的功能如下:

master读写过程中,slave都正常响应(回ACK)的情况下,结果显示如下:

master读写过程中,slave正常响应的结果

master读写过程中,slave正常响应的结果

master发完地址帧后,如果slave端回NACK,则master会发stop停止传输,结果显示如下:

master发完地址帧后,slave端回NACK的结果

master发完地址帧后,slave端回NACK的结果

如果数据阶段,slave回NAK的话,master还是会正常发完剩余的数据。结果显示如下:

数据阶段,slave回NAK的结果

注意:

  • 目前仅mcu mode支持地址帧后检测到NACK则停止的功能,dma模式不支持,dma模式不管slave端是否会NAK,会将整帧数据发完。

(2) Slave

slaver接收、发送数据的相关接口配置如下:

void i2c_slave_write(unsigned char* data , unsigned char len ).
void i2c_slave_read(unsigned char* data , unsigned char len ).

Slave端接收数据:可以用中断方式,在非DMA模式下的时候,使能rx(I2C_RX_BUF_MASK)中断和rx_done(I2C_RX_DONE_MASK)中断,用来判断是否接收完一帧数据。

Slave端中断的具体配置如下:

i2c_set_irq_mask(FLD_I2C_MASK_RX|FLD_I2C_MASK_RX_DONE);
i2c_rx_irq_trig_cnt(SLAVE_RX_IRQ_TRIG_LEVEL);
core_interrupt_enable();
plic_interrupt_enable(IRQ21_I2C);

注意:

  • 目前存在的问题:I2C作为slave时,软件不能区分master发过来的是读指令还是写指令。Slave端不知道是读还是写,所以slave需要在maste端发送读指令之前,提前将数据写到FIFO中。但是从应用角度来讲,因为不知道master端什么时候来读数据,提前塞数据的时间是很难控制的。
  • 因此会导致如下问题:在非DMA模式下,slave端需要在master发送读指令之前,提前将发送的数据放在fifo中。fifo的大小只有8个字节,如果master不读走的话,就会一直卡在i2c_slave_write函数中,所以要使用该功能时,需要软件层面控制好时间。

I2C DMA mode

(1) Master

master发送、接收数据的相关接口配置如下:

void i2c_master_write_dma(unsigned char id, unsigned char *data, unsigned char len);
void i2c_master_read_dma(unsigned char id, unsigned char *rx_data, unsigned char len);

在master端判断是否发送完、接收完数据,使用的相关接口如下:

static inline bool i2c_master_busy(void).

注意:

  • TX_DONE中断不代表一帧发送完毕(只是表示数据部分发送完毕,没有等stop信号),产生TX_DONE中断后,再查询busy信号,直到idle才代表结束。

(2) Slave

slave发送、接收数据的相关接口配置如下:

void i2c_slave_read_dma(unsigned char *data, unsigned char len);
void i2c_slave_write_dma(unsigned char *data, unsigned char len);

slave端接收中断需要配置DMA TC_MASK,具体配置如下:

i2c_clr_txdone_irq_status (I2C_TX_DONE_CLR);
i2c_set_irq_mask(I2C_TX_DONE_MASK);
core_interrupt_enable();
plic_interrupt_enable(IRQ21_I2C);

slave端发送中断需要配置I2C_TX_DONE_MASK ,具体配置如下:

dma_set_irq_mask(I2C_RX_DMA_CHN, TC_MASK);

注意:

  • Master端向slave端发送读指令,slave端需要在master发送读指令之前,使用i2c_slave_write_dma。
  • 函数提前将发送的数据放在DMA中。(注,需要保证此次填入就是需要回复的数据,因为调用该函数后,DMA就会将指定的buffer数据放在fifo中,等master的clock来了之后将数据读出)。

I2C demo说明

通过app.c和app_dma.c中的宏I2C_MASTER_WRITE_READ_MODE选择使用哪一种模式。

#define I2C_MASTER_WRITE_READ_NO_DMA            1

#define I2C_MASTER_WRITE_READ_DMA               2

#define I2C_MASTER_WRITE_READ_MODE              I2C_MASTER_WRITE_READ_NO_DMA

通过宏I2C_DEVICE选择master和slave模式。

#define I2C_MASTER_DEVICE             1

#define I2C_SLAVE_DEVICE              2

#define I2C_DEVICE                    I2C_MASTER_WRITE_READ_NO_DMA

注意:

  • 通过两个板子测试I2C通信时,当将master端和salver端的代码都烧到板子之后,将两个板子都断电,先给slave端上电,然后再给master上电(避免数据错误),另外两个板子之间需要共地。

功能说明

节点 功能
master 不断地发送写操作,读操作,然后将发送的数据与读取的数据进行比较,如果比较的结果不一样,LED2就会翻转。
slave 将接收到是数据返回给master。

示例结果

使用逻辑分析仪抓取I2C的发送数据和接收数据的时序,执行结果如下:

通道0:代表I2C的SCL信号。

通道1:代表I2C的SDA信号。

通道2:LED2 代表master端判断发送数据和接收数据是否一样的标志位,如果对比的数据不一样就会翻转。

master端发送数据:

Master端发送数据

master端接收数据:

Master端接收数据

特别说明:

(1) i2c write的时候,master在写完最后一个字节之后slave会回ACK,然后master发送stop信号结束通信。

(2) i2c read的时候,master在接收完slave发送的最后一个字节之后,slave端发送NACK信号,并释放SDA线,以便主控接收器发送一个停止信号P。

UART

简介

UART是一种异步全双工串行通信协议,由Tx和Rx两根数据线组成,由于没有时钟参考信号,所以使用UART的通信双方必须约定串口波特率、数据位宽、奇偶校验位、停止位等配置参数,从而按照相同的速率进行通信。

通常异步通信以一个字符为传输单位,通信中两个字符间的时间间隔多少是不固定的,但是在同一个字符中两个相邻位之间的时间间隔是固定的。当波特率为9600bps时,传输一个bit的时间间隔大约为104.16us,波特率为115200bps时,传输一个bit的时间间隔大约为8us。

数据传输速率用波特率来表示,即每秒传送的二进制位数。每一个字符由11位(1个起始位,8个数据位,1个校验位,1个结束位)组成,例如数据传输速率为120字符每秒,则其传送的波特率为11×120=1320字符/秒=1320波特。

数据通信时序

数据通信时序

其中各位的意义如下:

起始位:先发出一个逻辑“0”,表示传输字符的开始。

数据位:可以是5-8位的逻辑“0”或者“1”,如ASCII码(7位),扩展BCD码(8位),发送方式为小端传输,即LSB先发,MSB后发。

校验位:数据位加上这一位后,使得“1”的位数应为偶数(偶校验)或者奇数(奇校验)。

停止位:它是一个字符数据结束的标志,可以是1位,1.5位,2位的高电平(用于使双方同步,停止位时间越长,容错能力就越强)。

空闲位:处于逻辑“1”的状态,表示当前线路上没有数据传送。

异步通信时序

如上图,值得注意的是,异步通信是按照字符传输的,接受设备在收到起始信号之后只要在一个字符的传输时间内和发送设备保持同步就能正确接收。下一个字符起始位到来之后又需要同步重新校准(依靠检测起始位来实现发送与接收方的时钟自同步)。

通信原理

UART通信原理

以Telink SoC中UART模块为例,需要被发送的数据首先被芯片的MCU或者DMA写入TX缓冲区,然后UART模块通过TX引脚将TX缓冲区中的数据发送到其他设备。在接收数据的设备中,数据首先通过UART模块的RX引脚被写入RX缓冲区,然后数据被接收设备的MCU或者DMA读取。

如果芯片的RX缓冲区接近溢出状态,芯片会通过其RTS引脚发出一个信号(可以配置为高电平或者是低电平)给与其连接设备,即表明该设备应该停止继续发送数据。与其类似,如果芯片通过其CTS引脚接收到了一个信号,即表明其他设备的RX缓冲区接近溢出,芯片应该停止继续发送数据。

功能简介

初始化

注意事项如下:

(1) 每次使用UART端口时,最好都先调用uart_reset()函数复位一次,这样可以避免之前使用UART的操作对本次使用产生影响。(如UART相关寄存器遗留设置)

```C
uart_reset(uart_num_e uart_num)
```

(2) 使用uart_set_pin()用来设置UART中TX/RX引脚

```C
uart_set_pin(uart_tx_pin_e tx_pin,uart_rx_pin_e rx_pin)
```

**接线注意:** 当前设备的TX/RX与其他设备的TX/RX连接规则为TX-RX,RX-TX。

(3) DMA模式,注意选用没有被其他模块占用的DMA的通道

(4) NDMA模式,如果使用中断,需要使用如下配置:

a) uart_rx_irq_trig_level():用来设置接收字符中断触发个数level,如设置为1,则接收一个字符即进入中断一次。(推荐设置为1)。

b) uart_tx_irq_trig_level():用来设置发送字符中断触发level,当发送缓冲区字符个数达到level,即进入发送中断。设置为0表明缓冲区有数据即进行发送。

注意:

  • NDMA模式在使用中断接收数据时,建议在初始化时设置level=1(比较通用),原因如下: (1) NDMA模式接收数据时,可以在初始化时手动设置接收数据中断level,level值可以为1-4。我们默认设置level为1。如果将level设置为2,3,4,就需要接收数据的长度满足一定条件才能正常完成数据的接收。例如:设置level为2,如果实际接收数据长度为2的倍数,可以正常完成数据接收。但如果实际接收数据长度不是2的倍数,例如3,前两个数据可以正常接收,第三个数据会丢失。(NDMA模式从缓冲区取数据按照level的长度来取,如果最后一次取数据,长度达不到level,数据将取不出来)。level设置为3,4与之类似,需要接收数据长度为3,4的倍数,才能正常完成数据的接收。 (2) level设置为1时,需要注意:由于接收中断触发过于频繁,如果接收数据过快(波特率过高),可能会出现前一个数据还没有接收完,下一个数据已经到来的情况。如果出现这种情况,可以通过适当降低波特率来解决。

波特率

函数调用:

计算波特率会用到两个函数:

(1) uart_cal_div_and_bwpc()函数会根据输入的波特率和系统时钟计算出最佳的时钟分频数div和位宽bwpc。

```C
uart_cal_div_and_bwpc(unsigned int baudrate, unsigned int sysclk, unsigned short* div, unsigned char *bwpc)
```

(2) uart_init()将上面计算的时钟分频数div和位宽bwpc传入该函数,才真正的设置了波特率,同时该函数还设置了UART端口,设置停止位、校验位等。

```C
uart_init(uart_num_e uart_num, unsigned short div, unsigned char bwpc, uart_parity_e parity, uart_stop_bit_e stop_bit)
```

有一些应用对时序要求较高的,可以先计算好时钟分频数div和位宽bwpc,然后直接调用uart_init即可,这样省去了uart_cal_div_and_bwpc函数的执行时间。

实测数据:

在DMA和NDMA模式下,我们分别测试了不同波特率对数据传输准确性的影响。

测试数据:0x00,0x11, 0x22 …… 0xff共16个数据

测试条件:两块B91开发板TX/RX互连,每隔一秒通信一次(实验表明,无时间间隔和有时间间隔现象一致,这里为了现象更为稳定,采用1s的时间间隔),时钟设置为16MHZ-PCLK,16MHZ-HCLK,16MHZ-CCLK。

测试芯片:B91 80pin-EVK

测试程序:UART-DEMO。

NDMA模式

波特率
2000000 不通过 不通过
1500000 通过 通过
1000000 通过 通过
500000 通过 通过
256000 通过 通过
115200 通过 通过
57600 通过 通过
38400 通过 通过
19200 通过 通过
9600 通过 通过
4800 通过 通过
2400 通过 通过
1200 通过 通过
600 通过 通过
300 通过 通过

DMA模式

波特率
2000000 通过 通过
1500000 通过 通过
1000000 通过 通过
500000 通过 通过
256000 通过 通过
115200 通过 通过
57600 通过 通过
38400 通过 通过
19200 通过 通过
9600 通过 通过
4800 通过 通过
2400 通过 通过
1200 通过 通过
600 通过 通过
300 通过 通过

由测试可知,在300到1.5M波特率之间,NDMA和DMA都可以正常通信,在2M波特率下,DMA模式仍可以正常通信,但NDMA模式已经出现了错误(收发数据出现乱码)。

注意:

  • 进一步的测试表明,16MHZ-PCLK下DMA极限波特率可达5M。
  • 当PCLK提升到24MHZ,HCLK和CCLK提升到48MHZ时,NDMA模式极限波特率可达5M,DMA极限波特率可达8M,PCLK是UART波特率上限的主要影响因素。

中断

中断 产生条件 自动清除还是手动清除
TX DMA TC 每当接收一帧数据,会产生中断 需要手动清除。
UART_TXDONE NDMA和DMA共用。默认值为1,开始发送数据时会置0,当数据发送完成后,需要手动清除,硬件会自动置1 需要手动清除
UART_RXDONE(是否可以使用需要查看芯片差异) 该中断只能在DMA模式下使用,默认值为0,接受完一包数据会置1 需要手动清除
UART_RXBUF_IRQ_STATUS 当接收BUFF缓冲区数据量达到初始化时设置的level时,会产生中断 当缓冲区数据被读取后,该标志位自动清除。
UART_TXBUF_IRQ_STATUS 当发送BUFF缓冲区数据量达到初始化时设置的level时,会产生中断 当缓冲区数据被发送出去后,该标志位会自动清除。
UART_RX_ERR(是否可以使用需要查看芯片差异) (是否可以使用需要查看芯片差异)UART接收出错标志。当UART接收数据出错时(例如奇偶校验出错或停止位错误)会产生中断,中断后将停止接收数据 该标志位需要手动清除

DMA模式

发送数据:

使用该函数发送数据,该函数只是触发发送动作,并没有实际发完,需要使用查询或者中断的方式判断是否发完。

unsigned char uart_send_dma(uart_num_e uart_num,unsigned char * addr,unsigned char len)

查询

uart_send_dma(UART0, (unsigned char*)tx_byte_buff, 16);
while(uart_tx_is_busy(UART0));

需要使用uart_tx_is_busy接口查询当前状态,如果该标志位为0,则表明上一帧数据发送结束,可以进入到当前帧数据的发送中。

中断

使用TX_DONE中断,需要设置对应的mask:

uart_set_irq_mask(UART0, UART_TXDONE_MASK);

除了正常的使用中断相关函数的调用,还需要注意以下事项:

初始化的时候,需要调用下面函数(将TX_DONE信号置0,否则会一直进中断):

uart_clr_tx_done(UART0);

中断处理函数中需要按照如下方式处理:

_attribute_ram_code_sec_noinline_ void uart0_irq_handler(void)
{
     if(uart_get_irq_status(UART0,UART_TXDONE))//判断中断标志位
     {
         .........
         uart_clr_tx_done(UART0); //将TX_DONE信号置0
     }
 }

标志位:UART_TXDONE

使用中断方式时,产生UART_TXDONE中断,表示前一帧数据发送结束,可以进入到下一帧数据的发送中。

注意:

  • DMA模式使用中断方式发送数据时,我们需要利用UART_TXDONE的状态。由于UART_TXDONE的初始默认值是1,为了可以正常的产生中断,在初始化时使用uart_clr_tx_done(UART0)函数将UART_TXDONE置0,这样等发送完成UART_TXDONE会自动置1,此时进入UART_TXDONE中断,然后在中断的处理程序中使用uart_clr_tx_done(UART0)函数再将UART_TXDONE拉低。

接收数据:

使用DMA接收数据时,在初始化设有数据包接收结束的判断,即rx_timeout。

uart_set_dma_rx_timeout(uart_num_e uart_num, unsigned char bwpc, unsigned char bit_cnt, uart_timeout_mul_e mul)

其中,bwpc和bit_cnt设置传输一个byte所用时间,一个byte传输所需时间为(bwpc+1)*bit_cnt。mul设置超时时间,可以选择设置为0,1,2,3。mul设置为0表示接收时如果超过1个byte的时间没有接收到数据,则认为接收数据包结束。设置为1表示超过2个byte的时间没有接收到数据则认为数据包结束。

使用DMA模式接收数据时有两种方式:

(1) RX_DONE中断

RX_DONE中断接收数据是较为通用的方式,这种方式允许我们接收未知长度的数据包,但有些芯片不支持这种方式。芯片是否支持这种方式请查看UART的芯片差异章节。

(2) DMA中断

如果芯片不支持RX_DONE中断,可以使用DMA中断接收数据,但这种方式有使用限制,只能接收已知长度的数据包。

RX_DONE中断

使用RX_DONE中断,需要设置对应的mask。

uart_set_irq_mask(UART0, UART_RXDONE_MASK);

标志位:UART_RXDONE

使用中断方式接收数据时,产生RX_DONE中断,表示接收完数据,该标志位需要手动清除。

注意:

  • 使用DMA-RX_DONE中断时,在中断处理函数中设有接收数据长度rec_data_len的计算,这样在使用RX_DONE方式接收数据时,我们可以明确知道接收数据的长度。

DMA中断

通过中断标志位DMA_TC_IRQ,当有中断请求时,表示接收完数据,该标志位需要手动清除。

void uart_receive_dma(uart_num_e uart_num, unsigned char * addr,unsigned char rev_size)

注意:

  • 必须已知数据长度,才能产生中断。参数rev_size需要和接收数据的实际长度len满足一定关系,这样才不会有数据遗漏,并准确产生中断。(例如4*(n-1)<len≤4*n,则rev_size需要满足4*(n-1)<rev_size≤4*n,其中n为同一值)。(例如:rev_size设置为8,接收数据长度是5/6/7/8才会产生中断)

注意事项:

(1) rec_buff接收数组的大小要多预留部分区域。 原因是:DMA以4byte为单位搬运数据,例如接收数据的实际长度为5,rev_size设置为5 - 8均可,DMA会搬运两次,第二次搬运虽然有效数据只剩下一个,DMA仍会搬运4个数据,其中后三个数据为无用数据。

(2) 接收数据的长度len满足一定关系,例如当4*(n-1)<len≤4*n时,将rec_buff大小设置为大于等于4n。这样rec_buff前len个为有效数据,后面的为无效数据,我们需要进行准确区分。

(3) 在实际使用DMA接收数据时最好设置为:当接收数据长度len在4*(n-1)<len≤4*n时,设置rec_buff=rev_size=4n.

示例:rec_buff长度设置为8,rev_size设置为8,分别发送4,5,6,7,8个数据对比。

程序设置DMA为接收模式,初始化rec_buff全为0。通过BDT查看rec_buff值。

发送数据 接收数据 中断是否产生 无效数据个数 DMA搬运次数
0x11, 0x22, 0x33, 0x44 0x11, 0x22, 0x33, 0x44 0 1
0x11, 0x22, 0x33, 0x44, 0x55 0x11, 0x22, 0x33, 0x44, 0x55, 0xXX, 0xXX, 0xXX 3 2
0x11, 0x22, 0x33, 0x44, 0x55, 0x66 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0xXX, 0xXX 2 2
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0xXX 1 2
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88 0 2

注意:

  • 0xXX表示无效数据值,通常显示为乱码

NDMA模式

发送数据:

void uart_send_byte(uart_num_e uart_num, unsigned char tx_data)

注意:

  • NDMA模式还为我们提供了以hword(2byte)和word(4byte)形式发送数据,需要注意,发送数据时先发送低字节数据,再发送高字节数据。如一串数据为0x11223344,如果要在接收端接收数据也为0x11223344,以hword形式发送顺序为0x2211,0x4433。以word形式发送顺序为0x44332211。

查询

uart_send_byte(UART0, uart0_tx_buff_byte[i]);
while(uart_tx_is_busy(UART0));

需要使用uart_tx_is_busy接口查询当前状态,如果该标志位为0,则表明上一帧数据发送结束,可以进入到当前帧数据的发送中。

中断

标志位:UART_TXBUF_IRQ_STATUS

在初始化中,通过uart_tx_irq_trig_level()设置了TX的中断触发个数level,当发送缓冲区数据达到该level时,发送即开始。此时将进入发送中断。

注意:

  • DEMO中给出TX中断触发level为0,即缓冲区有数据即进行发送。此时如果使用TX中断,则程序将会一直处于TX中断状态,不利于实际使用。因此,DEMO中不给出TX中断的判断和处理,这样更符合我们的实际使用习惯(有发送需求直接发送即可),如果确实需要对发送状态进行判断,建议使用查询方式。

接收数据:

unsigned char uart_read_byte(uart_num_e uart_num)

标志位:UART_RXBUF_IRQ_STATUS

在初始化中设置了接收中断触发个数level,当接收字符个数达到该level时,该标志位即置1,此时即进入中断,将缓冲区数据移到我们预先定义的接收数组当中。

流控

因为UART双方处理速度的差异,在进行数据传送时,接收速率与发送速率存在很大的差距,这样在进行数据的发送和接收过程中可能出现接收方来不及接收的情况。为了防止数据丢失,这是就需要发送方进行控制,这就是所谓的流量控制。

CTS (clear to send) 允许发送

RTS (request to send) 请求发送

如果UART0与UART1进行通信,则UART0的RTS管脚与UART1的CTS管脚相连,UART0的CTS管脚与UART1的RTS管脚相连。

(1) CTS

uart_cts_config(uart_num_e uart_num,uart_cts_pin_e cts_pin,u8 cts_parity)

uart_cts_config()函数用来配置使用CTS流控的端口号,CTS引脚。当CTS引脚输入电平和cts_parity相等时,UART将停止发送。

(2) RTS

uart_rts_config(uart_num_e uart_num,uart_rts_pin_e rts_pin,u8 rts_parity,u8 auto_mode_en)
uart_rts_trig_level_auto_mode(uart_num_e uart_num,u8 level)

uart_rts_config()函数用来配置UART端口号,RTS引脚。

rts_parity只在auto模式下生效,它表明当接收数据量达到level值时RTS引脚跳变方向,为1时电平从低到高跳变,为0时从高到低跳变。

注意:

  • RTS在配置时有两种模式可选,UART_RTS_MODE_AUTO/UART_RTS_MODE_MANUAL.即自动模式和手动模式。自动模式下,RTS引脚在接收到RTS-THRESH个数据时自动进行RTS_INVERT相关的跳变。手动模式下,我们需要计算接收的数据长度,当数据长度达到预期值时,我们需要使用uart_set_rts_level()函数手动拉低或拉高RTS引脚。

uart_rts_trig_level_auto_mode()为RTS跳变触发设置,level表明触发门槛,如设置为5时,接收5个数据RTS引脚即进行跳变。

DEMO简介

在头UART_DEMO/app_config.h中可以选择配置UART工作模式(DMA和NDMA),如下图,分别对应app_dma.c与app.c中的内容。

#define UART_DMA      1 //uart use dma
#define UART_NDMA     2 //uart not use dma
#define UART_MODE     2

其中NDMA与DMA模式又可以具体选择流控模式,代码如下:

#define BASE_TX      0 //just for NDMA
#define NORMAL       1
#define USE_CTS      2
#define USE_RTS      3
#define FLOW_CTR     1

DMA模式

UART模块通过DMA不停的发送接收到的数据,初始rec_buff[]全为0。

通过串口工具进行验证:

DMA串口验证

NDMA模式

当接收字符个数达到预设的接受长度(即UART0_RX_IRQ_LEN)时,uart0_rx_flag为1,此时进入到此中断服务程序中,进行的处理为:将接收的字符发送出去。

通过串口工具进行验证:

NDMA串口验证

RTS与CTS

以NDMA为例,由于硬件条件限制,只单一演示RTS或者CTS。

CTS:设置STOP_VOLT=1,即高电平停止发送TX。

当CTS引脚为0时,持续发送,利用串口工具查看:

CTS引脚为0

将CTS置高电平,发送停止。

RTS:自动模式下,设置RTS_THRESH=5,RTS_INVERT=1,即累计当收到5个数据时,RTS引脚从低到高跳变。

为使现象较为显著,利用逻辑分析仪查看跳变结果。

使用串口工具发送4个数据:

使用串口工具发送4个数据

RTS跳变未发生

发送5个数据:

使用串口工具发送5个数据

RTS引脚完成了从低到高的跳变。

跳变

注意:

  • 实际使用UART的流控时,我们需要注意合理配置CTS与RTS的跳变方式。例如,一个设备的RTS引脚设置为触发时由低到高跳变,那么与之相连的另一个设备的CTS引脚就需要设置为高电平停止发送。这样UART流控才能正常工作。

芯片差异

UART_RXDONE中断

支持使用该中断才可以有效判断一个数据包的结束(即使不知道数据包的长度)。

芯片 DMA模式RX_DONE中断
B91 A0 不支持
其他 支持

UART_RX_ERR中断

B91 A0芯片:

(1) DMA模式:UART_RX_ERR中断不能使用。(原因:硬件检测到该中断后自动清除,软件检测不到)

(2) NDMA模式:UART_RX_ERR中断可以使用。

建议使用方法:清除中断状态,清除RX-FIFO,硬件指针和软件指针归零。

原因:接收出错时,FIFO中可能存在错误数据,为了不影响后续正确数据的接收,需要清空RX-FIFO(即指针归零,uart_reset()使硬件指针清零,同时软件指针保持一致也需要清零)。

总结:该模式下检测到UART_RX_ERR中断后,需要执行以下操作:

uart_clr_irq_status(UART0,UART_CLR_RX);//NDMA模式下会清除RX-FIFO和RX_ERR_IRQ,RX_BUFF_IRQ(注意:如果进入RX_ERR中断,说明接收数据出错,清除RX会同时清除RX_BUFF中断)。
uart_reset()//硬件指针清零
uart_clr_rx_index();//软件指针清零

其他芯片:

(1) DMA模式:UART_RX_ERR中断可以使用。

建议使用方法:只清除中断状态。

原因:DMA模式接收数据出错,当进入到RX_DONE中断时,DMA会将RX-FIFO中的数据搬运完,FIFO指针自动归零,不需要uart_reset(),错误数据不会对后续正确数据的接收造成影响。(注意:如果发生UART_RX_ERR中断,接收数组中的数据将是错误数据,无法使用)

总结:该模式下检测到UART_RX_ERR中断后,需要执行以下操作:

uart_clr_irq_status(UART0,UART_CLR_RX); //DMA模式下会清除RX_FIFO和RX_DONE_IRQ,RX_ERR状态。

SPI

简介

标准SPI接口

串行外设接口(Serial Peripheral Interface)简称SPI接口,是一种同步串行外设接口,允许嵌入式处理器与各种外围设备通过串行的方式进行通信、数据交换等。

标准SPI接口一般使用4条线通信:

SPI接口

名称 含义
CSN 设备片选信号线,低电平有效
CLK 时钟信号线
MOSI Master数据输出Slave数据输入线
MISO Master数据输入Slave数据输出线

SPI通信过程

下图是SPI通信的一个简单例子:

SPI通信

这是一个标准SPI的通信时序。CSN、CLK、MOSI信号都由Master产生,数据输出通过MOSI线,而MISO的信号由Slave产生,Master通过该信号线读取Slave的数据。MOSI与MISO的信号只在CSN为低电平的时候才有效,数据在CLK上升沿或下降沿时触发采样。在CLK的每个时钟周期MOSI和MISO传输1bit数据,8个时钟周期就可以实现1Byte数据传输。

根据空闲时CLK时钟极性CPOL(Clock Polarity)和采样时刻的CLK时钟相位CPHA(Clock Phase)的不同,SPI区分出四种工作模式,见下表,主机与从机需要工作在相同的模式下才可以正常通讯。

SPI工作模式 CPOL CPHA
SPI_MODE0 0 0
SPI_MODE1 0 1
SPI_MODE2 1 0
SPI_MODE4 1 1

CPOL: Clock Polarity

  • 当CPOL=0, CLK在空闲时刻保持低电平;

  • 当CPOL=1, CLK在空闲时刻保持高电平。

CPHA: Clock Phase

  • 当CPHA =0, 在CLK的奇数边缘触发采样;

  • 当CPHA =1, 在CLK的偶数边缘触发采样。

多样化的SPI接口

在标准SPI的基础上为适应不同的应用场景,逐渐衍生出很多类别的SPI接口。

3line SPI:3line SPI顾名思义只有3根线,CLK, CSN, MOSI,数据收发共用一根线,为半双工通信。

Dual SPI:它拓展了MOSI和MISO的用法,让它们工作在半双工,同向传输数据,用以加倍数据传输。也就是对于Dual SPI,MOSI变成IO0,MISO变成IO1,这样一个时钟周期内就能传输2个bit数据,加倍了数据传输。

Quad SPI:与Dual SPI类似,它拓展了WP和HOLD的用法,WP变成IO2和HOLD变成IO3,也就是同时拥有了四根数据线,一个时钟周期内可以传输4个bit,传输速度会大幅提升。

多样化的SPI接口

功能说明

SoC包含多种SPI模块,有HSPI、PSPI 和SPI Slave,针对每个模块都配有相应的驱动支持。

接口说明

接口命名规则:

  • spi前缀:hspi和pspi均可以使用的接口。
  • pspi前缀:仅供pspi使用。
  • hspi前缀:仅供hspi使用。
  • dma后缀:dma模式会用到的接口。
  • plus后缀:支持更丰富的读写模式和操作指令。

例如:spi_master_write_read_dma_plus接口在使用时就会使用DMA通道,先写地址到SPI Slave,再从SPI Slave对应地址读出数据。

注意:

  • 带有“read”字段“write_read”的接口的功能都是从SPI Slave读出数据。
  • 二者区别在于:
    (1)带有“read”字段的接口支持硬件自动发送address帧,适用于使能硬件address帧支持的应用场景,接口直接可以读取对应地址的数据。
    (2)带有“write_read”字段的接口是在没有使能硬件address帧的情况下使用的。地址信息先要通过“write”的方式写SPI Slave,然后才能读取对应地址的数据。

HSPI与PSPI

HSPI与PSPI是SoC所支持的两种高级的SPI接口,均支持Master和Slave模式。HSPI/PSPI Slave均会自动解析cmd,数据接收和发送需要用软件进行操作。

注意:

  • 例程中,HSPI/PSPI Slave接收和发送数据是通过软件方式实现的,优点是配置更灵活而且支持Quad I/O模式,缺点是配置方式不如直接使用SPI Slave模块简单,而且会消耗运算资源。在使用时如果需要简单配置本设备作为Slave,建议使用SPI Slave模块。

(1) Master

标准SPI Master

SoC的HSPI/PSPI模块支持标准SPI Master模式,此模式下调用的函数接口不带有“plus”后缀,功能和模式比较简单。

初始化接口如下:

spi_master_config(SPI_MODULE_SEL, SPI_NOMAL);

支持的读写操作接口为:

void spi_master_write(spi_sel_e spi_sel, u8 *data, u32 len);
void spi_master_write_dma(spi_sel_e spi_sel, u8 * data, u32 len);
void spi_master_write_read(spi_sel_e spi_sel, u8 * addr , u32 addr_len, u8 * data , u32 data_len);
void spi_master_write_read_dma(spi_sel_e spi_sel, u8 *addr, u32 addr_len, u8 *data, u32 data_len);

HSPI/PSPI Master

功能说明:

SoC的HSPI/PSPI Master对常用协议中的几种帧信息增设了对应的硬件配置,包括cmd帧,address帧,dummy帧(空周期)。

如下表,在HSPI/PSPI Master所支持的功能中,Y代表支持,N代表不支持。

SPI模块 cmd_en cmd_fmt address_en address_fmt 3line Dual Quad
HSPI Y Y Y Y Y Y Y
PSPI Y N N N Y Y N
  • cmd_en: 硬件cmd帧
  • cmd_fmt: cmd帧跟随对应Dual/Quad I/O的编码格式
  • address_en: 硬件address帧
  • address_fmt: cmd帧跟随对应Dual/Quad I/O的编码格式
  • 3line: 3line SPI模式
  • Dual: Dual SPI模式
  • Quad: Quad SPI模式

数据帧格式:

HSPI/PSPI Master支持的数据发送的格式为:【cmd】+【adress】+【dummy】+data。【】代表可选

Step 1 通过调用如下结构体接口来配置HSPI支持的数据帧格式,PSPI类似:

hspi_config_st hspi_slave_protocol_config = {
    .hspi_io_mode = HSPI_QUAD,
    .hspi_ dummy _cnt = 6,
    .hspi_ cmd _en = 1,
    .hspi_addr_en = 1 ,
    .hspi_addr_len = 3,//when hspi_addr_en = false,invalid set.
    .hspi_cmd_fmt_en = 0,//when hspi_cmd_en = false,invalid set.
    .hspi_addr_fmt_en = 1 ,//when hspi_addr_en = false,invalid set.
};

这段代码配置了HSPI Master的各种帧参数:

  • 模块配置为Quad I/O模式;
  • dummy帧长度为6个clock;
  • 硬件cmd使能,cmd传输为Single I/O模式,不跟随对应Dual/Quad I/O的编码格式(hspi_cmd_fmt_en = 0);
  • 硬件address使能,address帧长度为3Bytes,跟随对应Dual/Quad I/O的编码格式(hspi_addr_fmt_en = 1)。

对应的通信时序图如下:

通信时序

调用下面接口使能帧参数的配置:

hspi_master_config_plus(&hspi_slave_protocol_config);

读写操作调用的接口:

HSPI/PSPI Master读写操作调用的接口如下:

void spi_master_write_plus(spi_sel_e spi_sel, u8 cmd, u32 addr, u8 *data, u32 data_len, spi_wr_tans_mode_e wr_mode); 
void spi_master_write_dma_plus(spi_sel_e spi_sel, u8 cmd, u32 addr, u8 *data, u32 data_len, spi_wr_tans_mode_e wr_mode);

void spi_master_read_plus(spi_sel_e spi_sel, u8 cmd, u32 addr, u8 *data, u32 data_len, spi_rd_tans_mode_e rd_mode);
void spi_master_read_dma_plus(spi_sel_e spi_sel, u8 cmd, u32 addr, u8 *dst_addr, u32 data_len, spi_rd_tans_mode_e rd_mode);

void spi_master_write_read_plus(spi_sel_e spi_sel, u8 cmd, u8 *addrs, u32 addr_len, u8 *data, u32 data_len, spi_rd_tans_mode_e wr_ mode);
void spi_master_write_read_dma_plus(spi_sel_e spi_sel, u8 cmd, u8 *addr, u32 addr_len, u8 *rd_data, u32 rd_len, spi_rd_tans_mode_e rd_mode);

读写方式:

HSPI/PSPI Master通过操作指令对SPI Slave进行操作,同时也需要配置HSPI/PSPI的读写方式来配合操作过程。读写方式用来表示操作是否需要dummy(空周期)帧,指令操作到底是读还是写。

读写方式的枚举定义如下:

typedef enum{
    SPI_MODE_WR_WRITE_ONLY = 1,//write
    SPI_MODE_WR_DUMMY_WRITE = 8,//dummy_write
}spi_wr_tans_mode_e;

typedef enum{
    SPI_MODE_RD_READ_ONLY = 2,//must enbale CmdEn
    SPI_MODE_RD_DUMMY_READ = 9,//dummy_read
}spi_rd_tans_mode_e;

typedef enum{
    SPI_MODE_WR_RD       = 3,//must enbale CmdEn
    SPI_MODE_WR_DUMMY_RD = 5,//write_dummy_read
}spi_wr_rd_tans_mode_e; .

例如在读数据时,SPI Slave要求要有dummy空闲帧,读数据应选择SPI_MODE_RD_DUMMY_READ方式。

(2) Slave

SoC的HSPI作为Slave时,支持Single、Dual和Quad I/O模式。PSPI作为Slave时,支持Single、Dual I/O模式。二者均会自动解析cmd ,但Slave数据接收和发送需要用软件进行操作。

通信数据帧格式

HSPI/PSPI Slave支持的通信数据帧格式如下表。

a. HSPI/PSPI SINGLE WRITE

MOSI_IO0 cmd (write 8bit) dummy (8clock) data0 ...
MISO_IO1 - - - -

b. HSPI/PSPI SINGLE READ

MOSI_IO0 cmd (read 8bit) dummy (8clock) - ...
MISO_IO1 - - data0 -

c. HSPI/PSPI DUAL WRITE

MOSI_IO0 cmd (write 8bit) dummy (8clock) D6 D4 D2 D0 ...
MISO_IO1 - - D7 D5 D3 D1 ...

d. HSPI/PSPI DUAL READ

MOSI_IO0 cmd (read 8bit) dummy (8clock) D6 D4 D2 D0 ...
MISO_IO1 - - D7 D5 D3 D1 ...

e. HSPI/PSPI QUAD WRITE

MOSI_IO0 cmd (write 8bit) dummy (8clock) D4 D0 ...
MISO_IO1 - - D5 D1 ...
WP_IO2 - - D6 D2 ...
HOLD_IO3 - - D7 D3 ...

f. HSPI/PSPI QUAD READ

MOSI_IO0 cmd (read 8bit) dummy (8clock) D4 D0 ...
MISO_IO1 - - D5 D1 ...
WP_IO2 - - D6 D2 ...
HOLD_IO3 - - D7 D3 ...

HSPI/PSPI Slave支持的操作指令

HSPI/PSPI Slave支持的操作指令在Demo中用枚举定义来表示,使用时对应读写函数接口的cmd参数。

enum{
    SPI_READ_STATUS_SINGLE_CMD = 0x05,
    SPI_READ_STATUS_DUAL_CMD = 0x15,
    HSPI_READ_STATUS_QUAD_CMD = 0x25,
    SPI_READ_DATA_SINGLE_CMD = 0x0B,
    SPI_READ_DATA_DUAL_CMD = 0x0C,
    HSPI_READ_DATA_QUAD_CMD = 0x0E,
    SPI_WRITE_DATA_SINGLE_CMD = 0x51,
    SPI_WRITE_DATA_DUAL_CMD = 0x52,
    HSPI_WRITE_DATA_QUAD_CMD = 0x54,
};

注意:

  • 操作指令和HSPI或者PSPI的模式是相关的,SPI前缀表示HSPI和PSPI通用,HSPI前缀表示只能HSPI使用,DUAL_CMD对应Dual SPI模式,QUAD_CMD对应Quad SPI模式。

(3) 时钟设置

在SoC中,HSPI 的时钟源为hclk,PSPI的时钟源为pclk。

Master时钟

在Master模式下时钟计算公式为:

\[F_{Master} = \frac {F_{hclk\&pclk}}{(spi\_clk\_div +1)*2}\]

其中: spi_clk_div为分频系数。

\(F_{Master}\) 为Master输出的CLK频率。

\(F_{hclk\&pclk}\) 为时钟源的的频率,其中HSPI为hclk,PSPI为pclk。

Master的时钟频率配置接口为一个宏定义:

#define SPI_CLK   500000

时钟配置的使能接口为:

spi_master_init(SPI_MODULE_SEL, sys_clk.pclk * 1000000 / (2 * SPI_CLK) - 1, SPI_MODE0);

实际使用时只要修改SPI_CLK宏定义值为对应的时钟频率即可。

注意:

  • 当分频系数为0xff时,Master输出时钟频率最快可达到源时钟的频率。
  • SPI_CLK的配置范围在Demo中有说明,配置时可以参考,超出配置范围可能会导致通信失败。

Slave的时钟

Slave 的时钟由Master 输入,Slave无需配置相应的时钟,但Master输入给Slave的时钟,需要满足条件:

\[F_{Master} \le \frac {F_{hclk\&pclk}}{3}\]

其中:\(F_{Master}\) 为SPI Master输入给Slave的CLK频率。

\(F_{hclk\&pclk}\) 为Slave设备自己内部的时钟源频率,PSPI Slave对应pclk频率,HSPI Slave对应hclk频率

(4) 中断

SoC支持多种SPI中断类型,使用时可以根据应用场景灵活配置,各中断类型特征如下表:

模式 相关中断 是否是异常中断 是否需手动清除中断标志位 是Master还是Slave产生
非DMA SPI_RXF_OR_INT: RX FIFO over run中断。接收数据时,程序读取数据不够快,RX FIFO将会被新的数据覆盖,这种数据的丢失被称为over run Y Y Slave
非DMA SPI_TXF_UR_INT: TX FIFO under run中断。发送数据时,程序写入数据到TX FIFO中的速度跟不上发送的速度,发送数据出现间断,这种现象被称为under run。 Y Y Slave
非DMA SPI_RXF_INT: RX FIFO阈值(临界值)中断。在该中断使能的情况下,RX FIFO数据达到或者超过阈值会触发该中断 N Y Master和Slave
非DMA SPI_TXF_INT: TX FIFO 阈值(临界值)中断。在该中断使能的情况下,TX FIFO数据小于或者达到阈值会触发该中断 N Y Master和Slave
DMA、非DMA通用 SPI_END_INT: 数据传输结束中断,一笔数据传输完成会触发该中断 N Y Master和Slave
DMA、非DMA通用 SPI_SLV_CMD_INT: 当配置成Slave mode时,每接收到1Byte command会触发该中断 N Y Slave

(5) DMA模式

使用宏定义来选择DMA通道,相关接口如下:

#define TX_DMA_CHN DMA2
#define RX_DMA_CHN DMA3
hspi_set_tx_dma_config(TX_DMA_CHN);
hspi_set_rx_dma_config(RX_DMA_CHN);

示例这里配置DMA2为tx通道,配置DMA3为rx通道,PSPI类似,具体可以在Demo中查看。

DMA模式下判断是否发送、接收完数据使用的相关接口如下:

查询方式:

static inline _Bool spi_is_busy(spi_sel_e spi_sel)

中断方式:

spi_set_irq_mask(SPI_MODULE_SEL, SPI_END_INT_EN);//endint_en

注意:

  • SPI_END_INT中断不代表数据传输完毕(只是表示fifo数据传输结束,CSN并没有拉高),产生SPI_END_INT中断后,再查询busy信号,直到IDLE才代表结束。
  • 使用DMA进行传输时,定义发送(接收)的结构体或者数组要进行四字节对齐,Demo中通过__attribute__((aligned(4))) 来体现。
  • 使用DMA接收SPI数据到Buffer时,对应的目的地址的Buffer大小一定要是4的倍数。原因是:每次DMA都会送到Buffer 4个Byte。即使配置读取长度不够4,也会往目的地址写4个Byte。例如,定义数组Buffer大小为5Byte,配置DMA从SPI读取5Byte到Buffer,这时候DMA实际上传送了2次共8个Byte到Buffer,多的3个Byte数据会从数组溢出,严重时溢出的数据会覆盖别的变量。这时如果配置数组大小为8Byte,多的3个Byte数据就会存放在数组中,不会溢出,避免了潜在的风险。

(6) 3Line

HSPI/PSPI Master/Slave支持3line模式,需要在Slave中使能。

调用的接口为:

void spi_set_3line_mode(spi_sel_e spi_sel)

注意:

  • 3line模式的读写指令兼容HSPI/PSPI的SINGLE_CMD。

(7) 多SPI Slave结构

对于多SPI Slave的应用场景,可以为每个Slave都分配一个CSN引脚。一笔数据传输完成,CSN会拉高。这时可以切换CSN,达到切换Slave的效果。

HSPI Master调用接口如下:

void hspi_cs_pin_dis(hspi_csn_pin_def_e pin)
void hspi_cs_pin_en(hspi_csn_pin_def_e pin)

对于PSPI Master调用接口类似,可在接口的代码中查看。

(8) XIP模式

XIP:eXecute In Place,即芯片内执行,指应用程序可以直接在外置存储设备内取指、译码、执行。HSPI支持XIP模式,可以通过HSPI接口扩展SoC的地址空间至外置存储设备,为运行数据量庞大应用程序提供了硬件基础。

配置XIP模式

通过下面接口对XIP模式进行配置:

hspi_xip_seq_mode_en();//must
hspi_xip_page_size(4);
hspi_xip_en();

代码中的seq_mode(sequential mode)表示一种间隔收发模式,它表示把数据分成一个个\(2^{page\_size}\) Bytes大小的块进行间隔收发,每收发完一块就会拉高一次CS,直至数据传输完毕。

发送指令

通过下面接口发送指令:

void hspi_master_write_xip_cmd_data(u8 cmd, u32 addr_offset, u8 data_in, spi_wr_tans_mode_e wr_mode)

数据读写

读写数据调用接口如下:

void hspi_master_write_xip(u8 cmd, u32 addr_offset, u8 *data, u32 data_len, spi_wr_tans_mode_e wr_mode) 
void hspi_master_read_xip(u8 cmd, u32 addr_offset, u8 *data, u32 data_len, spi_rd_tans_mode_e rd_mode)

XIP片内运行程序

XIP模式运行程序需要把PC指针切换到XIP设备中程序对应的地址,然后才可以运行。这里XIP对应的基地址为0x1000000,通过下面两句指令切换PC指针到XIP设备的对应地址(基地址0x1000000+相对地址0x00),之后就可以用XIP模式运行程序了。

__asm__("li t0,0x1000000");
__asm__("jarr t0");

SPI Slave

顾名思义,SPI Slave只支持Slave模式。程序中只需要将SPI Slave对应GPIO配置为SPI功能即可。其硬件会自动对接收到的SPI数据进行协议解析(读写对应地址的值),并做相应的响应动作。除此之外,SPI Slave还支持Dual I/O模式。

(1) 通信数据帧格式

SPI Slave模块支持的通信数据格式如下表。

a. SPI SLAVE SINGLE WRITE

MOSI_IO0 cmd (write 8bit) addr(32bit) high -> low data0 data1 ...
MISO_IO1 - - - - -

b. SPI SLAVE SINGLE READ

MOSI_IO0 cmd (read 8bit) addr(32bit) high -> low dummy (8cycle) - -
MISO_IO1 - - - data0 -

c. SPI SLAVE DUAL WRITE

MOSI_IO0 cmd (write 8bit) addr(32bit) high -> low D6 D4 D2 D0 ...
MISO_IO1 - - D7 D5 D3 D1 ...

d. SPI SLAVE DUAL READ

MOSI_IO0 cmd (read 8bit) addr(32bit) high -> low dummy (8cycle) D6 D4 D2 D0 ...
MISO_IO1 - - - D7 D5 D3 D1 ...

注意:

  • “addr(32bit) high -> low”表示地址的排列顺序高字节在前。

(2) SPI Slave支持的操作指令

SPI Slave支持的操作指令在Demo中用枚举定义来表示,使用时对应读写函数接口的cmd参数。

typedef enum{
    SPI_SLAVE_WRITE_DATA_CMD = 0x00,
    SPI_SLAVE_WRITE_DATA_DUAL_CMD,
    SPI_SLAVE_WRITE_ADDR_DUAL_CMD,
    SPI_SLAVE_WRITE_DATA_DUAL_4CYC_CMD,
    SPI_SLAVE_WRITE_ADDR_DUAL_4CYC_CMD,
    SPI_SLAVE_WRITE_DATA_AND_ADDR_DUL_4CYC_CMD,
}spi_slave_write_cmd_e;

typedef enum{
    SPI_SLAVE_READ_DATA_CMD,
    SPI_SLAVE_READ_DATA_DUAL_CMD,
    SPI_SLAVE_READ_ADDR_DUAL_CMD,
    SPI_SLAVE_READ_DATA_DUAL_4CYC_CMD,
    SPI_SLAVE_READ_ADDR_DUAL_4CYC_CMD,
    SPI_SLAVE_READ_DATA_AND_ADDR_DUL_4CYC_CMD,
}spi_slave_read_cmd_e;

注意:

  • 操作指令和SPI的模式是相关的,DUAL_CMD对应Dual SPI模式,4CYC_CMD对应dummy 4cycle模式。这些指令调用时是相或的,例如address和data都支持Dual I/O编码,那么读指令格式为:SPI_SLAVE__DATA_DUAL_CMD | SPI_SLAVE_READ_ADDR_DUAL_CMD。

Demo说明

Demo结构说明

SPI Demo的应用.c文件分别为app.c,app_dma.c和app_hspi_xip.c,分别对应DMA、NDMA(非DMA)、XIP传输模式。

通过SPI_Demo/app_config.h中的宏SPI_MODE选择使用哪一种传输模式。

#define SPI_NDMA_MODE   1
#define SPI_DMA_MODE    2
#define SPI_XIP_MODE    3
#define SPI_MODE        SPI_NDMA_MODE

在DMA和NDMA传输模式中,通过配置各模式下的宏SPI_DEVICE选择为Master和Slave模式。

#define SPI_MASTER_DEVICE     1
#define SPI_SLAVE_DEVICE      2
#define SPI_DEVICE            SPI_MASTER_DEVICE

通过宏SPI_MODULE_SEL选择使用HSPI 或者PSPI模块。

#define PSPI_MODULE         0
#define HSPI_MODULE         1
#define SPI_MODULE_SEL      HSPI_MODULE

在DMA和NDMA传输模式中,通信协议按照Slave设备的不同分为三类,通过宏SPI_TRANS_MODE选择。

#define KITE_VULTURE_SLAVE_PROTOCOL      1
#define HSPI_PSPI_SLAVE_PROTOCOL         2
#define SPI_SLAVE_PROTOCOL               3
#define SPI_TRANS_MODE                   SPI_SLAVE_PROTOCOL

KITE_VULTURE_SLAVE_PROTOCOL:是为Telink的Kite(TLSR825x)或Vulture(TLSR827x)等作为Slave的使用场景而设计的模式。

HSPI_PSPI_SLAVE_PROTOCOL:是为SoC的HSPI/PSPI作为Slave的使用场景而设计的模式。

SPI_SLAVE_PROTOCOL:是为SoC的SPI SLAVE作为Slave的使用场景而设计的模式。

注意:

  • 通过两个板子测试SPI通信时,将Master端和Slave端的代码都烧到板子之后,先给Slave端上电,再给Master上电,还有两个板子之间需要稳定共地。

硬件连接

Demo中不同的SPI_TRANS_MODE,接线方式会有区别。

针对KITE_VULTURE_SLAVE_PROTOCOL的硬件连接如下:

HSPI/PSPI Master (SoC) Slave (Kite/Vulture)
CLK CLK
CSN CSN
MOSI_IO0 SDI
MISO_IO1 SDO

针对HSPI_PSPI_SLAVE_PROTOCOL的硬件连接如下:

HSPI/PSPI Master (SoC) HSPI/PSPI Slave (SoC)
CLK CLK
CSN CSN
MOSI_IO0 MOSI_IO0
MISO_IO1 MISO_IO1
WP_IO2(HSPI only) WP_IO2(HSPI only)
HOLD_IO3(HSPI only) HOLD_IO3(HSPI only)

针对SPI_SLAVE_PROTOCOL的硬件连接如下:

HSPI/PSPI Master (SoC) SPI Slave (SoC)
CLK CLK
CSN CSN
MOSI_IO0 MOSI_IO0
MISO_IO1 MISO_IO1

HSPI/PSPI Master/Slave的初始化配置

HSPI/PSPI Master/Slave的初始化流程如下图所示:

HSPI/PSPI Master/Slave的初始化流程

HSPI/PSPI Master的读写操作

HSPI/PSPI Master的读写操作流程如下图所示:

HSPI/PSPI Master的读写操作流程

测试实例:

Demo配置SPI_SLAVE_PROTOCOL模式下的HSPI为Dual SPI,HSPI Master通过Dual I/O写命令SPI_SLAVE_WRITE_DATA_DUAL_CMD | SPI_ SLAVE_WRITE_ADDR_DUAL_CMD使用DMA往SPI Slave写入16Bytes数据,然后通过Dual I/O读命令SPI_READ_DATA_DUAL_CMD | SPI_READ_ADDR_DUAL_CMD使用DMA读出。测试成功,逻辑分析仪波形如下:

逻辑分析仪波形

SPI_XIP_MODE模式

Demo适配的XIP Device是一款型号为APS1604M-3SQR的PSRAM,APS1604M-3SQR支持传统SPI和QUAD SPI等模式。

(1) 通信格式

Demo中对PSRAM的通信格式通过宏定义来使能,宏定义及含义如下:

#define SPI_XIP_SERIAL_CMD_READ                1
#define SPI_XIP_SINGLE_CMD_FAST_READ           2
#define SPI_XIP_SINGLE_CMD_FAST_QUAD_READ      3
#define SPI_XIP_QUAD_CMD_FAST_READ             4
#define SPI_XIP_QUAD_CMD_FAST_QUAD_READ        5
#define SPI_XIP_LOAD_PROGRAM_TO_PSRAM          6

#define SPI_XIP_TEST_MODE        SPI_XIP_SERIAL_CMD_READ

SPI_XIP_SERIAL_CMD_READ: 单线指令数据读写模式。

SPI_XIP_SINGLE_CMD_FAST_READ: 单线指令数据读写的升级模式,支持更高的CLK。

SPI_XIP_SINGLE_CMD_FAST_QUAD_READ: 单线指令四线数据的读写模式。

SPI_XIP_QUAD_CMD_FAST_READ: 四线指令四线数据的读写模式。

SPI_XIP_QUAD_CMD_FAST_QUAD_READ: 四线指令四线数据的读写的升级模式,支持更高的CLK。

SPI_XIP_LOAD_PROGRAM_TO_PSRAM: 芯片内运行程序的模式。

注意:

  • APS1604M-3SQR支持的通信格式多种多样,有单线的cmd帧,有四线的cmd帧,dummy的个数也随着模式区别而不一样,这些在Demo中都有体现。这里仅表明调用接口的含义,如果用户想去具体了解应用,开发时可以参考Demo和相关PSRAM手册。

(2) 配置XIP模式

APS1604M-3SQR硬件上要求单次数据传输时间不能超过8us(有的版本为4us,具体查看产品手册),否则会有出错风险,所以在配置XIP模式的时候要使能seq_mode。下图page_size设置为1,在PSRAM的基地址0x000000上写8个字节(0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x77)数据,数据被分成4块,每次发送\(2^{page\_size}\) = 2bytes,分四次发送完成。相邻CSN拉高之间的时间间隔小于8us。

配置XIP模式

注意:

  • 因为APS1604M-3SQR要求的8us间隔,就不得不提高CLK频率来减小每次传输\(2^{page\_size}\) Bytes数据所消耗的时间,使其满足8us这个要求,所以XIP模式下HSPI的SPI_CLK比一般SPI应用要高。当调节SPI_CLK无法满足8us的要求时,可以通过配置page_size的大小来减少每次传输的字节数,增加传输次数,来满足时间要求。

(3) 测试实例

Demo配置HSPI为QUAD XIP模式,通过HSPI XIP往PSRAM地址0x00写入数组led_program_in_sram中的LED灯闪烁程序,然后跳转到PSRAM的对应地址执行该程序。

测试发现LED2间隔闪烁,证明PSRAM片内运行程序成功。

读取PSRAM中的程序到数组led_program_in_psram,通过BDT工具读取数组led_program_in_psram和led_program_in_sram进行比较,两组数据完全一样,证明读写PSRAM成功。

测试实例

PM

MCU正常执行程序时处于working mode,电流会在mA级别。如果需要省功耗需要进入低功耗模式。

功能说明

低功耗模式(low power mode)又称sleep mode,有三种,分别是:

(1) Suspend

(2) deep sleep without SRAM retention(下面简称deep)

(3) deep sleep with SRAM retention(可保留部分SRAM空间的数据)(下面简称deep retention)

每种模式又因唤醒源不同分为PAD唤醒、32k_timer唤醒(分内部32k、外部32k时钟源)、MDEC唤醒、LPC唤醒、CORE唤醒,其中CORE唤醒仅支持suspend模式,其他唤醒源支持所有模式。

目前支持deep retention模式的芯片型号包括:blackHawk(8K)、kite(8K/16k/32k)、vulture(16k/32k)、eagle(32k/64k)。

三种低功耗模式下Sram、数字寄存器(digital register)、模拟寄存器(analog register)状态如下:

module suspend deep retention deep
SRAM 100% keep First 16K/32K/64K keep, others lost 100% lost
digital register 99% keep 100% lost 100% lost
analog register 100% keep 99% lost 99% lost

三种低功耗模式介绍如下。

Suspend

Suspend模式下,程序停止运行,类似一个暂停功能,当suspend被唤醒后,程序继续执行。Suspend模式下,PM模块正常工作,SRAM不掉电(数据不丢失),全部的analog register不掉电,少量的digital register掉电。为了节省功耗,软件可以设置把RF/USB/Audio等模块掉电,此时这几个模块相应的部分digital register会丢失,例如RF在唤醒后需要重新进行初始化才能发包,其余寄存器均不丢失。如果想要在唤醒后直接能够发包,可以不设置相应模块掉电,但是相应的功耗会增加。可以通过IO、timer等方式唤醒;这里需要注意的是,pad唤醒模式下为了避免误触发,需要做相应的上下拉确保初始电平正确,且维持不变直到pad触发唤醒。

Deep

Deep模式下,程序停止运行,MCU绝大部分的硬件模块都断电,当deep被唤醒后,MCU将重新启动,类似于重新上电,程序重新开始初始化。Deep模式下,pm模块正常工作,SRAM掉电,数据丢失,大部分的3.3V analog register会保存,其他的analog register掉电,全部的digital register掉电。可以通过IO、Timer等方式唤醒,SRAM的数据丢失。

Deep retention

Deep模式电流很低,但是无法存储Sram信息;suspend模式Sram和register可以保持不丢,但是电流偏高。为了实现一些需要sleep时电流很低又要能够确保sleep唤醒后能立刻恢复状态的应用场景,增加了deep retention模式。Deep retention模式更接近deep模式,与deep的唯一区别是可以保存Sram,可根据实际需求选择Sram retention area的大小,保存的Sram越大,功耗就越大。

Deep retention模式下,程序停止运行,MCU绝大部分的硬件模块都断电,当deep retention被唤醒后,MCU将重新启动,类似于重新上电,程序重新开始初始化。Deep retention模式下,pm模块正常工作,SRAM保持部分空间不掉电,其他都掉电,大部分的3.3V analog register会保存,其他的analog register掉电,全部的digital register掉电,比deep模式增加的电流值就是保持Sram所消耗的电流值。可以通过IO、Timer等方式唤醒,由于deep retention会保存Sram,因此醒来之后可以省去一部分从flash搬代码/数据到ram的动作。另外,程序里还可以定义retention data,定义为retention data的变量在唤醒后不会去flash取值,直接保存在SRAM中,因此会保存最后一次修改的值。

低功耗模式工作流程

不同的睡眠模式,MCU的运行流程不一致。下面详细介绍suspend、deepsleep、deepsleep retention 3种睡眠模式被唤醒后的MCU运行流程。请参考下图。

MCU运行流程

流程图中各模块说明:

a) 运行硬件bootloader (Run hardware bootloader)

MCU硬件上执行一些固定的动作,这些动作固化在硬件上,软件无法修改。

b) 运行软件bootloader (Run software bootloader)

hardware bootloader运行结束之后,MCU开始运行software bootloader。Software bootloader就是vector段,对应S文件里面的汇编程序。Software bootloader是为了给后面C语言程序的运行设置好内存环境,可以理解为整个内存的初始化。

c) 系统初始化(System initialization)

System initialization对应main函数中sys_init、clock_init等各硬件模块初始化,设置各硬件模块的数字/模拟寄存器状态。

d) 用户初始化(User initialization)

User initialization对应函数user_init等。

e) main_loop

User initialization完成后,进入while(1)控制的main_loop。main_loop中进入sleep mode之前的一系列操作称为"Operation Set A",sleep 唤醒之后一系列操作称为"Operation Set B"。

各睡眠模式的流程分析:

(1) no sleep

没有睡眠,MCU的运行流程为在while(1)中循环,反复执行"Operation Set A"->"Operation Set B"。

(2) suspend

调用cpu_sleep_wakeup函数进入suspend模式,当suspend被唤醒后,类似于cpu_sleep_wakeup函数的正常退出,MCU继续运行到"Operation Set B"。在suspend睡眠期间所有的Sram数据保持不变,绝大多数的数字/模拟寄存器状态保持不变(只有几个特殊的例外);所以suspend唤醒后,程序接着原来的位置运行,几乎不需要考虑任何sram和寄存器状态的恢复。

(3) deepsleep

调用cpu_sleep_wakeup函数进入deep模式,当deep被唤醒后,类似于重新上电,MCU回到hardware bootloader重新运行。在deep睡眠期间所有的Sram和绝大多数的数字/模拟寄存器都会掉电(只有一些特殊的模拟寄存器例外),所有的软硬件初始化都重新做。

(4) deepsleep retention

deep retention是介于suspend和deep之间的一种睡眠模式。调用cpu_sleep_wakeup函数进入deep retention 模式,当deep retention被唤醒后,MCU回到Run software bootloader开始运行。在deep retention睡眠期间Sram只保留一部分SRAM不掉电,绝大多数的数字/模拟寄存器都会掉电(只有一些特殊的模拟寄存器例外)。唤醒后,Sram前面的一部分数据是保持的,可以跳过"Run hardware bootloader"这一步,由于Sram上retention area有限,"run software bootloader"无法跳过,必须得执行;由于deepsleep retention无法保存寄存器状态,所以system initialization必须执行,寄存器的初始化需要重新设置。由于程序里还可以定义retention data,user initialization可以做一些优化改进,与上电/deep唤醒后的user initialization做区分处理。

驱动说明

驱动层提供了一些接口资源给上层应用层使用,下面是对这些接口的介绍:

预留保留信息BUF

芯片进入deep/retention状态时,大多数的寄存器都会丢失,芯片预留了8个寄存器,在deep/retention不会丢失,这样应用层可以记录一些睡眠状态下,想要保存的信息。驱动中定义如下(其中0x38<0>、0x39<0>被驱动使用,剩余的留给应用层使用):

(1) 下面的寄存器在watchdog、硬件/软件reset、上电三种情况下会被清零。

//watchdog, chip reset, RESET Pin, power cycle
#define PM_ANA_REG_WD_CLR_BUF0     0x38 // initial value 0xff. [Bit0] is already occupied. The customer cannot change!

(2) 下面的寄存器只有在重新上电才会清零。

#define PM_ANA_REG_POWER_ON_CLR_BUF0   0x39 // initial value 0x00. [Bit0] is already occupied. The customer cannot change!
#define PM_ANA_REG_POWER_ON_CLR_BUF1   0x3a // initial value 0x00
#define PM_ANA_REG_POWER_ON_CLR_BUF2   0x3b // initial value 0x00
#define PM_ANA_REG_POWER_ON_CLR_BUF3   0x3c // initial value 0x00
#define PM_ANA_REG_POWER_ON_CLR_BUF4   0x3d // initial value 0x00
#define PM_ANA_REG_POWER_ON_CLR_BUF5   0x3e // initial value 0x00
#define PM_ANA_REG_POWER_ON_CLR_BUF60x3f // initial value 0x0f

状态信息

驱动中定义了一个全局变量g_pm_status_info,会在sys_init函数中更新pm的相关状态,包含的内容如下:

typedef struct{
    unsigned char is_pad_wakeup;  //本次是否是被pad唤醒
    unsigned char wakeup_src; //本次是被哪种唤醒源唤醒,包括PAD、TIMER、MDEC、LPC和CORE的唤醒状态。
    pm_mcu_status mcu_status; //mcu是从哪种状态回来,包括power on/watchdog/deep/deep ret四种状态
    unsigned char rsvd;
}pm_status_info_s;

extern _attribute_aligned_(4) pm_status_info_s g_pm_status_info;

注意:

  • 唤醒源WAKEUP_STATUS_TIMER,只要timer时间到设定的时间,标志就会自己置起来,即使没有设置timer唤醒开启,标志也会置起来,只是不唤醒。清掉标志以后,等到了时间仍然会置起来。
  • 唤醒源WAKEUP_STATUS_CORE,属于数字部分的唤醒方式,唤醒过程数字寄存器不能断电,所以仅支持suspend睡眠模式。寄存器配置默认打开core唤醒,power on/deep/deep ret回来唤醒源标志位会置起来,需要忽略这个标志位,suspend回来唤醒源标志位显示正常。
  • 唤醒源WAKEUP_STATUS_PAD,在进入睡眠时,如果pad满足唤醒条件,程序会不进入睡眠,继续往下跑。如果是deep模式,driver睡眠函数设置进入reboot,程序按照reboot处理。如果retention模式,driver睡眠函数未作处理,上层软件可按照suspend处理,退出睡眠函数后,需再对rf模块初始化。

Suspend power设置

接口函数:pm_set_suspend_power_cfg

该函数配置suspend的时候,basebend,usb,npe三个模块是否断电。默认状态时,是全部power down的,如果想在suspend模式下,可以在进入suspend之前调用该函数进行设置。

LPC唤醒

在测试中发现设置872mv和50%时,2.02V以下都能唤醒(正常应该是1.744V以下才可以唤醒),原因是LPC功能在LPC_LOWPOWER模式下精度较低。

USB唤醒

在测试中发现USB唤醒标志会被误置,因为USB唤醒的触发条件是USB引脚DP、DM上有电压变化,产生了数据,所以在设置USB唤醒前,需要软件配置把DP引脚接上拉,DM引脚接下拉,这样能保证电平的稳定。

注意:

  • DM引脚接下拉是为了模拟和host连接的状态,这只有测试USB唤醒的时候才需要这有配置。

Demo说明

流程说明

Demo的流程图如下:

Demo的流程图

流程图说明:

(1) 开始加2s的延时是为了保持可以通信,因为进入睡眠后,swire就不通了,这样使用BDT的时候容易active失败,不能烧录程序。

(2) 电流测试开始前将所有的IO引脚关闭防止漏电。

(3) CORE唤醒只支持suspend睡眠模式,不支持deep模式和deep retention模式。

(4) Suspend模式在睡眠前唤醒后打开和关闭LED2,deep和deep retention模式进入睡眠时,芯片掉电,LED1自动关闭。(LED只是为了指示状态)

(5) 因为RC时钟是不准确的,并且会随着温度而变化,所以一般需要定时校准。给出以下建议:

a) 24M RC:

可以每10s校准一次,睡眠之前校准,这个准确度会影响睡眠后晶振的起振时间,睡眠醒来后,硬件有用24M RC的时钟去kick crystal,时间越准确,则起振时间越快。

b) 32K RC:

在上电、deep起来会校准一次。因为PM中用的是tricking的方式(用16M去数32K的固定周期的时间),所以这里对于Timer唤醒的时间准确度是没有影响的。

如果是其他模块有用到32K RC,需要根据应用需求进行处理。

c) 32K xtal:

上电后都需要再重新kick。另外使用32K xtal的时候需要外面焊接电容。

芯片差异

睡眠电流值

CURRENT_TEST电流测试宏置1,测试睡眠电流,单位uA(此为一颗芯片的测试数据,仅作为参考,具体的可以参照datasheet)。

B91 A0芯片的睡眠电流值:

- Pad ldo Pad dcdc 32k rc ldo 32k rc dcdc 32k xtal ldo 32k xtal dcdc mdec ldo mdec dcdc lpc ldo lpc dcdc
deep 0.7 0.7 1.3 1.2 1.7 1.6 1.4 1.4 1.6 1.6
deep retention 32k sram 1.8 1.8 2.4 2.4 2.8 2.7 2.6 2.6 2.8 2.8
deep retention 64k sram 2.7 2.7 3.2 3.2 3.7 3.6 3.4 3.4 3.7 3.8

B91 A1芯片的睡眠电流值:

#9 Pad ldo Pad dcdc 32k rc ldo 32k rc dcdc 32k xtal ldo 32k xtal dcdc mdec ldo mdec dcdc comp. ldo comp. dcdc core ldo core dcdc
suspend 36.6 36.7 37.1 37.1 36.8 37.1 37.1 36.8 37.9 37.7 37 36.8
deep 0.6 0.5 1.1 1.1 1.5 1.4 1.1 1.0 1.5 1.5 - -
deep ret 32k 1.8 1.7 2.3 2.2 2.7 2.5 2.3 2.2 2.7 2.7 - -
deep ret 64k 2.7 2.6 3.1 3.1 3.5 3.4 3.2 3.1 3.6 3.1 - -

LPC

简介

低功耗电压比较器(Low Power Compare,后简称LPC)将乘以所选比例系数的输入电压与参考电压进行比较,并输出比较结果。LPC有两种工作模式,分别是:

(1) Normal mode, 内部基准电压来自bandgap(简称BG),精度较高,功耗大,工作在芯片正常供电的环境下。

(2) Low power mode, 内部基准电压来自UVLO,精度较低,功耗小,工作在芯片睡眠的环境下。

低功耗比较器的输出也可用作从低功耗模式唤醒系统的信号。

工作原理

LPC需要将32K RC时钟源用作比较器时钟。比较结果如下:

(1) 如果[输入电压*缩放比例]的值大于参考电压,则输出将为低(“ 0”)。

(2) 如果[输入电压*缩放比例]的值小于参考电压,则输出将为高(“ 1”)。

(3) 如果[输入电压*缩放比例]的值等于参考电压,或选择输入通道为float,则输出将不确定。

Demo说明

Demo流程图:

Demo流程图

参考电压值设置为872mv,缩放比例设置为50%,那么当输入电压为0 \~ 1.744V时,lpc_get_result返回值为”1”;当输入电压为1.744 \~ 3.3V时,lpc_get_result返回值为”0”。

MDEC

MDEC即曼彻斯特解码模块。

测试环境搭建

实现MDEC功能的工作环境除了开发板之外还有另外两块电路板:

(1) AT9001H-V1.1: 接收射频数据,并将数据通过MCU_1发送出去。

(2) E21480094v-0: 发送射频数据给AT9001H—V1.1板子。

如图AT9001H—V1.1, 黑线代表GND, 红线代表MCU_1。右边的长铁片为GND,其上方的呈对角状的铁片为VBAT,接线时注意需将MCU_1接入开发板中已设置的MDEC功能引脚(图示为PA0)。电路板E21480094v—0红线为24V+,黑线为24V-。

电路板AT9001H-V1.1

电路板E21480094v-0

此外,电路板E21480094v—0需要24V稳压源供电。具体的接线示意图如下:

接线示意图

接线示意图

当环境搭建完成并上电后,电路板E21480094v—0会连续发送无线包数据内容,当电路板AT9001H—V1.1收到后会通过MCU_1以电平的方式发送到SoC的曼彻斯特接口IO,注意摆放两块电路板的时候尽量靠近,正确的电平波形如下:

电平波形

注意:

  • 如果看到的波形比较乱,请重新摆放一下两块电路板的位置,并调整一下E21480094v—0下方接口中的黑色长线圈。

功能说明

使用MDEC模块需要开启32K时钟,其中32K RC和32K Xtal均可。通过读取曼彻斯特输入引脚的电平,获得并判断相关数据。可以用于低功耗模式下的MDEC唤醒。

注意:

  • 使用32K时钟源, 是因为MDEC设计的时候考虑了在deep以及suspend等场景中, MDEC作为唤醒源存在。

RF

RF驱动所支持的收发模式包含了BLE1M、BLE2M、BLE125K、BLE500K、Zigbee250K、Hybee1M、Hybee2M、Hybee500K、Private1M、Private2M、Private250K、Private500K、ANT模式。其中BLE1M、2M还包含了打开PN和关闭PN两种模式。

BLE模式可在符合标准的1Mbps BLE模式、2Mbps增强BLE模式、125Kbps BLE远程模式(S8)、500kbps BLE远程模式(S2)下工作。 Zigbee模式在符合IEEE 802.15.4标准的250Kbps模式下工作。

初始化

RF模块初始化:

rf_mode_init(); //RF initialization
rf_set_ble_1M_mode(); //different modes call different mode initialization, here take BLE_1M mode as an example
rf_set_power_level(RF_POWER); //Set the transmit energy
rf_access_code_comm(ACCESS_CODE);// Zigbee, hybee mode has no access code concept, so this step is not needed
rf_set_tx_dma(0,128);//1 FIFO, each FIFO size is 128bytes
rf_set_rx_dma(rx_packet,RX_FIFO_NUM-1,RX_FIFO_DEP);
rf_set_ble_chn(17);//for BLE open PN 2440MHz

如果想要收发包,有两种使用方式可以选择:

(1) Manual Mode:tx、rx所有的使用过程,由软件流程控制,比如,设置tx mode,等PLL稳定再发包等。

(2) Auto Mode:只要触发对应状态机模式,后面的动作由硬件自动控制,不需要软件控制。

能量设置

目前设置能量的接口有两个:

void rf_set_power_level (rf_power_level_e level)
适用模式 参数chn_num
所有模式 设置能量所对应的枚举变量值
void rf_set_power_level_index (rf_power_level_index_e idx)
适用模式 参数chn
所有模式 传入的为能量所对枚举变量在对应数组中的index值。

注意:

  • 两者区别仅在于传参的方式不一致,应用中可以根据需要选择使用其中的哪一个。

频点设置

目前设置频点的接口有两个:

void rf_set_ble_chn(signed char chn_num)
适用模式 参数chn_num
ble_1M, ble_2M, ble_250K, ble_500K 实际设置的频点为 (chn_num+2400) MHz
void rf_set_chn(signed char chn)//所有模式均可以使用
适用模式 参数chn
支持所有模式 传入的为频点的index值。通过索引的变换设置为相对应的频点,索引关系如下。

rf_set_ble_chn函数参数chn_num与频点的对应关系如下表:

对应的频点(MHz) chn_num
2402 37
2404 0
2406 1
2408 2
2410 3
2412 4
2414 5
2416 6
2418 7
2420 8
2422 9
2424 10
2426 38
2428 11
2430 12
2432 13
2434 14
2436 15
2438 16
2440 17
2442 18
2444 19
2446 20
2448 21
2450 22
2452 23
2454 24
2456 25
2458 26
2460 27
2462 28
2464 29
2466 30
2468 31
2470 32
2472 33
2474 34
2476 35
2478 36
2480 39

中断

下面所有中断都需要软件手动清零。

模式 相关中断
Auto FLD_RF_IRQ_RX_TIMEOU: 若从trigger到timeout设置的时间内仍未收到包,则产生中断,状态机回到idle状态。通过void rf_set_rx_timeout();设置timeout时间,timeout起始于trigger RX
Auto FLD_RF_IRQ_CMD_DONE: 状态机在完成一次正常的收包或者发包操作后,正常回到IDLE状态,产生中断
Auto FLD_RF_IRQ_RX_CRC_2: BTX,BRX,PTX,PRX在收包(连续收包)过程中连续两次检测到CRC错误,会产生中断
Auto FLD_RF_IRQ_FSM_TIMEOUT: 含有从接收切换到发送的状态机使用的,规定整个状态机的时间,在TX_WAIT状态中判断是否超出规定的时间
Auto FLD_RF_IRQ_TX_RETRYCNT: 在ptx retry次数超过设定的r_max_retry_cnt 产生中断
Auto FLD_RF_IRQ_TX_DS: PTX、PRX发送的payload长度 != 0,产生tx_ds中断
Auto FLD_RF_IRQ_RX_DR: PRX、PTX、SRX接收到的数据包检测到数据包的payload length != 0,产生rx_dr中断
Auto FLD_RF_IRQ_STX_TIMEOUT: 在STX状态中规定时间内没等到tx_done,从而timeout,产生中断
Auto FLD_RF_IRQ_INVALID_PID: PTX或PRX接收到invalid pid,产生中断
Auto FLD_RF_IRQ_FIRST_TIMEOUT: BRX、PRX、SRX、SRT第一次rx timeout,当第一次收包超时时会产生中断
Auto FLD_RF_IRQ_WIFI_DENY: 蓝牙芯片在与WiFi芯片连接时接收到WiFi芯片发来的wifi_deny信号后,蓝牙芯片产生中断
Auto/Manual FLD_RF_IRQ_RX: 每收完一个包都会产生中断
Auto/Manual FLD_RF_IRQ_TX: 每发完一个包都会产生中断
Auto/Manual FLD_RF_IRQ_SUPP_OF: 该中断主要用于AOA和AOD,若iq采样频率过高硬件FIFO会溢出出错,产生中断iq sample 同步fifo overflow

包格式

每种模式下的收发包数据包在ram的格式有所不同,下面根据收包和发包来分别介绍不同模式下的包内容在ram中的格式。

所有mode中的发包格式的前四个字节都是DMA_LEN_INFO,可以调用如下函数获得并填入:

DMA_LEN_INFO = rf_tx_packet_dma_len (data_len).

注意:

  • 接收/发送buffer必须四字节对齐。例如: unsigned char rx_packet[128*4] __attribute__ ((aligned (4)));

BLE包格式

(1) BLE发包格式

发送数据包在RAM内的格式如下所示(其中,payload length为数据长度,data是要发送的数据):

BLE TX Packet:

Address Content
addr, addr + 1, addr + 2, addr + 3 DMA_LEN_INFO: rf_tx_packet_dma_len(payload length+2).
addr + 4 header0
addr + 5 header (payload length)
addr + 6 data(0)
addr + 7 data(1)
addr + 8 data(1)
..... .....
addr +6+(length-1) data(length-1)

(2) BLE收包格式

当SoC处于BLE模式进行接收包时,接收到的包数据在ram中的存储格式如下表:

BLE收包格式:

Address Content Description
rba-4, rba-3, rba-2, rba-1 - -
rba header0 refer to Bluetooth low energy spec
rba+1 header1 (payload length) indicate length of payload only, do not include 3 crc bytes
rba+2 data(0) payload
rba+3 data(1) payload
rba+4 data(2) payload
..... ..... payload
rba+2+(length-1) data(length-1) payload
rba+2+(length) crc(0) crc byte0
rba+2+(length+1) crc(1) crc byte1
rba+2+(length+2) crc(2) crc byte2
rba+2+(length+3) r_tstamp[7:0] time stamp byte0
rba+2+(length+4) r_tstamp[15:8] time stamp byte1
rba+2+(length+5) r_tstamp[23:16] time stamp byte2
rba+2+(length+6) r_tstamp[31:24] time stamp byte3
rba+2+(length+7) pkt_fdc[7:0] low byte of recorded frequency offset after demodulation
rba+2+(length+8) {1'b0,rx_packet_chn_efuse[2:0]}, high byte of recorded frequency offset after demodulation
rba+2+(length+9) pkt_rssi recorded packet RSSI
- [0] crc error
- [1] sfd error
- [2] link layer error
- [3] power error
- [4] long range 125K indicator
- [6:5] N/A
- [7] NoACK indicator

(3) BLE收包数据解析

根据前面章节介绍的收包格式,接收包中常用的信息可以通过接口获取,已经封装了相关的函数:

函数 说明
rf_ble_packet_crc_ok(p) 判断收包CRC是否正确
rf_ble_dma_rx_offset_crc24(p) 获取CRC在包中位置的index
rf_ble_dma_rx_offset_time_stamp(p) 获取time_stamp在包中的index值
rf_ble_dma_rx_offset_freq_offset(p) 获取frequency offset在包中的index值
rf_ble_dma_rx_offset_rssi(p) 获取rssi在包中的index值

(4) 收包解析示例

收包解析示例

Zigbee/hybee包格式

(1) Zigbee/hybee发包格式

Zigbee 250K以及hybee模式的发包数据在ram中的存储格式如下表:

Zigbee/Hybee TX Packet:

Address Content
tba, tba + 1, tba + 2, tba + 3 DMA_LEN_INFO: rf_tx_packet_dma_len(payload length-1)
tba + 4 Payload length
tba + 5 data(0)
tba + 6 data(1)
tba + 7 data(1)
..... .....
tba+5+(length-3) data(length-3)

(2) Zigbee/hybee收包格式

当SoC处于zigbee 250K或者hybee模式下收包,收到的包数据在ram中存储格式如下表所示:

Zigbee/hybee收包格式:

Address Content Description
rba-4, rba-3, rba-2, rba-1 - -
rba length (payload+crc) indicate length of payload and 2 crc bytes
rba+1 data(0) payload
rba+2 data(1) payload
rba+3 data(2) payload
..... ..... payload
rba+1+(length-3) data(length-3) payload
rba+1+(length-2) crc(0) crc byte0
rba+1+(length-1) crc(1) crc byte1
rba+1+(length) r_tstamp[7:0] time stamp byte0
rba+1+(length+1) r_tstamp[15:8] time stamp byte1
rba+1+(length+2) r_tstamp[23:16] time stamp byte2
rba+1+(length+3) r_tstamp[31:24] time stamp byte3
rba+1+(length+4) pkt_fdc[7:0] low byte of recorded frequency offset after demodulation
rba+1+(length+5) {1'b0,rx_packet_chn_efuse[2:0]}, high byte of recorded frequency offset after demodulation
rba+1+(length+6) pkt_rssi recorded packet RSSI
rba+1+(length+7) [0] crc error
rba+1+(length+7) [1] sfd error
rba+1+(length+7) [2] link layer error
rba+1+(length+7) [3] power error
rba+1+(length+7) [4] long range 125K indicator
rba+1+(length+7) [6:5] N/A
rba+1+(length+7) [7] NoACK indicator

(3) 收包数据解析

根据包格式对接收到包内信息的index进行获取。

函数 说明
rf_zigbee_packet_crc_ok(p) 判断收包CRC是否正确
rf_zigbee_dma_rx_offset_crc(p) 获取CRC在包中位置的index
rf_zigbee_dma_rx_offset_time_stamp(p) 获取time_stamp在包中的index值
rf_zigbee_dma_rx_offset_freq_offset(p) 获取frequency offset在包中的index值
rf_zigbee_dma_rx_offset_rssi(p) 获取rssi在包中的index值

(4) 收包解析示例

根据前面章节中的zigbee/hybee模式下收包的存储格式,可以对收到的数据进行解析,示例如下:

收包解析示例

Private包格式

Private模式下可以分为SB和TPLL(Telink Proprietary Link Layer)两种,本节分别介绍两者的收发包格式

Private TPLL发包格式

当SoC在private模式下采用TPLL的方式发包时,包数据的存储格式如下表:

Private TPLL TX Packet:

Address Content
tba, tba + 1, tba + 2, tba + 3 DMA_LEN_INFO: rf_tx_packet_dma_len(payload length).
tba + 4 Payload length
tba + 5 data(0)
tba + 6 data(1)
..... .....
tba+5+(length-1) data(length-1)

Private TPLL收包格式

当SoC处于Private TPLL接收数据包时,接收到的数据包在RAM中的格式如下所示:

Private TPLL模式下的收包格式:

Address Content Description
rba-4, rba-3 0 -
rba-2 - -
rba-1 - -
rba payload length indicate length of payload only, do not include 2 crc bytes
rba+1 data(0) payload
rba+2 data(1) payload
rba+3 data(2) payload
..... ..... payload
rba+1+(length-1) data(length-10) payload
rba+1+(length) crc(0) crc byte0
rba+1+(length+1) crc(1) crc byte1
rba+1+(length+2) r_tstamp[7:0] time stamp byte0
rba+1+(length+3) r_tstamp[15:8] time stamp byte1
rba+1+(length+4) r_tstamp[23:16] time stamp byte2
rba+1+(length+5) r_tstamp[31:24] time stamp byte3
rba+1+(length+6) pkt_fdc[7:0] low byte of recorded frequency offset after demodulation
rba+1+(length+7) {1'b0,rx_packet_chn_efuse[2:0]},{1'b0,pkt_fdc[10:8]} high byte of recorded frequency offset after demodulation
rba+1+(length+8) pkt_rssi recorded packet RSSI
rba+1+(length+9) [0] crc error
rba+1+(length+9) [1] sfd error
rba+1+(length+9) [2] link layer error
rba+1+(length+9) [3] power error
rba+1+(length+9) [4] long range 125K indicator
rba+1+(length+9) [6:5] N/A
rba+1+(length+9) [7] NoACK indicator

TPLL收包解析

根据包格式对接收到包内信息的index进行获取

函数 说明
rf_pri_tpll_packet_crc_ok(p) 判断收包CRC是否正确
rf_pri_tpll_dma_rx_offset_crc(p) 获取CRC在包中位置的index
rf_pri_tpll_dma_rx_offset_time_stamp(p) 获取time_stamp在包中的index值
rf_pri_tpll_dma_rx_offset_freq_offset(p) 获取frequency offset在包中的index值
rf_pri_tpll_dma_rx_offset_rssi(p) 获取rssi在包中的index值

TPLL收包解析示例

TPLL收包解析示例

Private SB发包格式

当SoC处于private SB模式下进行发包时,数据在ram中的存储结构如下表:

Private SB TX Packet:

Address Content
tba, tba + 1, tba + 2, tba + 3 DMA_LEN_INFO: rf_tx_packet_dma_len(payload length).
tba + 4 Payload length
tba + 7 data(1)

注意:

  • 在private SB模式下发包格式中不包含payload长度信息,所以发包格式有所不同。Payload长度可以通过函数void rf_set_private_sb_len()进行设置。

Private SB收包格式

当SoC处于private SB模式进行收包时,收到的包数据在ram中的存储数据如下表所示:

Private SB接收数据包格式:

Address Content Description
rba-4, rba-3 0 -
rba-2 - -
rba-1 - -
rba data(0) payload
rba+1 data(1) payload
rba+2 data(2) payload
..... ..... payload
rba+1+(length-1) data(length-1) payload
rba+1+(length) crc(0) crc byte0
rba+1+(length+1) crc(1) crc byte1
rba+1+(length+2) r_tstamp[7:0] time stamp byte0
rba+1+(length+3) r_tstamp[15:8] time stamp byte1
rba+1+(length+4) r_tstamp[23:16] time stamp byte2
rba+1+(length+5) r_tstamp[31:24] time stamp byte3
rba+1+(length+6) pkt_fdc[7:0] low byte of recorded frequency offset after demodulation
rba+1+(length+7) {1'b0,rx_packet_chn_efuse[2:0]},{1'b0,pkt_fdc[10:8]} high byte of recorded frequency offset after demodulation
rba+1+(length+8) pkt_rssi recorded packet RSSI
rba+1+(length+9) [0] crc error
rba+1+(length+9) [1] sfd error
rba+1+(length+9) [2] link layer error
rba+1+(length+9) [3] power error
rba+1+(length+9) [4] long range 125K indicator
rba+1+(length+9) [6:5] N/A
rba+1+(length+9) [7] NoACK indicator

SB收包解析示例

SB收包解析示例

注意:

  • 所有模式的demo框架基本一致,但在private模式下还需要区分TPLL和SB两种情况;在使用TPLL方式时操作基本与其他模式一致,但当使用SB模式收发包时需要配置相应的寄存器打开SB模式,对应接口void rf_private_sb_en(void)。同时因为SB模式下包格式中未包含payload length信息,因此需要对接收和发送端payload length进行设置,对应接口void rf_set_private_sb_len(int pay_len)。

Manual mode

Manual TX

单频发送:

在manual TX模式下进行单频点发送,设置步骤:

rf_set_txmode(); //进入tx模式等待触发发包
delay_us(113); //等待PLL稳定
while(1)
{
    rf_tx_pkt(ble_tx_packet); //触发发包
    while(! (rf_get_irq_status(FLD_RF_IRQ_TX))); //等待发包结束
    rf_clr_irq_status(FLD_RF_IRQ_TX); //清除中断标志
}

注意:

  • 在manual模式下,tx_settle 时间需要人为的去控制等待settle阶段的PLL稳定。Settle时间最小为112.5us,由于在manual模式下设置完rf_set_txmode() PLL一直在工作,所以该动作只需要在打开manual tx时做一次。

跳频发送:

若在发送过程中需要切换频点,则需要等待一个包发送完成才能进行频点切换并进行下一次发包,设置步骤:

//首先完成初始化动作
rf_set_ble_chn(17);// 2440MHz
rf_set_txmode();
delay_us(113);//等待PLL稳定
rf_tx_pkt(ble_tx_packet);//触发发包
while(! (rf_get_irq_status(FLD_RF_IRQ_TX))); //等待发包完成
rf_clr_irq_status(FLD_RF_IRQ_TX); //清中断状态
//跳频发送时通常需要等待前一个包发完再进行频点切换,然后触发下一次发包。
rf_set_ble_chn(37); //切换频点到2402MHz
rf_tx_pkt(ble_tx_packet); //再次触发发包

Manual RX

单频接收:

在完成初始化设置后如果需要使用manual模式进行发包,可以通过rf_set_rxmode()接口进行设置进入收包状态。设置步骤:

rf_set_rxmode();//进入收包模式,该阶段收不到包,需等待PLL稳定
    delay_us(85); //等待PLL稳定之后进入真正收包阶段,如果不切换状态的话,会一直处于RX状态,一直可以收包
    while(1)
    {
        if(rf_get_irq_status(FLD_RF_IRQ_RX)) //判断收包是否完成
        {
            if(rf_ble_packet_crc_ok(rx_packet)) //判断收到包CRC是否正确
            {
                rx_cnt++; //收包计数
            }
            rf_clr_irq_status(FLD_RF_IRQ_RX); //清收包中断状态
        }
    }

注意:

  • 在manual模式下,rx_settle 时间需要人为的去控制等待settle阶段的PLL稳定。Settle时间最小为85us,由于在manual模式下设置完rf_set_rxmode() PLL一直在工作,所以该动作只需要在打开manual rx时做一次。
  • 当收完一个包后manual模式下PLL一直处于打开状态,所以在收完一个包后清除中断标志就会进入下一个收包状态。
void rf_set_rx_dma (unsigned char *buff,unsigned char wptr_mask, unsigned short fifo_byte_size)

函数说明:

参数 说明
buff 收包地址
wptr_mask 收包时dma写指针mask = FIFO个数 - 1
fifo_byte_size FIFO大小

注意:

  • manual模式下,由于硬件不会维护读写指针,因此只会使用一个FIFO进行接收,所以在设置dma时FIFO个数通常设置为1,设置示例:rf_set_rx_dma(rx_packet,0,128)一个FIFO,FIFO大小为128byte。

manual模式设置示例

跳频接收:

如在收包过程中需要进行跳频接收,设置示例如下:

// 首先完成初始化动作
rf_set_ble_chn(17);// 2440MHz
rf_set_rxmode();//进入收包模式
delay_us(85);//等待PLL稳定,稳定后进入actual收包阶段
if(rf_get_irq_status(FLD_RF_IRQ_RX))
{
    if(rf_ble_packet_crc_ok(rx_packet))
    {
                rx_cnt++;
    }
    rf_clr_irq_status(FLD_RF_IRQ_RX);
}
while(rf_receiving_flag());//等待收包结束(目前使用上会等待收包完成,收包状态直接打断是否存在问题仍在确认)
rf_set_ble_chn(37);//切换频点到2402MHz等待下一次收包

发送接收切换:

如在运行过程中需要切换收发模式,代码示例:

// 首先完成初始化动作
rf_set_ble_chn(17);// 2440MHz
rf_set_txmode();//进入收包模式
delay_us(113);//等待PLL稳定,稳定后进入actual收包阶段
while(! (rf_get_irq_status(FLD_RF_IRQ_TX))); //等到发包结束
rf_clr_irq_status(FLD_RF_IRQ_TX); //清除中断状态
//切换状态的过程中需要等待上一个状态结束后才能进行状态切换
rf_tx_rx_off();//关闭tx,rx
rf_set_rx_mode(); //进入rx状态
delay_us(85);//等待PLL稳定

Auto mode

STX

在Auto mode下状态机的工作流程如下:

Auto mode下状态机的工作流程

调用函数void rf_start_stx (void* addr, unsigned int tick)触发STX,进入tx settle,settle结束进入actual tx状态。

void rf_start_stx (void\* addr, unsigned int tick)

函数说明:

参数 说明
addr 发包地址
tick 当前tick值大于等设置tick时立即触发

注意:

  • TX_SETTLE 时间默认值为150us,可以调用接口void rf_tx_settle_us(unsigned short txstl_us)对settle时间进行调整,但是tx settle时间不应小于112.5us。

单频发送:

调用rf_start_stx函数将触发状态机进入TX(包含tx settle),发包完成,回到IDLE状态,设置步骤:

// 首先完成初始化动作
rf_start_stx(ble_tx_packet,clock_time());//触发第一次发包,发包完成后状态机自动回到IDLE状态
    while(1)
    {
        while(! (rf_get_irq_status(FLD_RF_IRQ_TX))); //判断发包是否完成
        rf_clr_irq_status(FLD_RF_IRQ_TX);//
        rf_start_stx(ble_tx_packet,clock_time());
    }

跳频发送:

若在发包过程中需要切换频点,设置步骤如下:

// 首先完成初始化动作
rf_set_ble_chn(17);// 2440MHz
rf_start_stx(ble_tx_packet,clock_time());//触发第一次发包,发包完成后状态机自动回到IDLE状态
while(! (rf_get_irq_status(FLD_RF_IRQ_TX)));//判断发包是否完成
rf_clr_irq_status(FLD_RF_IRQ_TX);//清除发包中断状态
rf_set_ble_chn(37);//2402MHz切换频点
rf_start_stx(ble_tx_packet,clock_time());//触发再次发包

SRX

Auto mode下收包时状态机的工作过程如下图:

Auto mode下收包时状态机的工作过程

auto mode使用函数void rf_start_srx(unsigned int tick);触发SRX,进入rx settle,settle结束进入actual rx,收包完成自动回到idle状态,在规定时间内没有同步到包则触发FLD_RF_IRQ_RX_TIMEOUT中断。

void rf_start_srx (unsigned int tick)

函数说明:

参数 说明
tick 当前tick值大于等设置tick时立即触发

注意:

  • rx_settle时间默认值为150us,可以调用接口void rf_rx_settle_us(unsigned short txstl_us)对settle时间进行调整,但是tx settle时间不应小于85us。

单频接收:

如在auto模式下进行单频点接收,设置步骤:

// 首先完成初始化动作
rf_start_srx(clock_time());//触发后状态机会从IDLE状态进入同步状态,同时timeout开始计时
    while(1)
    {
        if(rf_get_irq_status(FLD_RF_IRQ_RX))//判断收包是否结束
        {
            u8* raw_pkt = rf_get_rx_packet_addr(RX_FIFO_NUM,RX_FIFO_DEP,rx_packet);//查找当前收包地址,仅auto mode下需要。
            if(rf_ble_packet_crc_ok(raw_pkt))//判断包的CRC是否正确
            {
                rx_cnt++;//记录收包数
                rf_clr_irq_status(FLD_RF_IRQ_RX);//清除中断状态
                rf_start_srx(clock_time());//触发下次收包
        }
     }

注意:

  • 在auto mode下DMA会根据FIFO个数和FIFO深度在往ram地址中搬运数据时自动进行偏移。因此每次收包完成后需要通过rf_get_rx_packet_addr(int fifo_num,int fifo_dep,void* addr)来查找本次收包内容在ram中的地址进行crc校验。

rf_get_rx_packet_addr函数说明如下:

参数 说明
fifo_num FIFO个数
fifo_dep 每个FIFO大小
addr 收包地址

当使用函数rf_set_rx_dma(rx_packet,3,128);设置DMA时会有4个FIFO进行收包。

收第一个包前

收第二个包前

直到4个FIFO收满,回到第一个FIFO。下图为auto模式下收包结果,其中FIFO个数为4,FIFO大小为128byte。

auto模式下收包结果

跳频接收:

若在收包过程中需要切换频点,设置步骤如下:

// 首先完成初始化动作
rf_set_ble_chn(17);// 2440MHz
rf_start_srx(clock_time());//触发收包,收包完成后状态机自动回到IDLE状态
if(rf_get_irq_status(FLD_RF_IRQ_RX))//判断收包结束
{
    u8* raw_pkt = rf_get_rx_packet_addr(RX_FIFO_NUM,RX_FIFO_DEP,rx_packet);//查找收包地址
    if(rf_ble_packet_crc_ok(raw_pkt))
    {
            rx_cnt++;
    }
    rf_clr_irq_status(FLD_RF_IRQ_RX);
}
while(rf_receiving_flag());//判断发包是否完成
rf_clr_irq_status(FLD_RF_IRQ_RX);//清除收包中断状态
rf_set_ble_chn(37);// 2402MHz切换频点
rf_start_srx(clock_time());//触发再次收包

自动模式切换:

如若在auto模式下进行收发切换,代码示例:

// 首先完成初始化动作
rf_set_ble_chn(17);// 2440MHz
rf_set_stx(ble_tx_packet,clock_time());
while(! (rf_get_irq_status(FLD_RF_IRQ_TX))); //等待发包结束
rf_clr_irq_status(FLD_RF_IRQ_TX); //清除中断状态
// 在进行状态切换时需等待前面一个状态结束才可进行状态切换
rf_set_tx_rx_off_auto_mode();//关闭tx,rx
rf_start_srx(clock_time());//触发收包

注意:

  • 目前在进行状态切换时通常等到前一个状态结束后再停状态机,然后切换下一个状态。

ISO-7816

ISO-7816协议简介

ISO-7816即国际智能卡通信标准,规定了接触式智能卡的相关规范,包括物理特性、接口规范、传输协议、命令交换格式等。

Telink SoC集成了ISO-7816通信模块,支持与接触式IC卡的通信,本文简单介绍了建立Telink SoC与接触式IC卡通信的方法。

ISO-7816使用方法

硬件连接

硬件连接

在实际使用时,我们需要将IC卡的各个触点一一与SoC相连,这是SoC与IC卡通信的硬件基础。

(1) VCC是IC卡的电源电压,RST是IC卡的复位信号。我们可以在SoC的空余GPIO引脚中任意选择两个与之相连。

(2) CLK是IC卡的时钟触点。由SoC供给时钟给IC卡。

(3) TRX即I/0触点是IC卡输入输出触点,由于IC卡只支持半双工通信,在某一刻I/O触点只支持输入或输出,所以在实际使用时需要注意时序。

注意:

  • IS07816-3协议规定了IC卡3种工作电压:A类-5V,B类-3V,C类-1.8V,在实际使用时需要SoC提供的电压和卡片的工作电压相匹配。

初始化

s7816_set_pin(gpio_pin_e rst_pin,gpio_pin_e vcc_pin,gpio_pin_e clk_pin,gpio_pin_e trx_pin)
s7816_init(uart_num_e uart_num,s7816_clock_e clock,int f,int d)

s7816_set_pin()用来配置RST,VCC,CLK,TRX引脚。

S7816_init()用来选择UART通道(UART0和UART1),配置IC卡时钟,以及IC卡时钟频率调节因子F(默认为372)和比特率调节因子D(默认为1)。

s7816_en(uart_num_e chn)

配置完成后需要使用函数s7816_en()来使能7816模块。

注意:

  • S7816是通过SoC的UART功能来实现的,所以使用时会占用对应的UART模块。

IC卡激活与冷复位

s7816_coldreset();

ISO7816协议规定,复位引脚拉起之后,冷复位应答将在之后的400-40000个时钟周期之内开始。

冷复位

以SoC时钟配置4MHZ为例,冷复位过程:

a) VCC拉起之后,从Ta时刻SoC输出CLK,SoC配置TRX引脚,SoC的TRX引脚配置之后,置SoC的TRX引脚为接收状态。

b) 在Ta后的40000周期内(10000us),RST拉高。

c) RST拉高之后的40000个周期内(10000us),IC卡会传回复位字符给SoC。

s7816_set_time(int rst_time_us)

s7816_set_time()用来对冷复位中的s7816_rst_time时间进行重设。

(1) rst_time_us对应s7816_rst_time,即时序中Ta到Tb的复位等待时间,默认为40000个时钟周期。

(2) s7816_atr_time,即时序中Tb之后的等待ATR返回时间,范围在400-40000个时钟周期不定。在实际使用时需要等ATR字符接收完成之后再进行下一步操作(可以在主函数进行协议解析来判断ATR字符是否接收完成)。

热复位

s7816_warmreset()

热复位

IC卡对终端的复位应答有着规定的规格和内容,如果中断收到的复位应答不符合规定要求时,终端将启动一个热复位并从IC卡获得复位信号。

以时钟配置4MHZ为例,热复位过程:

a) CLK和VCC始终保持正常状态(CLK施加,VCC拉起)。

b) 在Tc时刻,将RST从高置低。

c) 在Tc时刻的200个周期内(50us),SoC置TRX引脚为接收状态。

d) 在Tc后40000个周期内(10000-10000us),将RST置高电平。IC卡的复位应答将在Td后40000个周期内开始(10000us)。

和冷复位相同,热复位同样可以使用s7816_set_time()函数重设s7816_rst_time (默认值40000个时钟周期)。需要等待ATR字符接收完成才可以进行下一步操作。

注意:

  • 冷复位和热复位的复位应答(初始ATR)是相同的。两者的差别在于:冷复位伴随着IC卡的激活,热复位是在IC卡激活后进行的。

触点释放

s7816_release_trig()

触点释放

冷复位或者热复位时,如果IC卡没有在规定时间内进行复位应答,则终端需要启动一个触点释放时序。

(1) 终端以置RST为低电平状态开始触点释放序列。

(2) 在置RST为低电平之后且VCC断电之前,终端将CLK和I/O也置低电平。

(3) 最后,在实际断开触点之前,终端将VCC去电。

Demo简介

以经过初始化的SMARTCOS-PSAM卡为例。Demo先进行冷复位,再取随机数。

取随机数指令为:0x00,0x84,0x00,0x00,0x04(取4字节的随机数)

冷复位获得初始ATR:

冷复位获得初始ATR

初始ATR共16个byte。初始ATR分析如下:

(1) 3B:正向约定。

(2) 6C:即T0,6二进制为0110,表示TB1和TC1存在,C表示历史字符为12个。

(3) 由T0得知,TB1为00,表示无需额外的编程电压。

(4) 由T0得知,TC1为02,需要额外两个保护时间,即由终端向IC卡发送数据时,每两个byte之间需要额外增加两个etu的时间。

(5) 由T0得知,12个历史字符为0x58,0x02,0x86,0x38,0x50,0x53,0x41,0x4d,0x80,0x00,0x83,0x73.

(6) IC卡使用T=0协议,没有TCK校验字符。

取随机数:

取随机数

其中,0x00,0x84,0x00,0x00,0x04为终端向IC卡发送的取随机数指令。

取得的随机数为0xf0,0x44,0xef,0x7f共4个byte,每次取的随机数都会不同。

数据尾9000表示指令执行成功。

ADC

简介

ADC驱动可用于ADC采样外部GPIO电压、电池电压和温度传感器。

工作原理

内部结构

SAR_ADC的内部结构如下图所示:

ADC的内部结构

SAR ADC只支持差分模式,通过差分采样获取code值,然后由驱动换算成电压值或温度值输出。

应用场景 P端 N端
单个GPIO采样 ADC_GPIO GND
两个GPIO差分采样 ADC_GPIO1 ADC_GPIO2
Vbat channel VBAT GND
Temp sensor ADC_TEMSENSORP_EE ADC_TEMSENSORN_EE

P端和N端电压需满足的条件:

单个GPIO采样的范围,可以参照下面的(1)和(2)来确定最终的采样范围。

两个GPIO差分采样的适用范围,需要满足以下四个条件:

(1)P端和N端的电压均不允许超过prescal*Vreference;

(2)P端和N端的电压均不允许超过V_ioh(IO输出高电平时的电压值);

(3)P端和N端的差值电压不允许超过prescal*Vreference;

(4)((Vp+Vn)/2)<(prescal*Vreference)。

芯片 V_ioh(IO输出高电平时的电压值)
B85 V_ioh=vbat<3.6v
B87 V_ioh=vbat<3.6v
B91 (1)当应用场景中vbat电压一定低于3.6v时,设置VBAT_MAX_VALUE_LESS_THAN_3V6模式,V_ioh=vbat;(2)当应用场景中vbat电压可能高于3.6v时,设置VBAT_MAX_VALUE_GREATER_THAN_3V6模式,且vbatV_ldo时,V_ioh=V_ldo。(V_ldo=3.3v(+-10%))

采样电压值计算

模拟输入电压(\(V_{IN}\))与参考电压(\(V_{REF}\)) 比较,按比例生成N位采样code值,存储在寄存器中。实际应用时一般会对\(V_{IN}\)进行预分压来支持更大的采样范围,以14位分辨率采样code值为例,当预分压系数pre_scale = 1/4时,\(V_{IN}\)与code值的换算公式为:

\[\frac {{\frac 14}*{V_{IN}}}{V_{REF}} = \frac {adc\_code}{ref\_code}\]

其中:adc_code为采样\(V_{IN}\)得到的code值。

ref_code为\(V_{REF}\)换算得到的code值,14位分辨率对应0x1fff(bit13 为符号位)。

以此反推可以得到\(V_{IN}\)的采样电压值。

注意:

  • 对于GPIO,经过prescale分压后就可以作为差分信号与参考电压进行比较。但是Vbat需要经过Vbat divider和prescale两次分压才可以进行比较,最终的分压系数为两次分压系数的乘积。例预分压系数pre_scale = 1且Vbat_divider = 1/3时,\(V_{IN}\)与code值的换算公式为:
\[\frac {{\frac 13}*{1}*{V_{IN}}}{V_{REF}} = \frac {adc\_code}{ref\_code}\]

B91 ADC使用说明

接口说明

接口命名规则:

  • init后缀:初始化用到的接口。
  • dma后缀:dma模式采样会用到的接口。
  • 无dma后缀:手动采样会用到的接口。

Demo说明

Demo结构说明

ADC Demo的应用.c文件分别为app.c, ADC_Demo/app_config.h中的宏ADC_MODE选择使用哪一种采样模式。

#define ADC_DMA_MODE                 1
#define ADC_NDMA_MODE                2

#define ADC_MODE               ADC_NDMA_MODE

在ADC_NDMA_MODE(手动采样模式)和ADC_DMA_MODE(DMA采样模式)中,通过配置宏ADC_SAMPLE_MODE选择ADC的使用场景为GPIO模拟信号输入、电池电压(Vbat)和温度传感器中的一种。

#define ADC_GPIO_SAMPLE          1 //GPIO voltage
#define ADC_VBAT_SAMPLE          2 //Vbat channel Battery Voltage
#define ADC_TEMP_SENSOR_SAMPLE           3 //Temp test

#define ADC_SAMPLE_MODE              ADC_GPIO_SAMPLE

ADC的初始化配置

ADC的初始化流程如下图所示:

ADC的初始化流程

ADC的采样和转换过程

ADC的采样和转换如下图所示:

ADC的采样和转换

注意:

  • 手动(ADC_NDMA_MODE)采样时,一次只能获取一个adc_code,期间会关闭ADC采样功能,且在使用时一定要保证连续获取adc_code的时间间隔大于2个采样周期。

Demo测试实例

配置DMA方式采样Vbat。

通过BDT工具查看采样结果如下:

采样结果

图中:adc_sample_buffer存储的为8组采样的code值,adc_vol_mv_val代表采样电压值,0xae2换算成10进制为2786mV(电压表测得2790mV)。

芯片差异

功能支持差异

芯片 GPIO采样 Vbat采样方式 是否支持Temp采样
B85 PB0-7/PC4-5 不支持vbat channel模式,是利用将gpio输出高进行gpio采样的方式进行vbat采样的,此时GPIO的电压就是Vbat的电压(该方法是不需要硬件连线的,可以设置一个没有封装出的pin,以节省gpio资源) 不支持
B87 PB0-7/PC4-5 vbat channel 支持
B91 PB0-7/PD0-1 硬件电路上使用外部分压,软件使用GPIO方式进行采样 支持

校准配置说明

下面列出有芯片级校准的配置,推荐使用这些配置以减少误差(如果使用其他配置,校准值就不准了),如果想要误差更小,可以在产线上用夹具进行板级校准。

芯片 出厂带的校准值 采样误差(测试数据量较少,仅供参考)
B85 GPIO采样,采样率96KHz,预分压系数1/8,参考电压1.2V。 误差在-14~12mV。(29颗样品芯片)
B87 GPIO采样,采样率96KHz,预分压系数1/8,参考电压1.2V。 误差在9~12mV。(20颗样品芯片)
B87 vbat channel采样,采样率96KHz,预分压系数1,vbat分压系数1/3,参考电压1.2V。 误差在10mV以内。(10颗样品芯片)
B91 GPIO采样,采样率48KHz,预分压系数1/4,参考电压1.2V。 误差在-11~7mV。(19颗样品芯片)

信噪比和误差概念的区分

B91的datasheet注明信噪比为10.5bit,含义如下:

信噪比(有效位)为10.5位,对应模拟量1200mV/(2^10.5)≈0.82mV(假设参考电压选择1200mV),意思是ADC采样精度为0.82mV,即1单位的code值表示0.82mV电压值。

误差的概念,比如误差是10mV,输入500mV,采样结果是510mV。

外部分压电路

当需要采样的电压超过ADC采样范围时,必须使用外部分压电路将原电压分压至采样范围,再输入到采样点,并通过GPIO采样。建议的外部分压电路配置如下(可以根据各自的应用需求去选择):

硬件电路参考:

由于芯片内阻就有几十M,所以分压电路的总阻值不能过大(最好不要超过2M),否则电流过小,ADC无法正常采样。

M级分压电路:漏电小,采样慢

M级分压电路

百K级分压电路:漏电大,采样快

百K级分压电路

软件使用要求:

sar adc的输入等效是一个15pF左右的电容,在采样时会有一个等效\(f*c*v\)的动态电流,所以电阻分压的采样点的电压值会慢慢的产生一个误差电压。 所以

(1)打开ADC立刻采样时,采样点的值还是处在比较准确的地方,所以采样值较准确。

(2)关闭ADC后延时,由于没有动态电流,分压点电阻值会重新回到准确的值。

(3)采样率越高,误差电压越大。

针对不同工作模式下,有不同的解决方法。以B91为例说明。

(下面的标注出的延时部分,会和采样频率以及电压电流等相关,所以使用时仅需要参考下面的逻辑处理方式,但是相关延时部分,请根据实际应用进行测试,并预留一定的余量出来。)

硬件配置:B91校准芯片 + M级分压电路

软件配置(ADC_Demo):

  • 1.2V Vref基准电压
  • 1 / 4的pre_scale预分压系数
  • 采样频率23K

(1)正常工作模式时:

    a. 打开ADC Power立刻采样,然后立刻关闭ADC Power
    b. 延时(大于50ms,小于这个值误差会偏大些)
    c. 进行下一次采样,采样步骤跟a和b一样

注意:

  • 如果需要切换PIN,切换PIN的操作加在步骤a前即可。

(2)deep或者deep retention模式时,采样后会进入deep或者deep retention,不需要额外操作。

(3)suppend模式时,采样后会进入suppend,配置suspend的时间大于200ms。

芯片级校准误差为-9 \~ 5mV;板级校准误差为-3 \~ 8mV。(10颗样本芯片)

硬件配置:B91校准芯片 + 百K级分压电路

软件配置(ADC_Demo):

  • 1.2V Vref基准电压
  • 1 / 4的pre_scale预分压系数
  • 采样频率23K

(1)正常工作模式时:

    a. 打开ADC Power立刻采样,然后立刻关闭ADC Power
    b. 延时(大于5ms,小于这个值误差会偏大些)
    c. 进行下一次采样,采样步骤跟a和b一样

注意:

  • 如果需要切换PIN,切换PIN的操作加在步骤a前即可。

(2)deep或者deep retention模式时,采样后会进入deep或者deep retention,不需要额外操作。

(3)suppend模式时,采样后会进入suppend,配置suspend的时间大于50ms。

芯片级校准误差为-5 \~ 7mV;板级校准误差为-4 \~ 6mV。(10颗样本芯片)

USB简介

USB (Universal Serial Bus) 即通用串行总线,是一个外部总线标准,用于规范电脑与外部设备的连接和通讯,是应用在PC领域的接口技术。USB接口支持设备的即插即用和热插拔功能。USB是在1994年底由英特尔、康柏、IBM、Microsoft等多家公司联合提出的。如下图所示,USB由四根线组成,即VCC、GND、D-(DM)、和D+(DP),USB可选择通过主机供电也可自供电,目前大部分USB设备是通过主机供电。

USB接口

USB通信是指控制器与设备间的通信,电脑主机即为控制器,Telink USB为设备,后文中引用的主机默认为控制器。USB总线是一种单向总线,通信只能由控制器发起,设备收到控制器请求时,将数据发给控制器。控制器是每隔n个单位时间向设备发送请求,n为用户配置参数。

USB有超高速(5.0Gbit/s)、高速(480Mbit/s)、全速(12Mbit/s)和低速(1.5Mbit/s)等四种工作速度,其中全速和低速USB总线的通信帧周期(连续两帧数据的发送间隔)为1ms,高速USB总线的通信帧周期为125us。USB1.1仅支持全速和低速,USB2.0支持高速、全速和低速,超高速仅在USB3.0中有支持。

USB包格式和传输过程

包(Packet)是USB数据传输的最基本单元,即每次数据传输都是以包的形式进行。而包要组成事务才能进行有效通信。根据通信的需求有各种包,用于组成各种事务(IN、OUT、SETUP)。一个或多个事务组成一个传输(控制传输、批量传输、终端传输和等时传输)。

包是USB总线上数据传输的最小单位,不能被打断或干扰,否则会引发错误。若干个数据包组成一次事务,一次事务也不能打断,即属于一次事务的几个包。

USB包结构

包(Packet)是USB系统中信息传输的基本单元,所有数据都是经过打包后在总线上传输的。

如下图所示,USB包由七部分组成,即同步域(SYNC)、包标识(PID)、地址域(ADDR)、端点域(ENDP)、帧号域(FRAM)、数据域(DATA)和校验域(CRC)。注意,并不是每个USB包都包含上述的七个域,也就是说有些包只包含其中的几个域。

USB包通用格式

(1) 同步域

同步域主要是通知对方数据传输开始,并提供同步时钟。对于低速设备和全速设备,同步域使用的是0000 0001(二进制数);对于高速设备使用的是00000000 00000000 00000000 00000001。

(2) 包标识 (PID)

包标识主要用于标识包的类型,由8位组成:低 4位是PID编码,高4位是校验字段,是对低4位取反得到,USB中各种包是通过PID字段来区分。

(3) 地址域

由于接入USB总线的设备可能有多个,因此需要引入地址域,以便于区分当前通信的设备是哪个设备。地址域包含7个数据位,最多可以指定128个地址,地址0用作缺省地址,不分配给USB设备。对于USB总线上的每个设备,地址唯一。

(4) 端点域

端点域用于指定USB总线上某个设备的一个端点号,包含4个数据位;全速/高速设备最多可以含有16个端点,低速设备最多含有3个端点。所有USB设备都必须含有一个端点号为0的端点,用于主机与设备间交换基本信息。除端点0外,其余的端点都是具体USB设备所特有的。地址域和端点域组合,明确了主机与设备间通信的通道。

(5) 帧号域

帧号字段用于指出当前帧的帧号,它仅在每帧/微帧开始的SOF令牌包中被发送,其数据位长度为11位,每传输一帧,主机就将其加1,当达到最大值7FFH时归零。

(6) 数据域

数据字段包含主机和USB设备间需要传输的数据,以字节为单位,最大长度为1024,而实际长度取决于传输的具体情况。

(7) 校验域

校验域主要是为了校验通信数据的正确性。USB令牌包和数据包中都使用了CRC。但是,CRC是发送方在进行位填充之前产生的,这样要求接收方在去除位填充之后,再对CRC字段进行译码。信息包中的PID字段本身含有校验,所以CRC计算不包含有PID部分。令牌包的CRC采用的是5位CRC,数据包中的数据字段使用的是16位CRC。

令牌包:

SOF包由主机发送给设备:对于full-speed总线,每隔1.00 ms±0.0005 ms发送一次;对于high-speed总线,每隔125us±0.0625us发送一次。

SOF包格式

SOF包格式

IN,OUT,SETUP包格式

IN OUT SETUP包格式

数据包:

数据包(DATA0, DATA1, DATA2, MDATA)

数据包

握手包:

PRE, ACK, NAK, STALL, NYET包格式

PRE ACK NAK STALL NYET包格式

USB传输过程

(1) USB事务

在USB的一次数据接收或发送的处理过程称为事务处理(Transaction),事务通常是由一系列包组成,事务不同,组成的包也不同。USB数据传输中,常见的事务有输入(IN)事务、输出(OUT)事务和设置(SETUP)事务。注意,SOF只是指示一帧的开始,无有效数据,并不是一次事务; EOF帧发送结束后的一种电平状态,也不是事务。

事务通常由两个或者三个包组成:令牌包、数据包和握手包,令牌包启动事务,数据包传输数据,握手包的发送者通常为数据接收者,当数据正确接收后,发送握手包,设备也可以使用NACK表示数据未准备好。

(2) 输入事务

输入(IN)事务是主机从USB设备的某个端点中获取数据的过程,如下图所示,一个输入事务有三种状态,即正常的输入事务(图(a))、设备忙或无数据时的输入事务(图(b))和设备出错时的输入事务(图(c)),正确的输入事务包括令牌包、数据包和握手包三个阶段。

输入事务处理流程

(a) 正常的输入事务

(b) 设备忙或无数据时输入事务

(c) 设备出错时的输入事务

结合正常的输入事务实例,对正常的输入事务进行介绍和分析,如下图所示,一个正常的输入事务包含三个交互流程:(1)Host向Device发送一个IN令牌包;(2)Device收到IN令牌包后,将待发送数据发给主机;(3)主机收到数据包后,回复一个ACK包来确认数据包被正确接收。

正常输入事务实例

(3) 输出事务

输出(OUT)事务是主机向USB设备的某个端点中发送数据的过程,如下图所示,一个输出事务有三种状态,即正常的输出事务(图(a))、设备忙时的输出事务(图(b))和设备出错时的输出事务(图(c))。正确的输出事务包括令牌、数据和握手三个阶段。

输出事务处理流程

(a) 正常的输出事务

(b) 设备忙时的输出事务

(c) 设备出错时的输出事务

下面结合正常的输出事务实例,对正常的输出事务进行介绍和分析,如下图所示,一个正常的输出事务包含三个交互流程:(a)Host向Device发送一个OUT令牌包;(b)Host向Device发送数据包;(c)Device收到数据包后,回复一个ACK包来确认数据包被正确接收。

正常输出事务实例

(4) 设置事务

设置(SETUP)事务处理并定义了Host与Device之间的特殊的数据传输,它仅适用于USB控制传输的建立阶段。如下图所示,设置事务通常有三种状态,即正常设置事务(图(a))、设备忙设置事务(图(b))和设备出错设置事务(图(c)),正确的设置事务包括令牌、数据和握手三个阶段。

设置事务处理流程

(a) 正常设置事务

(b) 设备忙时的设置事务

(c) 设备出错时的设置事务

下面结合正常的设置事务实例,对正常的设置事务进行介绍和分析, 如下图所示,一个正常的设置事务包含三个交互流程:(1)Host向Device发送一个SETUP令牌包;(2)Host向Device发送DATA0数据包;(3)Device收到数据包后,回复一个ACK包来确认数据包被正确接收。

正常设置事务实例

USB传输

传输由OUT、IN或SETUP等事务构成,USB标准协议中定义了4种传输类型:控制传输 (Control Transfer)、批量传输 (Bulk Transfer)、中断传输 (Interrupt Transfer)和同步传输 (Isochronous Transfer)。四种传输的优先级由高到底一次为:同步传输、中断传输、控制传输、批量传输。

(1) 控制传输

控制传输(Control Transfer)是USB中最基础、最重要的传输方式,也是端口0的默认传输方式。控制传输典型地用在主机和USB外设之间的端点(Endpoint)0之间的传输,但是指定供应商的控制传输可能用在其它的端点上控制传输主要用来主要进行查询,配置和给USB设备发送通用的命令。控制传输是单向传输(端点0除外,端点0为双向传输),数据量通常较小。控制传输的数据包最大长度取决于其工作速度,低速模式最大包长固定为8字节、高速模式最大包长度为64字节,全速模式可在8、16、32和64字节中选择。

注意:

  • Telink USB的端点0的包长度为固定长度8字节,无法配置其他值。
  • 控制传输分为三个阶段,即建立阶段、数据阶段(可选)和状态阶段组成,每个阶段都由一次或多次(数据阶段)事务组成。
  • 建立阶段: 如图建立阶段由SETUP事务组成。SETLUP事务的数据阶段总是用DATA0,且定长8字节。

建立事务输流程图

  • 数据阶段: 数据阶段可选。如果有数据阶段,则包括一个或多个IN/OUT事务。用于传输建立阶段要求的,具有USB定义格式的数据。数据阶段的事务具有相同的方向,即要么全是IN,要么全是OUT。如果要传输的数据大于一个包的长度,则主控器将其分成多个包传输,一旦数据传输方向改变,就会认为进入到状态过程,数据过程的第一个数据包必须是DATA1包,然后每次争取传输一个数据包后就在DATA0和DATA1之间交换。如果最后一个包大小等于最大包大小,则应再传一个0大小的包,以确定结束。根据数据阶段的数据传输的方向,控制传输又可分为3种类型,即控制写入(Control Write)、控制读取(Control Read)和无数据控制(No-data Control),如下图所示

控制传输序列图

  • 状态阶段: 状态阶段是控制事务处理的最后一个阶段,由一个IN或OUT事务组成,总是使用DATA1 数据包组成。状态阶段与数据阶段传输的方向相反,即如果数据阶段是IN,则状态阶段是OUT,反之亦然。用于报告建立阶段和数据阶段的传输结果。

(2) 中断传输

中断传输(Interrupt Transfer)在流程上除不支持PING和NYET包之外,其他的和批量传输相同,因此其序列图可参考批量传输。中断传输与批量传输的主要区别体现在两点:(1)优先级不一样,中断传输的优先级高于批量传输,仅次于同步传输;(2)支持最大包长度不同,中断传输低速模式最大包长上限为8字节,全速模式最大包长上限为64字节,高速模式最大包长上限为1024字节。

需要注意的是,这里所说的中断和硬件上的中断是不一样的。由于USB不支持硬件的中断,所以必须靠主机周期性地轮询,以便获知是否有设备需要传送数据给主机。由此可知,中断传输也是一种轮询过程,轮询的周期由用户设备决定(全速设备的轮询间隔为1ms \~ 255ms,低速设备10ms \~ 255ms),主机只需要保证在不大于该时间间隔内,安排一次传输即可。轮询周期非常重要,如果太快,会占用太多的总线带宽,如果太低,数据可能会丢,因此用户需要根据自身数据的状况来设置。

中断传输通常用在数据量不大,但是对时间要求较严格的设备中,如人机接口设备(HID)中的键盘、鼠标等。中断传输也可用来不断检测设备状态,当条件满足时,再使用批量传输来传送大量数据。中断传输的端点类型一般为IN端点,即从Device到Host(IN事务),很少用在OUT端点上,有些电脑甚至不支持中断传输的OUT事务。

中断传输流程图

(3) 同步传输

同步传输(Isochronous Transfer),又叫等时传输,是不可靠传输。同步传输只有令牌包(IN/OUT令牌包)与数据包(DATAx)两个阶段,它没有握手包,也不支持PID翻转,主机在排定传输时,同步传输有最高的优先级。同步传输数据包最大长度为全速模式上限为1023字节,高速模式上限为1024字节,低速模式不支持同步传输。

同步传输流程图

同步传输适用于必须以固定速率抵达或在指定时刻抵达,可以容忍偶尔错误的数据上。USB为其保留总线带宽,保证能在每帧/小帧内都得到服务。速率准确,传输时间可预测。但不采用差错控制和重传机制,不保证每次传输都成功,适用于音频、视频设备。

(4) 批量传输

批量传输(Bulk Transfer),又称为块传输,是单向可靠传输,由一个或多个IN/OUT事务组成,事务中的数据包按照DATA0-DATA1-DATA0-…方式翻转,以保证传输端和接收端的同步,如下图所示。

批量传输流程图

USB中的错误检测和重传机制是通过硬件来完成的,若本次传输错误,DATA包不会翻转,并重新发送该包。同时,接收端接收到连续PID相同的DATA包,将视为重传包。USB允许连续3次以下的传输错误,超过3次,主机认为该端点功能错误(STALL),放弃该端点的传输任务。

USB应用

本章节所讲的USB应用并不是指USB的用途,而是位于USB驱动层之上的应用层设计。本章节将从用户的角度,来详细讲解USB的基本概念和工作原理,以方便用户熟悉和掌握USB的基础知识和使用方法。

基本概念

USB硬件设备和软件设备的关系为,一个USB硬件设备可以对应一个或多个软件设备,这取决于用户的枚举信息(配置描述符信息)。软件设备是PC将硬件设备中,实现同一个功能的类的接口抽象出来,并可统一操作的虚拟设备。一个软件设备包含一个或多个接口,一个接口包含一个或多个端点(端点将在下面讲解),而接口和端点是硬件设备中所有的概念。

端点(Endpoint)是USB设备中的可以进行数据收发的最小单元。除端点0(固定为双向控制传输)之外,其他所有端点仅支持单向通信,即输入端点(数据流为从设备到主机)或输出端点(数据流为从主机到设备)。设备支持端点的数量是有限制的,除默认端点0外,低速设备最多支持2 组端点(2个输入,2个输出),高速和全速设备最多支持15 组端点。

接口(Interface) 是USB设备中组成一个基本功能的端点的集合,是USB设备驱动程序控制的对象(主机会根据接口,在PC端虚拟一个可直接操作的USB设备,该虚拟设备即是一个USB设备类)。从主机的角度来看,一个USB设备可由一个或多个接口组成,如集成了鼠标和键盘的USB设备,有两个Interface,一个键盘,另一个是鼠标;如音频设备就是由一个用于命令传输的接口和一个用于数据传输的接口组成。

总结如下:

端点:端点是USB设备的唯一可识别部分,其是主机和设备之间的通信流的终点,是一个USB设备或主机上的一个数据缓冲区,用来存放和发送USB的各种数据。

接口:可以理解为一个功能。

配置:对接口的组合,在连接期间选定是那种组合。

USB应用

标准描述符

描述符(Descriptor)是用来描述设备的属性的数据结,分为标准描述符和专有描述符。标准描述符是所有USB设备类通用的属性描述,包括设备描述符、配置描述符、接口描述符、端点描述符和字符串描述符等,其中字符串描述符又分为序列号描述符、产品描述符、厂商描述符以及语言ID描述符。专有描述符是每个设备类所特有的描述符,如HID类特有的描述符有HID描述符、报告描述符和实体描述符等,下图为USB协议规定的标准设备请求结构。

标准设备请求的数据结构

设备描述符

设备描述符描述了有关USB设备的基本信息,设备有且只有一个设备描述符。下图给出了标准设备描述符的结构,前8个字节概括了USB的基本属性,这也是USB枚举中主机首先要获取的信息。

偏移量 大小 描述
0 bLength 1 数字 此描述表的字节数
1 bDescriptorType 1 常量 描述符的类型(此处应为0x01,即设备描述符)
2 bcdUSB 2 BCD码 此设备与描述表兼容的USB设备说明版本号(BCD 码)
4 bDeviceClass 1 设备类码
5 bDeviceSubClass 1 子类 子类掩码
6 bDeviceProtocol 1 协议 协议码
7 bMaxPacketSize0 1 数字 端点0的最大包大小(仅8,16,32,64为合法值)
8 idVendor 2 ID 厂商标志(由USB-IF组织赋值)
10 idProduct 2 ID 产品标志(由厂商赋值)
12 bcdDevice 2 BCD码 设备发行号(BCD 码)
14 iManufacturer 1 索引 描述厂商信息的字符串描述符的索引值。
15 iProduct 1 索引 描述产品信息的字串描述符的索引值。
16 iSerialNumber 1 索引 描述设备序列号信息的字串描述符的索引值。
17 bNumConfigurations 1 数字 可能的配置描述符数目

注意:

  • idVendor(VID)和idProduct(PID)用来唯一标识一个设备,但是对于Windows系统来说,仅给定VID和PID,并不能唯一确定设备,表现为不停安装新的驱动。此时,还需要考虑序列号字符串,也就是说只有VID、PID和序列号都一致时,Windows只需要安装一次驱动。
  • 三个字符串描述符的索引值应该是不同的值(0除外)。

配置描述符

配置描述符定义了设备的配置信息,一个设备可以有多个配置描述符。

偏移量 大小 描述
0 bLength 1 数字 此描述表的字节数长度
1 bDescriptorType 1 常量 配置描述表类型(此处为0x02)
2 wTotalLength 2 数字 此配置信息的总长(包括配置,接口,端点描述符)
4 bNumInterfaces 1 数字 此配置所支持的接口个数
5 bConfigurationValue 1 数字 在SetConfiguration(x)请求中用作参数来选定此配置
6 iConfiguration 1 索引 描述此配置的字串描述表索引(0-无)
7 bmAttributes 1 位图 配置特性: D7:保留(设为一) D6:自给电源 D5:远程唤醒 D4..0:保留(设为一)
8 MaxPower 1 mA 在此配置下的总线电源耗费量,以 2mA 为一个单位

接口描述符

接口描述符说明了接口所提供的配置,一个配置所拥有的接口数量通过配置描述符的bNumInterfaces决定。

偏移量 大小 描述
0 bLength 1 数字 此表的字节数
1 bDescriptorType 1 常量 接口描述表类(此处应为0x04)
2 bInterfaceNumber 1 数字 接口号,当前配置支持的接口数组索引(从零开始)。
3 bAlternateSetting 1 数字 可选设置的索引值。
4 bNumEndpoints 1 数字 此接口用的端点数量,端点0除外
5 bInterfaceClass 1 接口所属的类值
6 bInterfaceSubClass 1 子类 子类码
7 bInterfaceProtocol 1 协议 协议码:bInterfaceClass 和bInterfaceSubClass 域的值而定。
8 iInterface 1 索引 描述此接口的字串描述表的索引值。

端点描述符

USB设备中的每个端点都有自己的端点描述符,由接口描述符中的bNumEndpoint决定其数量。

偏移量 大小 描述
0 bLength 1 数字 此描述表的字节数长度
1 bDescriptorType 1 常量 端点描述表类(此处应为0x05)
2 bEndpointAddress 1 端点 此描述表所描述的端点的地址、方向:Bit 3..0 : 端点号. 端点编号在改配置中不能重复; Bit 6..4 : 保留,为零; Bit 7: 方向,如果控制端点则略。0:输出端点(主机到设备)1:输入端点(设备到主机)
3 bmAttributes 1 位图 端点的特性。Bit 1..0 :传送类型 00=控制传送 01=同步传送 10=批传送 11=中断传送
4 wMaxPacketSize 2 数字 当前配置下此端点能够接收或发送的最大数据包的大小。对于中断传输,批量传输和控制传输,端点可能发送比之短的数据包。
6 bInterval 1 数字 主机轮询该端点的间隔,对于批量传送和控制传送的端点忽略;对于同步传送的端点,必须为1;对于中断传输,低速模式此处为10 \~ 255(ms),全速模式为1 \~ 255(ms) 。

字符串描述符

字符串描述符是可选的.如果不支持字符串描述符,其设备、配置、接口描述符内的所有字符串描述符索引都必须为0,语言字符串描述符的索引为0。

偏移量 大小 描述
0 bLength 1 数字 此描述表的字节数(bString域的数值N+2)
1 bDescriptorType 1 常量 字串描述表类型(此处应为0x03)
2 bString N 数字 UNICODE 编码的字串

USB枚举

枚举就是主机从设备端读取一些信息,知道设备是什么样的设备,如何进行通信,这样主机就可以根据这些信息加载合适的驱动程序。调试USB设备,很重的一点就是查看USB的枚举,只要枚举成功了,那么就已经成功了大半。下面结合USB枚举序列图(下图(a))和Telink 鼠标类USB枚举实例(下图(b)),详细介绍USB的枚举流程。

USB枚举序列

下图(a)中给出了USB枚举序列图,从图中我们可以看出,USB枚举过程分为8步完成,其中Step 1 ~ 7为标准USB的枚举过程,Step 8 USB设备类专有的枚举流程。

(a)USB枚举序列图

  • Step 1 主机检测到有设备接入后:首先,主机根据差分信号线上的电平状态,来判断该设备是低速设备,还是全速设备(高速设备在上电初始时,默认为全速设备);然后主机等待设备电源稳定(>=100ms)后,给设备发送复位信号(D+和D-全为低电平,持续>=10ms);最后,如果为高速设备,且主机(Hub)支持高速模式时,主机与设备进行高速检测和握手后,设备可切入高速模式,否则仍维持全速模式。

  • Step 2 进行完Step 1后,主机会使用端点0(默认端点,控制传输),发送GetDescriptor请求(设备地址为0),设备接收到请求后,会将自己的设备描述符发给主机,主机会根据此设备描述符(bMaxPacketSize0字段)来进行下一步动作。需要注意的是:(1)只有设备在Step 1中接收到复位信号才去响应主机;(2)已经完成枚举的设备不响应该请求;(3)描述符的长度至少为8个字节(bMaxPacketSize0字段在第8个字节);(4)如果设备超时未响应或响应错误,主机会重新开始,并尝试三次,三次仍无法获取正确的响应,主机会认为该设备为无法识别的设备(下同)。

  • Step 3 主机在正确完成Step 2,获取端点0的最大数据包长度后,重新复位设备,之后将按照该长度来对数据包进行拆包和组包。

  • Step 4 主机给设备分配一个非0的地址,该地址与集线器上其他设备不同,用于保证定向通信的稳定性。主机完成地址设备后,主机和设备的通信将一直按照新的地址进行通信,直至设备复位或移除。

  • Step 5 主机按照Step 4中设备的新地址,依次获取设备的标准描述符(设备描述符、配置描述符、接口描述符、端点描述符和字符串描述符)。注意:(1)设备描述符的长度,主机在Step 2中就已经获得,因此主机指定长度(最大长度为设备描述符长度)获取设备描述符,其他描述符则按照最大长度255来获取,设备端只需要按照实际长度发送即可;(2)设备的接口描述符和端点描述符等,可能包含在配置描述符中,主机会根据配置描述符中的wTotalLength字段,来获取该配置的所有数据;(3)如果设备有多个配置,主机会分为多次来索要配置描述符;(4)主机会根据设备、配置、接口和端点等描述符中所包含的字符串描述符个数,按照其索引值,依次索要字符串描述符,索引值为0的是特殊字符串描述符(语音信息描述符)。

  • Step 6 主机在进行完Step 5后,获取配置描述符的实际长度,依次获取配置描述符信息和配置描述符中所包含的其他信息(如接口描述符和端点描述符等)。

  • Step 7 Step 1 \~ 6为标准USB枚举过程,只有当Step 1 \~ 6全部正确后,主机发出SetConfiguration的指令,来激活并使用设备的一个配置,此时设备才是真正意义上的可用状态。主机在设备配置完成后,会根据设备的标准描述符来将设备分成一个或多个虚拟设备。

  • Step 8 Step 7完成后,主机上产生一个或多个虚拟设备,每个虚拟设备都有其类别标识,主机会根据其VID、PID和序列号查询对应的驱动,并安装驱动(如果主机上有备份,则直接使用,不再安装)。然后,主机根据驱动加载对应类专属描述信息。此处给出标准HID设备,主机自带驱动。

USB枚举实例

下图(b)是Telink Dongle做鼠标设备时的枚举流程,图中的Step x与上图(a)中的Step x一一对应,Step 8为HID设备类专有的枚举流程,即获取报告描述符,有关报告描述符的结构可参照USB HID协议Universal Serial Bus (USB)-Device Class Definition for Human Interface Devices (HID)。

(b)Telink Dongle做鼠标设备时的枚举流程

USB硬件介绍

Telink USB硬件模块内部固化了原始包和事务的处理,自动完成IN端点数据的保存和OUT端点数据的发送,将端点0的数据打包成标准用户数据包,这样不仅可以提高USB执行效率,而且最大程度上降低了USB开发的难度。

USB端点

端点配置

Telink USB支持USB1.1协议,采用外部供电模式, DP管脚有一个1.5k上拉电阻(用户选配),用户可以通过设置模拟寄存器,来使能(置1)或关闭(置0)USB功能,默认为关闭状态。Telink USB共有9个端点,即端点0和端点1 \~ 8,端点1、2、3、4、7和8可以配置为输入端点,而端点5和6配置为输出端点。端点0只能使用控制传输,端点1 \~ 8支持除控制传输以外的其他三种传输模式,在音频应用中,端点6和端点7默认为同步传输端点(用户可通过清除数字寄存器0x38的bit6和bit7来关闭端点6和7的同步传输功能)。端点6支持同步输出,端点7支持同步输入。

可配置端点类型 端点号
控制端点(既可输入又可输出) 0
输出端点 5、6
输入端点 1、2、3、4、7和8

端点内存分配

Telink USB使用8+256字节的USB专属RAM来缓存各个端点的数据,端点0的RAM地址已固定且大小为8字节,其余端点共用256字节。用户可根据需要配置地址寄存器,来设置每个端点数据的起始位置,端点的缓存大小为下一个端点的起始地址减去本端点的起始地址,根据下表(USB端点寄存默认分配表)的默认配置,计算得到USB端点资源分配图。硬件会根据用户的配置,将接收到的数据存入相应的buffer,或者从相应缓冲buffer中取数据,并发送给主机。需要注意的是,用户在配置地址寄存器的时候,一定要计算好每个端点的地址,否则可能引发数据覆盖的问题。

端点起始地址 含义
0x00 Endpoint 1 起始地址
0x08 Endpoint 2 起始地址
0x10 Endpoint 3 起始地址
0x20 Endpoint 6 起始地址
0x30 Endpoint 7 起始地址
0x40 Endpoint 4 起始地址
0x80 Endpoint 8 起始地址
0xc0 Endpoint 5 起始地址

USB端点资源分配图

注意:

  • 端点缓存大小由下列两个条件决定: a) 每个端点的起始位置,端点的缓存大小为下一个端点的起始地址减去本端点的起始地址,例如端点1的起始地址为0x00,端点2的起始地址为0x08,则端点1的缓存大有为0x08个bytes。默认的端点地址不是按照端点1-8的顺序排列的,而是按照端点的起始地址排序。 b) 端点缓存最大值由max寄存器决定(端点7除外,可分配到所有缓存空间),默认为64bytes,所以每个端点的缓存大小,最大不应超过64Bytes。
  • 端点起始地址,只有在枚举设备中使用了,才对端点的缓存分配起作用。例如,usb audio 只使用了端点6和7,其他默认起始地址,不起作用。

中断

USB中断可分为三类,端点0中断、端点1-8中断和suspend/250us/reset 中断,如下表

中断 产生条件 自动清除还是手动清除
CTRL_EP_SETUP(IRQ7) 端点0控制传输setup阶段 手动清status
CTRL_EP_DATA (IRQ8) 端点0控制传输data阶段 手动清status
CTRL_EP_STATUS (IRQ9) 端点0控制传输状态阶段 手动清status
Endpoint(1-8) interrupts(IRQ11)
FLD_USB_EDP8_IRQ (in)
FLD_USB_EDP1_IRQ (in)
FLD_USB_EDP2_IRQ (in)
FLD_USB_EDP3_IRQ (in)
FLD_USB_EDP4_IRQ (in)
FLD_USB_EDP5_IRQ (out)
FLD_USB_EDP6_IRQ (out)
FLD_USB_EDP7_IRQ (in)
1. 除同步端点:
输出端点:host out事务,状态寄存器的相应位置1,产生中断,接收完回ACK
输入端点:数据填充完成后,配置ACK通知硬件,并产生中断,硬件在收到host in事务中,将数据发送给host
2. 同步端点:端点6,7可设置为同步端点,定时1ms产生中断。
手动清status
USB_IRQ_USB_SUSPEND (IRQ24) USB总线空闲,例如拔掉USB接口,host休眠 手动清status
USB_IRQ_250us (IRQ34) 250us定时中断 手动清status
USB_IRQ_RESET (IRQ35) Host发送reset时序 手动清status

注意:

  • Driver枚举过程的传输都是采用轮询的方式处理,没有使用中断方式。

自动和手动模式

Telink USB有两种模式,即自动模式和手动模式:

用户可通过设置端点0的配置寄存器,来控制是选择自动模式还是手动模式。端点0的配置寄存器默认为0xFF,即自动模式,此时与USB端点0相关的所有编解码均由Telink硬件自动驱动完成,Telink自带驱动为Print设备,使用端点8作为控制接口的端点,打印机数据是从端点0发送的;

手动模式需要用户修改EDP0CFG寄存器(下图),一般是将bit[7]和bit[5]设为0,即由用户去完成标准USB的枚举和使用用户定义的描述符。

端点0配置寄存器

USB软件基础

USB运行流程

Telink USB的软件运行流程可以分为两个阶段,即初始化阶段和循环检测阶段,如下图所示。

Telink USB运行流程图

初始化阶段主要完成USB的相关配置和使能USB。USB配置选项主要包括模式切换(自动模式和手动模式)、设置USB数据缓冲区和设置其他配置项;模式切换主要是将USB的工作模式切换为手动模式,此时相关的枚举过程和描述符由用户控制,设备会将用户准备好的枚举信息上报给主机;设置USB缓冲区是用户根据自己端点(端点0除外)的使用情况,分别给相关端点指定一段缓冲区(缓冲区总大小256Bytes,没有使用到的端点不用指定);设置其他配置是用来进行其他配置的操作,用户如果不想使用系统默认的配置项,可选择自行配置,如以中断形式传输数据等。

循环检测主要是不断检测数据接收和发送缓冲区是否有数据,如果有数据则进行相关操作,程序运行过程中会反复执行usb_handle_irq。端点0的操作流程可分为三个阶段,如下图所示,分别对应控制传输的SETUP阶段、DATA阶段和STATUS阶段,主要完成USB的识别和配置等相关操作,如USB的枚举,该过程是在main loop中完成,SETUP解析主机下发的指令,并根据主机的指令, 来准备相应的数据;DATA是将在数据阶段准备的数据发给主机或者接收主机发送的数据;STATUS是双方握手的过程。

端点0数据操作流程

数据接收与发送

数据接收

Telink USB数据接收由硬件完成,硬件会将接收到的数据保存到RAM中,接收完成后硬件会产生一个中断来通知用户,用户只需要在检测到中断后,读取数据即可。数据检测和接收工作应该在usb.c中的usb_handle_irq函数中进行,结合下图,具体分析Telink USB数据接收的处理流程:

(1) 用户需要检测相关的中断标识位(reg_usb_irq)是否置1,如果置1则进入数据接收阶段。

(2) 一旦检测到数据后,用户需要将中断标识位清除,即reg_usb_irq = BIT((USB_EDP_CUSTUM_OUT & 0x07))。

(3) 用户读取数据之前,需要使用reg_usb_ep_ptr(USB_EDP_CUSTUM_OUT)来获取接收到的数据长度。

(4) 用户获取完数据长度后,即可通过反复读取usbhw_read_ep_data(USB_EDP_CUSTUM_OUT)来获取本次接收到的所有数据。

(5) 用户接收完数据后,需要将调用usbhw_data_ep_ack(USB_EDP_CUSTUM_OUT)。(注意,这一步很重要,只有将OUT端点的ACK置起,硬件才会去接收主机下发到该端点的数据,并在接收完成后产生中断。)

Telink USB数据接收

数据发送

Telink USB数据发送和数据接收一样,是由硬件完成,用户只需要将数据填充到相应的USB RAM中,并将数据ACK位置1。用户在填充数据之前,需要首先检测USB RAM中是否有待发送数据,如果有待发送数据,则需要等待发送完成之后再填充新的数据,否则会发生数据覆盖现象。

下图给出了Telink SDK中,发送数据的实例,下面结合该实例,详细分析USB数据发送流程:

(1) 用户发送数据前,需要检测操作端点是否繁忙,如果繁忙(有待发送数据),则需要等待发送完成后,再填充数据。

(2) 如果端点处于空闲状态,需要先重置端点计数器,即reg_usb_ep_ptr(USB_EDP_CUSTUM_CMISC_IN) = 0。

(3) 重置完端点计数器后,用户就可向端点中填充数据。需要注意的时,reg_usb_ep_dat(USB_EDP_CUSTUM _CMISC_IN) = data[i]是将数据放到USB RAM中(硬件操作)。

(4) 用户填充完数据后,需要调用reg_usb_ep_ctrl(USB_EDP_CUSTUM_CMISC_IN) = FLD_EP_DAT_ACK来通知硬件数据已经准备好了,硬件在收到该指令后,会在下一个主机索取数据的时候,将数据发送给主机。

Telink USB数据发送

USB demo

USB应用主要介绍了USB标准设备类中的HID(Human Interface Device)设备,Audio设备,CDC(Communication Device Class)设备的简单应用,客户可根据需求自由组合。

HID类设备是USB设备中常用的设备类型,是直接与人交互的USB设备,如USB mouse,USB keyboard;

USB Audio类设备最为常见的是microphone和speaker;

USB的CDC类是USB通信设备类简称,虚拟串口设备是CDC类设备的一种类型。

在头USB_DEMO/app_config.h中可以选择配置成不同的设备。

#define     USB_MOUSE           1
#define     USB_KEYBOARD        2
#define     USB_MICROPHONE      3
#define     USB_SPEAKER         4
#define     USB_CDC             5
#define     USB_MIC_SPEAKER     6

#define     USB_DEMO_TYPE       USB_MOUSE

USB mouse

Mouse处理流程:

USB HID设备是通过report来传输数据,一个报告描述符可以描述多个报告,不同的报告通过ID来识别,报告ID为报告的第一字节,没有规定报告ID时,报告没有ID字段,开始就是数据,详细的报告描述符资料可参考USB HID协议以及HID用途表(HID Usage Tables)。

首先host将Telink USB 识别成mouse设备,需要经历枚举阶段,设备枚举成功后,进入数据收发阶段。根据mouse报告描述符的内容,报告ID为USB_HID_MOUSE的描述符中,有4个字节。第1字节字的低5位表示按键是否按下,高3位为常数无用;第2字节为X轴的改变量;第3个字节为Y轴的改变量;第4个字节为滚轮的改变量。通过函数usbmouse_hid_report(USB_HID_MOUSE,mouse,4)返回报告。

Demo程序中,定义了数组unsigned char mouse[4],其中:mouse[0]:BIT(0) - left key; BIT(1) - right key; BIT(2) - middle key; BIT(3) - side key; BIT(4) - external key. 对应的bit置1,代表对应鼠标键按下;mouse[1]:相对于x坐标的改变量;mouse[2]: 相对于y坐标的改变量;mouse[3]: 滚轮的改变量。

Mouse测试:

按下测试

Demo程序中,报告ID:USB_HID_MOUSE=1,

数组mouse赋值:mouse[0]=BIT(1),mouse[1]= -2(补码),mouse[2]=2, mouse[3]=2。

将开发板上的引脚PD1接地后再拔出,会执行函数usbmouse_hid_report(USB_HID_MOUSE,mouse,4)。

可以观测到桌面的鼠标右键按下,鼠标光标向左下移动,如下图所示,也可以从USB抓包工具Input Report[1]中看到:x:-2,Y:2,wheel:0,Btns=[2]。

Mouse Input Report抓包图

释放测试

对引脚PD2进行和PD1一样的操作,mouse数组清零,按键释放。

USB keyboard

Keyboard处理流程:

根据keyboard报告描述符的内容,有输入和输出报告,其中输入报告规定了8个字节,第1个字节的8位表示特殊键是否按下。

BYTE0:BIT(0) – 左Ctl;BIT(1) – 左Shift;BIT(2)-左Alt;BIT(3) - 左GUI

BIT(4) – 右Ctl;BIT(5) – 右Shift;BIT(6) - 右Alt;BIT(7) - 右GUI

第2字节为保留值,都为0

BYTE1:0

第3到8字节是一个普通键键值,当没有键按下时,全部6字节都为0,这6个字节的第一字节值即为按键的键值,当有多个按键同时按下时,则同时返回这些按键值,键值在数组的先后顺序无关。具体键值请参考HID用途表文档,例如:0x59对应 数值键盘 1;0x5a 对应 数值键盘 2;0x5b 对应 数值键盘 3;0x39 对应 大小写切换键。

Keyboard测试:

按下测试

Demo程序中,定义数组kb_data[6],赋值为kb_data[0] = 0;kb_data[1] = 0;kb_data[2] = 0x59;kb_data[3] = 0x5a; kb_data[4] = 0x39;kb_data[5] = 0;

将开发板上的引脚PC1接地后再拔出,会执行函数usbkb_hid_report_normal(0x10,kb_data),其中参数1对应第一字节,参数2是对应3 ~ 8字节的数组。

可以观测到在编辑窗口输入界面,会输入数字1和2,右Ctrl键和大小写键按下。

如下图,也可以从USB抓包工具Input Report中看到:Keys=[Rctrl 1 2 CapsLk]

Keyboard Input Report抓包图

释放测试

对GPIO_PC2进行和GPIO_PC1一样的操作,特殊键和kb_data数组清零,按键释放。

USB MIC

MIC处理流程:

AMIC为例,USB microphone设备是将device的AMIC数据通过USB传输到host,需要保证整条数据通道采样率和通道数匹配。Demo程序中,主要是将数据上传到usb部分不同, Mic端点中断是1ms定时中断,也即1ms进一次中断,根据不同采样率,1ms产生数据量不同,例如16K采样率收音,单声道数据,1个sample为2 bytes,1ms 数据为32bytes,将对应audio buff的填入usb sram中。

Mic demo测试:

Audio相关的测试,借助Audacity软件,如下图麦克风选择Telink Audio16,扬声器选择PC扬声器。

Device Mic收音,PC扬声器播放,如录音的人声能不失真通过扬声器播放出来,说明Mic工作正常。

Audacity软件设置(Mic)

USB speaker

Speaker处理流程:

USB speaker设备是将host的音频数据通过USB传输到device ,这个处理过程也在1ms中断完成的,读出usb sram数据长度,将对应的数据填充到 audio buff。

Speaker demo测试:

在Audacity软件中,麦克风选择PC麦克风,扬声器选择Telink Audio16。通过输出音频接口(3.5mm耳机插口)。如录音的人声能不失真的通过耳机播放出来,说明speaker工作正常。

Audacity软件设置(Spk)

USB CDC

CDC设备有两个接口,CDC控制接口和CDC数据接口,控制接口分配端点2,作为中断输入端点传输。数据接口分配端点5(out),端点4(in),一定要先将端点5的ACK置起,端点才可以从USB主机接收数据。

CDC处理流程:

CDC设备USB安装host首次识别成CDC设备需要手动安装.inf文件,如下图在8278_USB_Demo路径下。

.inf文件路径

数据接收(host to device)

Demo程序中,在函数void usb_cdc_irq_data_process(void)中host发送数据给device,端点5产生中断后,函数usb_cdc_rx_data_from_host(usb_cdc_data)进行数据接收。

数据发送 (device to host)

如下图所示,在main_loop里当判断接收buff数据长度不为0时,通过函数usb_cdc_rx_data_from_host(usb_cdc_data)将接收的数据发送给host。

数据发送

CDC demo测试:

测试现象如下图所示,将串口助手发送的数据返回。

CDC数据收发测试

CPU性能测试

衡量处理器的一个重要指标是功耗,另外一个重要指标便是性能。在处理器领域的 Benchmarks 非常众多, 在嵌入式处理器领域最为知名和常见的 Benchmarks 为Dhrystone 和 CoreMark。

Dhrystone

Dhrystone标准的测试方法很简单,就是单位时间内跑了多少次Dhrystone程序,其指标单位为DMIPS/MHz。MIPS是Million Instructions Per Second的缩写,每秒处理的百万级的机器语言指令数。DMIPS中的D是Dhrystone的缩写,它表示了在Dhrystone标准的测试方法下的MIPS,主要用于测整数计算能力。

CoreMark

CoreMark程序使用C语言写成,包含如下四类运算法则:数学矩阵操作(普通矩阵运算)、列举(寻找并排序)、状态机(用来确定输入流中是否包含有效数字)、CRC(循环冗余校验)。与Dhrystone类似,CoreMark标准的测试方法:在某配置参数组合下单位时间内跑了多少次CoreMark程序,其指标单位为CoreMark/MHz。CoreMark数字越高,意味着性能更高。

测试

Dhrystone和CoreMark测试对应的Demo分别为Dhrystone_Demo和CoreMark_Demo。测试的信息由USB 输出。

Benchmarks 测试结果 备注
Dhrystone 2.7 (DMIPS) attach1
CoreMark 3.38 attach2

注意:

  • 为提高测试性能,上述两个demo对应的link文件为flash_boot_ramcode.link。

Audio

Audio简介

声音基础

声音(sound)是由物体振动产生的声波,是一种机械波。音频录制(record)过程是模数转换过程,播放(playback)过程相反。

模数转换过程

  • 采样:对模拟信号隔一定的时间间隔取一个点。

  • 量化:给纵坐标加刻度,根据近似取整数值,使采样得到的点的值都是整数。

  • 编码:对量化取得的整数值按二进制进行编码。

  • 数字信号:把编码得到的0和1的序列变为高低电平的信号。

上述整个模数转换的过程称为:脉冲编码调制(Pulse Code Modulation),简称PCM。由上面的模数转换可知,PCM 格式文件存储的内容实际上就是编码得到的序列。

采样音频基本概念

采样频率: 所谓采样就是在时间轴上对模拟信号进行数字化,根据奈奎斯特定理(采样定理),按照比声音最高频率2倍以上的频率进行采样(AD转换)。频率在20 Hz ~ 20 kHz之间的声音是可以被人耳识别的。所以采样频率一般为40kHz左右,常用的音乐为44.1kHz(44100次/s采样)、48kHz等,电话的采样率为8K。

采样位数: 每个采样点能够表示的数据范围。采样位数通常有8 bits或16 bits两种,采样位数越大,所能记录声音的变化度就越细腻,相应的数据量就越大。16 bit是最常见的采样精度。

声道数: 声道数是指支持能不同发声的音响的个数,常用的声道数有单声道,立体声(左声道和右声道)。

例如,cd音质的相关参数为,采样位宽16bit,采样率44100,声道数2,用来衡量音频数据单位时间内的容量大小,cd音质的数据比特率则为:44100 * 16 * 2 = 1411.2 kbps。

PCM音频数据: PCM是采样量化后的未压缩音频数据,由模拟信号经过采样、量化、编码转换成的标准的数字音频数据。如果是单声道的音频文件,采样数据按时间的先后顺序依次存入(如果是双声道的话就按照LRLR的方式存储,存储的时候还和机器的大小端有关)。大端模式如下图所示。

PCM数据格式

I2S协议

I2S(Inter-IC Sound)总线, 又称集成电路内置音频总线,是飞利浦半导体公司(现为恩智浦半导体公司)针对数字音频设备之间的音频数据传输而制定的一种总线标准,该总线专门用于音频设备之间的数据传输。

I2S信号线:

(1) 串行时钟BCLK

串行时钟BCLK,对应数字音频的每一位数据,BCLK都有一个脉冲。

(2) 帧时钟LRCK

帧时钟LRCK,用于切换左右声道的数据。LRCLK(Left/Right CLOCK),LRCK的频率 = 采样频率。

(3) 串行数据(SDATA)

就是用二进制补码表示的音频数据,(MSB ---> LSB:数据由高位到低位依次传输)。

I2S数据格式:

根据data相对于LRCLK与BCLK位置的不同,分为I2S标准格式(I2S),左对齐(LJ)和右对齐(RJ),发送和接收端必须使用相同的数据格式。

注意:

  • 驱动默认的是I2S格式。

(1) I2S format

数据的最高位总在LRCLK变化(也就是一帧开始)后的第2个BCLK脉冲处,时序图如下图所示。

I2S format

(2) LJ format

在LRCLK发生翻转的同时开始传输数据。注意此时LRCLK为1时,传输的是左声道数据,这刚好与I2S Philips标准相反。左对齐(MSB)标准时序图如下图所示。

LJ format

(3) RJ format

声音数据LSB传输完成的同时,LRCLK完成第二次翻转(刚好是LSB和LRCLK是右对齐的,所以称为右对齐标准)。注意此时LRCLK为1时,传输的是左声道数据。

RJ format

(4) DSP/PCM mode

DSP/PCM mode分为Mode-A和Mode-B共2种模式。不同芯片的datasheet中有的称为PCM mode有的称为DSP mode。I2S左右声道分别为高低电平,PCM只有一个起始信号,左声道数据紧跟右声道。如下图A,Mode-A数据在第1个BCLK脉冲处。如下图B,Mode-B数据在第2个BCLK脉冲处。

DSP format (Mode-A)

DSP format (Mode-B)

Audio框架说明

CODEC介绍

如下图所示,音频编解码器(CODEC)的模数转换器(ADC),CODEC将AMIC、Line-in输入的模拟信号进行A/D转换,把模拟的信号转变CPU能够处理的数字信号;数模转换器(DAC),将PCM等信号进行D/A转换,把数字的音频信号转换为模拟信号。

CODEC框架

Audio框架

如下图所示,音频模块包括3个部分:数据输入输出接口(不同SoC可能会有差异,在Input_Path有I2S RX接口、音频编解码器(CODEC)ADC接口,在Output_Path有I2S TX接口、CODEC DAC接口);FIFO和DMA组成数据交互接口(蓝色框图);储存PCM数据的BUFF(橙色框图)。

  • Input_Path:CODEC将A/D转换后的数字信号传入BUFF或者通过I2S RX接口直接将数据直接传入BUFF。

  • Output_Path:将BUFF中的PCM等信号进行D/A转换,把数字的音频信号转换为模拟信号或者通过I2S TX直接输出。

Audio框架图

Audio I2S时钟

如下图所示,当SoC作为master为适应不同的采样率(LRCLK),支持8K/16K/32K/48K/44.1K,需要设置不同的分频系数计算出相应的采样率,其中Audio模块时钟源的MCLK时钟和I2S_CLK都直接来自于PLL=192M,I2S_CLK再分出BCLK和LRCLK。

SoC audio clock source

其中,MCLK=PLL/diver1,LRCLK=PLL/divder2/divider3/divider4。

Audio驱动说明

DMA传输

Audio数据传输是通过DMA(Direct Memory Access)传输,介绍下DMA和Audio BUFF的工作机制。

DMA传输:

DMA传输是将外设的数据不经过CPU直接送入内存储器,或者,从内存储器不经过CPU直接送往外部设备。传输动作本身是由DMA控制器来实行和完成,传输过程中不需要CPU的参与,如下图所示,Audio FIFO与BUFF数据交互是通过DMA传输,录音(Rx)DMA的源地址为Rx_FIFO的首地址,深度为8 word,目的地址为Rx_BUFF的首地址,深度可配。放音(Tx)DMA的目的地址为Tx_FIFO的首地址,深度为8 word,源地址为Tx_Buff的首地址,深度可配。如果Rx_BUFF与Tx_BUFF公用一块BUFF,那Rx的目的地址和Tx的源地址一致。

DMA与Audio FIFO交互图

DMA链传输:

Audio数据流是不间断产生,DMA传输一次完成,再进行下一次传输需要配置相关control寄存器和dma length才能再次trigger dma传输。DMA链表可以解决这一问题,使用链表的方法在不需要CPU参与下,完成连续传输的功能。DMA链表包括以下内容:DMA control、src_addr、dst_addr、DMA length和LLP_ptr(下一个链表的地址),DMA完成第一传输后,设置好链表头(Head_of_list),dma会按照链表配置的内容进行传输,再链接到下一个链表进行传输,循环往复。

(1) DMA ping-pong buff链传输

如下图所示,以Tx为例,ping-pong buff的链式传输,先建立头结点Head_of_list,然后向循环链表添加两个链表,分别是Tx_dma_list[0]和Tx_dma_list[1],Tx_dma_list[0]对应的源地址为Tx_BUFF[1],Tx_dma_list[1]对应的Tx_BUFF[0]。按其流程先搬运Tx_BUFF[0]的数据,然后根据链表头LLP指向Tx_dma_list[0],搬运Tx_BUFF[1]的数据,Tx_dma_list[0]中LLP指向Tx_dma_list[1]搬运Tx_BUFF[0]的数据,其LLP又指向了Tx_dma_list[0],收尾相接形成一个ping-pong buff。

ping-pong BUFF链式

(2) DMA单个buff链传输

如下图所示,以Tx为例,在ping-pong buff的基础上,使Tx_dma_list的LLP指向自己,发dma就是一直搬运单个Tx_BUFF的数据。

单BUFF链式

Audio buff工作机制

Audio使用的是环形buff,录音为Rx,放音为Tx,数据通过DMA传输,Rx的DMA的源地址为audio外设FIFO地址,目的地址为RX_BUFF首地址,Tx的DMA的源地址为Tx_BUFF,目的地址为audio外设FIFO地址。

Rx Path:

如下图所示,设定BUFF_size长度的Rx_BUFF , 录音数据由DMA搬入Rx_BUFF,获取由硬件维护的rx_wptr(不同芯片获取的方式有差异)。根据软件从Rx_BUFF获取数据操作,记录rx_rptr,由软件维护。红色线条部分为可读数据长度read_len为:

  • rx_wptr > rx_rptr, read_len = rx_wptr - rx_rptr;

  • rx_wptr < rx_rptr, read_len = buff_size - (rx_rptr - rx_wptr)。

Rx_BUFF示意图

Tx Path:

如下图所示,设定BUFF_size长度的Tx_BUFF,放音是将BUFF数据由DMA搬出BUFF,获取由硬件维护的tx_rptr(不同芯片获取的方式有差异)。根据软件往Tx_BUFF填数据操作,记录tx_wptr,由软件维护。红色线条部分为可写数据长度write_len:

  • tx_rptr > tx_wptr, write_len = tx_rptr - tx_wptr;

  • tx_rptr < tx_wptr, write_len = buff_size - (tx_wptr - tx_rptr)。

Tx_BUFF示意图

Audio_Demo

在头Audio Demo里配置app_config.h中的宏AUDIO_MODE来选择不同的AUDIO模式。

Demo 功能
LINEIN_TO_LINEOUT 模拟音频在输入插孔输入,在输出插孔接扬声器或耳机可以实时听到经过CODEC经过处理的音频
AMIC_TO_LINEOUT AMIC录音,在输出插孔接扬声器或耳机可以实时听到经过CODEC处理的音频
DMIC_TO_LINEOUT DMIC录音,在输出插孔接扬声器或耳机可以听到实时经过CODEC处理的音频
BUFFER_TO_LINEOUT 将BUFF里的PCM数据经过CODEC处理输出,在输出插孔接扬声器或耳机可以听到BUFF里的音频(一般为1K正弦波)
FLASH_TO_LINEOUT 将FALSH里的PCM数据,读取出来按一定方式填入AUDIO BUFF ,再经过CODEC处理输出,在输出插孔接扬声器或耳机可以听到FLASH里的音频
EXT_CODEC_LINEIN_LINEOUT SoC的I2S接口跟外部CODEC(WM8731为例)进行数据交互,外部CODEC LINE_IN to LINE_OUT

芯片差异

Input Path与Output Path差异

(1) B91 Audio Input Path

参考Audio框架图中的Input_Path,如下图所示,audio输入有2种方式:经内部CODEC(没有特殊说明,CODEC都指内部CODEC)处理后的I2S信号(AMIC/DMIC/LINE-IN);从外部CODEC输入的I2S信号。

I2S输入:

通过Mux选择将I2S接口的IO连接到内部CODEC或者外部CODEC,I2S支持I2S、LJ、RJ、DSP格式;位宽支持16bit、20bit、24bit和32bit的数据格式,传输数据时I2S_Rx会将接收的串行I2S数据转为并行数据写进Rx_BUFF。

Audio input path

(2) B91 Audio output Path

参考Audio框架图中的Output_Path,如下图所示,Audio输出有2种方式:Tx_BUFF里的音频数据,通过I2S接口输出到内部CODEC或者外部CODEC。

I2S输出:

Tx_BUFF里的音频数据经过I2S_Tx转成的串行I2S格式的数据,输入给内部的CODEC或外部CODEC。

Audio output path

Audio Demo差异

(1) LINEIN_TO_LINEOUT

LINEIN_TO_LINEOUT

注意:

  • ADC输入支持单端和差分,默认为差分模式,DAC只支持差分输出。
  • MONO模式下,可以选择单左声道输出或者左右声道同时输出,此时两声道的数据是一样的,默认是后者。
  • STEREO模式下,左路输入对应左路输出,右路输入对应右路输出。

(2) AMIC_TO_LINEOUT

AMIC_TO_LINEOUT

注意:

  • ADC输入支持单端和差分,默认为差分模式,DAC只支持差分输出。
  • MONO模式下,输入通道:默认是左声道的AMIC输入,可以调用audio_set_mono_chn接口设置为右声道AMIC输入。输出通道:可以选择单左声道输出或者左右声道同时输出,此时两声道的数据是一样的,默认是后者。
  • STEREO模式下,左路输入对应左路输出,右路输入对应右路输出。

(3) DMIC_TO_LINEOUT

DMIC_TO_LINEOUT

注意:

  • MONO模式下,单DMIC只需要2根信号线data和clk, clk频率固定为3M。
  • STEREO模式下,左路输入对应左路输出,右路输入对应右路输出,双DMIC有1根信号线data和2根clk,双DMIC共用data,2路clk时序是一样的,在clk上沿采集一DMIC数据,在clk下沿采集另一个DMIC数据。

(4) BUFFER_TO_LINEOUT

Demo里提供了频率为1K采样率为44.1K,单声道音频数据和频率为1K采样率为48K单声道音频数据。

注意:

  • 这里的BUFF的首地址就是DMA的源地址,将BUFFER作为audio_buff。
  • 无论在MONO还是在STEREO模式下,输出双路道输出会有一sample的相位差,调用audio_invert_i2s_lr_clk使i2s clk的取反,可以消除相位差。

(5) EXT_CODEC_LINEIN_LINEOUT

该demo的功能实现:SoC的I2S接口跟外部CODEC(WM8731为例)进行数据交互,外部CODEC LINE_IN to LINE_OUT。

EXT_CODEC_LINEIN_LINEOUT

注意:

  • 利用PWM0提供MCLK(12M)。
  • 默认采样率为32K,MONO,BIT_16。

(6) FLASH_TO_LINEOUT

该demo的功能实现:将FALSH里的PCM数据,读取出来按照定长填入AUDIO_BUFF,再通过LINE_OUT输出,参考AUDIO BUFF工作机制, BUFF的长度为4K(AUIDO_BUFF_SIZE),Flash 数据量很大,需要分批填入BUFF中。

B91 audio模块没有效的中断去填充和获取音频数据:

  • 录音数据由DMA以word单位搬入Rx_BUFF,DMA的目的地址每次自加4,自加到BUFF_size-4后归0,目的地址相对于Rx_BUFF首地址的偏移量记为rx_wptr。

  • 放音是将BUFF数据由DMA以word单位搬出BUFF,DMA源地址每次自加4,自加到BUFF_size-4后归0,源地址相对于Tx_BUFF首地址的偏移量记为tx_rptr。

先介绍函数接口:

u32 audio_get_tx_dma_rptr (dma_chn_e chn) //参数chn 是配置为tx dma通道,获取对应通道dma源地址

初始化DMA的源地址配置为audio_buff的首地址,audio_buff的大小为4K, 放音是将buff数据由dma以word单位搬出buff,每搬一次,dma的源地址会加4,自加到buff_szie-4后归0,将dma的源地址减去audio_buff的首地址,可以表征硬件在audio_buff读的状态,记为读指针tx_rptr:

tx_rptr= ((audio_get_tx_dma_rptr (DMA3)-(u32)auido_buff));

而audio_buff_buff的写状态,由软件维护,记为tx_wptr,参考AUDIO BUFF工作机制中Tx Path的计算公式,audio_buff的剩余可写空间:

if((tx_wptr&(AUIDO_BUFF_SIZE - 1))>tx_rptr)
    {
        unused_buff=AUIDO_BUFF_SIZE-(tx_wptr&(AUIDO_BUFF_SIZE-1))+tx_rptr;
    }
    else
    {
        unused_buff=tx_rptr-tx_wptr;
    }

例如按定长数据(AUIDO_THD_SIZE)填入BUFF,其在while(1)的流程如下。

流程

注意:

  • ADC输入支持单端和差分,默认为差分模式,DAC只支持差分输出。
  • MONO模式下,可以选择单左声道输出或者左右声道同时输出,此时两声道的数据是一样的,默认是后者。
  • STEREO模式下,左路输入对应左路输出,右路输入对应右路输出。