TMS320C55xx——AIC23B的进阶使用_中断与DMA+中断

芯片:TMS320C5509A

代码工程项目首页 - TMS320C5509A - GitCode

代码文件BSP/aic23.h Drivers/zq_dma.h Drivers/zq_bsp.h Drivers/zq_i2c.h

前提条件TMS320VC5509的上手教程__CCS和CLion开发TI工程(间断更新中)

引言

        在嵌入式系统中,高效的数据传输是提升系统性能的关键。TMS320C5509A作为一款高性能数字信号处理器,其内置的DMA控制器能够在CPU不干预的情况下完成外设与存储器之间的数据传输。本文将介绍AIC23B的进阶使用,从中断到DMA逐级讲解,因此AIC23B的初始化不是本篇介绍的重点。

        AIC23B在TMS320C5509A中常被用作ADC或DAC,其通过I2C配置AIC23B的寄存器参数,通过McBSP(通常使用DSP模式,另一种模式是I2S)进行音频数据通信。

        详情见AIC23B_DMA分支,DMA使用代码在Core/core.h文件里

一、轮询

 1,I2C初始化

        I2C初始化的重点是时钟配置正确即可

  • PSC        I2C的预分频寄存器,可以通过其把CPU系统时钟分频成8~12MHz,以便于配置后续时序
  • CLKL、CLKH        I2C时序中的高低电平周期,与前面的PSC共同发挥作用确定I2C的通信频率

        晶振时钟为12MHz,系统时钟为192MHz(倍频系数为16),为获得12MHz,需要把I2C的PSC寄存器设置为(16-1)。为达到400KHz的通信速率,SCL的高低电平需要设置为12MHz/400KHz=30个周期,由于SCL的高低电平分别自带6和4个周期,因此,CLK_L和CLK_H都设置为(30-10)/2=10个周期即可

        static void init(const i2c::Config &cfg)
        {
            using namespace i2c::detail;

            //置I2C控制器复位
            MD::IRS::clear();

            // 模块时钟 = 系统时钟 / (PSC + 1)
            // SCL周期 = (CLKL + CLKH) / 模块时钟
            // 目标:SCL频率 = 400KHz

            // 步骤1:计算预分频器PSC(生成约8-12MHz模块时钟)
            const uint32_t module_clock = 12000000; // 目标模块时钟12MHz
            const uint16_t psc = cfg.system_clock / module_clock - 1;
            PSC::write(psc);

            // 步骤2:计算CLKL/CLKH(400KHz SCL)
            const uint16_t total_cycles = module_clock / cfg.bitrate-10; // 30 cycles @12MHz
            const uint16_t clk_l = total_cycles / 2;   // 低电平周期 = 10(+6)
            const uint16_t clk_h = total_cycles - clk_l; // 高电平周期 = 10(+4)
            CLKL::write(clk_l);
            CLKH::write(clk_h);

            //设置主和从地址
            OAR::write(0x7F);

            //将I2C控制器从复位中取出,置于主模式
            MD::or_mask(MD::IRS::MASK|MD::MST::MASK);
        }

        此外,I2C的发送和读取也很重要,此处以发送为例。在AIC23B中,I2C常用来配置寄存器参数的,不涉及大量数据传输,因此可以定义如下函数专门配置寄存器参数。

        static void send(const uint16_t device, const uint16_t reg_addr, const uint16_t data)
        {
            using namespace i2c::detail;
            // 可以使用寄存器位域里的MASK,比如MD::STP::MASK
            // 1. 配置数据计数 (2字节: 寄存器地址 + 数据)
            CNT::write(2);

            // 2. 设置从机地址 (7位模式)
            SA::write(device & 0x7F);

            // 3. 配置模式寄存器:
            MD::or_mask(
                MD::STT::MASK | //   - 产生START条件 (STT)
                MD::STP::MASK | //   - 产生STOP条件 (STP)
                MD::MST::MASK | //   - 主机模式 (MST)
                MD::TRX::MASK   //   - 发送模式 (TRX)
                // MD::IRS::MASK   //   - 启用模块 (IRS)
            );
            // 4. 发送寄存器地址 (等待发送就绪)
            DX::write(reg_addr & 0xFF);
            while (STR::XRDY::read_bit_not()) {}// 等待 XRDY (ICSTR_ICXRDY)

            // 5. 发送数据 (等待发送就绪)
            DX::write(data & 0xFF);
            while (STR::XRDY::read_bit_not()) {}// 等待 XRDY
        }

2,McBSP初始化

        确保McBSP配置为如下模式:

  • 帧参数                  16位(或者32位),单相,无延迟(有延迟会导致波形输出有问题)
  • 帧同步                   最小化极性配置,输入0即可,否则波形输出畸形
  • 采样率发生器        启用采样率发生器,确保不会产生严重的时钟漂移
            static void init(const bool receive_IT =false, const bool transmit_IT=false)
            {
                // McBSP0复位
                regs::spcr1::clear();
                regs::spcr2::clear();

                // 配置帧参数(16位,单相,无延迟)
                regs::xcr1::write(info::XCR1::XWDLEN1_16);
                regs::xcr2::write(info::XCR2::XPHASE_SINGLE | info::XCR2::XDATDLY_0);
                regs::rcr1::write(info::RCR1::RWDLEN1_16);
                regs::rcr2::write(info::RCR2::RPHASE_SINGLE | info::RCR2::RDATDLY_0);

                /// 使用帧同步会出问题
                // 禁止内部生成帧同步信号,配置为从模式使用FSX引脚的外部帧同步
                // 帧同步高电平有效,数据在CLKX时钟上升沿进行采样
                // regs::pcr::write(info::PCR::CLKXP);

                // 最小化极性配置
                regs::pcr::write(0); // 所有极性默认(上升沿有效,高电平有效)

                // 关键:启动采样率发生器!!!
                regs::spcr2::GRST::set();  // 启动采样率发生器
                systick::Delay::us(10); // 等待稳定

                // 设置接收中断模式为 "RRDY 触发中断" (RINTM = 00)
                regs::spcr1::RINTM::write_bits(0);

                //发送器摆脱复位  接收器使能
                regs::spcr1::or_mask(info::SPCR1_MASK::RRST);
                regs::spcr2::or_mask(info::SPCR2_MASK::XRST);

                if (receive_IT)
                    start_receiveIT();
                if (transmit_IT)
                    start_transmitIT();
            }

3,AIC23B初始化

         AIC23B初始化是建立在I2C初始化和McBSP初始化基础上的。在本文中,AIC23B可作为ADC和DAC使用,为保证作为ADC时读取数据噪声更少、效果更好,因此选用线输入,并且只读取单个通道(麦克风输入只有一个通道,两个寄存器里的值是相同的)

            /**
             *
             * @param receive_IT true表示立即开启接收中断
             * @param transmit_IT true表示立即开启发送中断
             */
            template<SAMPLE_RATE::Type sample_rate>
            static void init(const bool receive_IT =false, const bool transmit_IT=false)
            {
                // 初始化I2C
                zq::i2c::Config cfg;
                cfg.system_clock = 192000000;
                cfg.bitrate = 400000;
                cfg.loopback = false;
                zq::I2C::init(cfg);

                // 初始化AIC23寄存器
                write_cmd(detail::REG::RESET, 0); //复位寄存器
                write_cmd(detail::REG::POWER_DOWN_CTL, 0); //所有电源都打开
                // write_cmd(detail::REG::ANALOG_AUDIO_CTL, detail::ANAPCTL::DAC | detail::ANAPCTL::INSEL); //打开DAC 选择传声器 选择麦克风输入(MICIN)
                write_cmd(detail::REG::ANALOG_AUDIO_CTL, detail::ANAPCTL::DAC); //打开DAC 选择传声器 选择线输入
                write_cmd(detail::REG::DIGITAL_AUDIO_CTL, 0); //数字音频通道控制   禁止去加重

                // 打开线入声道音量
                write_cmd(detail::REG::LT_LINE_CTL, 0x01F); //左声道输入衰减正常,最小为-34.5dB
                write_cmd(detail::REG::RT_LINE_CTL, 0x01F); //右声道输入衰减正常,最小为-34.5dB  禁止左右声道同时更新

                // 数字音频接口主模式    输入长度16位     DSP初始化
                // 采样率控制 44.1KHz 比较常用   SRC_BOSR为272fs  USB clock
                write_cmd(detail::REG::DIGITAL_IF_FORMAT,
                          detail::DIGIF_FMT::MS | detail::DIGIF_FMT::IWL_16 | detail::DIGIF_FMT::FOR_DSP);

                // 理论上0x01是48KHz,0x1D是96KHz,0x23是44.1KHz
                // write_cmd(detail::REG::SAMPLE_RATE_CTL, 0x1D);
                set_sample_rate(sample_rate);

                // 打开耳机声道音量和数字接口激活
                write_cmd(detail::REG::LT_HP_CTL, 0x1ff); //激活 衰减+6dB 零点检测  开启
                write_cmd(detail::REG::RT_HP_CTL, 0x1ff);
                write_cmd(detail::REG::DIG_IF_ACTIVATE, detail::DIGIFACT::ACT);

                // 初始化McBSP
                zq::mcbsp::Control::init(receive_IT,transmit_IT);
            }

4,轮询读取或发送

        AIC23B的读取和发送数据是依靠McBSP完成的,所谓轮询,即不断询问标志是否完成,再决定是否读取或发送。显而易见,CPU的大部分时间都会浪费在判断标志是否完成的过程

        STATIC_ASSERT此处是已经定义好的宏,见Drivers/zq_conf.h文件
            /**
             * 读取通道数据
             * @tparam T 数据类型,可为short或者volatile short
             * @param data_1 通道1数据
             * @param data_2 通道2数据
             * @note 此函数在调试时小心,调试时有可能导致调试窗口无法以图像的形式显示,重启即可。不能显示的表面原因是:Current CPU is null
             */
            template<typename T>
            static void read(T& data_1,T& data_2)
            {
                STATIC_ASSERT(sizeof(T)==sizeof(short),"data type is not short!");
                // 等待McBSP0准备好
                while (regs::spcr1::RRDY::read_bit_not()){}
                data_1 = regs::drr1::read();
                data_2 = regs::drr2::read();
            }



            static void write(const uint16_t data_L, const uint16_t data_R)
            {
                // 等待McBSP0准备好
                while (regs::spcr2::XRDY::read_bit_not()) {}//  此处应该使用XRDY
                regs::dxr1::write(data_L);
                regs::dxr2::write(data_R);
            }

        常见用法是在main函数的while循环里调用读取或者发送函数,弊病也显而易见,如果在while循环里有其他耗时任务,则会导致读取或者发送会错过指定时间(AIC23B读取或者发送数据,都由AIC23B的采样率寄存器控制)。

int main()
{

    // 初始化……

    short data_1,data_2;

    while(1)
    {
        // 轮询方式读取数据
        zq::mcbsp::Control::read(data_1,data_2);

        // 其他任务……
    }


}

        聪明一点的做法是使用定时器中断读取或者发送数据,但仍是轮询的方式,大部分时间仍会浪费在轮询标志过程。如果定时器中断频率设置太低,会导致读取发送数据不及时,设置太高,甚至会因为轮询的原因导致程序一直处在定时器中断里,无法进入主程序。

void interrupt Timer0_ISR()
{
    zq::mcbsp::Control::read(data_1,data_2);

}

        好一点的办法是把read或write函数里的while轮询标志改成if判断(需注意判断条件要反过来),不过需要定时器频率高于采样率频率,不然会遗漏数据

            static void write_inTimerISR(const uint16_t data_L, const uint16_t data_R)
            {
                // 等待McBSP0准备好
                if (regs::spcr2::XRDY::read_bit()) 
                {
                    regs::dxr1::write(data_L);
                    regs::dxr2::write(data_R);

                }//  此处应该使用XRDY

            }

 二、中断

1,简介

        中断正是为了解决频繁轮询标志导致CPU计算资源被严重浪费的问题,在TMS320C55xx的可屏蔽中断中,中断使能分为CPU中断和外设中断两部分,前者使能只能确保CPU接收到中断信号后去执行,后者只能确保外设能发出中断信号。因此想要开启某个外设的中断,需要把CPU中断和外设中断都要使能,下面将以McBSP0为例。

        在使能中断前,需要注册好中断向量,中断向量是在中断向量表汇编文件定义的,McBSP0有接收和发送两个中断向量,先使用.ref声明两个中断函数(函数名前需要加上下划线_,并且.ref不能顶格写)

        在中断向量表中,RINT0和XINT0分别表示McBSP0的读取和发送中断向量

        注册好之后,需要把声明的函数实现,需确保函数定义是以C符号链接,如果是C++环境,需要把函数定义囊括在extern "C"内

void interrupt McBSP0_Receive_ISR()
{
    // ……
}


void interrupt McBSP0_Transmit_ISR()
{
    // ……
}

        在使能McBSP0的中断后,可以把发送、读取相关任务放在中断里了,不过此时需要把轮询标志的过程去除(if判断标志也可以不加,因为McBSP接收中断一般用于RRDY中断使能,后面会说)

            // 读取第一个通道的数据
            template<typename T>
            static void read_data1_IT(T &data_1)
            {
                STATIC_ASSERT(sizeof(T)==sizeof(short),"data type is not short!");
                // 等待McBSP0准备好
                if (regs::spcr1::RRDY::read_bit())
                    data_1 = regs::drr1::read();
            }



            static void write_IT(const uint16_t data_L, const uint16_t data_R)
            {
                if (regs::spcr2::XRDY::read_bit())
                {
                    regs::dxr1::write(data_L);
                    regs::dxr2::write(data_R);
                }
            }

        前面介绍了中断向量的设置,接下来将介绍如何使能MCBSP0的中断

 2,使能中断

        依前面所言,中断使能分成CPU和外设两部分,首先是外设中断使能,在前面的McBSP初始化函数里可以看到下面这行代码,McBSP的SPCR1寄存器里有个RINTM的字段,用于设置McBSP的接收中断使能模式,一般配置为0,即RRDY触发中断模式,意为接收数据就绪时(RRDY置1),触发中断。此外还有帧结束触发中断模式。

        发送中断同理,在SPCR2寄存器中,默认为0,即XRDY触发中断模式。

        然后是CPU中断使能,在IER0和IER1两个中断使能寄存器中找到对应的字段,RINT0在IER0中,XINT0在IER1中。此外,还需要把调试中断寄存器DBIER0和DBIER1配置成IER0和IER1一样,确保调试时中断能正常使用

                cpu::IER0::RINT0::set();
                cpu::DBIER0::RINT0::set();

        如果你的工程是C工程,那么可以自行查手册获取寄存器的字段配置,或者在Drivers/zq_cpu.h中找到你需要的寄存器字段声明,如下RIN0IER0的第5位,IER0地址为0x0000

3,启用中断读取或发送数据

         这一点尤为重要,在前面初始化中断的情况下,想要正确读取或发送数据还需要其他配置。因为中断使能后,并不会直接自动进行数据的收发,还需要发送特定的事件

            static void start_receiveIT()
            {
                // 启动接收中断
                cpu::IER0::RINT0::set();
                cpu::DBIER0::RINT0::set();
                volatile uint16_t temp;
                read_data1(temp);// 必须先读取一次数据(重点在读drr寄存器),之后的中断才能正常触发
            }

            static void start_transmitIT()
            {
                // 启动接收中断
                cpu::IER1::XINT0::set();
                cpu::DBIER1::XINT0::set();
                write(0xFFFF, 0xFFFF);
            }

        如上,可以看到启动发送或者接收中断的函数里,处理使能中断外,还在后面加上了读取或者发送数据的过程。正如前面所说,需要主动产生发送或者读取数据的事件,才能正常触发一次中断(换成“触发下次中断”或许更好理解)。

        同理,如果想要源源不断读取或者发送数据,那么需要在接收或者发送中断里,调用发送或者读取数据的函数,即前面定义的那两个函数

void interrupt McBSP0_Receive_ISR()
{
        if (index_adc >= 128)
            index_adc = 0;
        zq::mcbsp::Control::read_data1_IT(buf_adc[index_adc]);
        ++index_adc;
}


void interrupt McBSP0_Transmit_ISR()
{
        if (index_dac >= 128)
            index_dac = 0;
        zq::mcbsp::Control::write_IT(buf_dac[index_dac], buf_dac[index_dac]);
        ++index_dac;
}

        以读取数据为例,效果如下(对mcbsp的读取和发送函数进行了一层封装)

三、DMA+中断

1,引言

        DMA+中断方式事实上还是DMA,只不过DMA身为外设,也有自己的中断罢了。DMA中断的配置和使用相当麻烦,当然,我指的是初次对照着寄存器配置,坑实在太多。废话不过说,我们直接开始

2, 基本介绍

①架构 

       手册上C5509A有6个DMA,如DMA0等,实际上只有DMA控制器,DMA0等不过是其6个通道。其DMA控制器有以下关键特性:

  • 6个独立通道:支持6路并行数据传输

  • 4种标准端口:DARAM、SARAM、外部存储器和外设

  • 自动初始化:支持循环传输模式

  • 事件同步:可绑定到外设事件(如McBSP接收完成)

  • 双缓冲机制:配置寄存器与工作寄存器分离

②数据传输层级

        DMA传输数据是分成以下层级的

  • 字节:顾名思义,数据的基本单位

  • 元素:1-4字节的数据单元,是DMA传输的基本单位

  • :1-65535个元素组成,或者可以理解为“包”。总之,理解为传输一维数组的数据即可

  • :1-65535帧组成,可理解为传输二维数组

3,寄存器

        DMA的寄存器有不少,就不一一介绍了。从手册里可以查到,DMA寄存器分为全局寄存器和通道寄存器两种,前者是6个DMA通道共享的寄存器,后者是每个DMA通道都有的寄存器

                // ============== 全局寄存器 (所有通道共用) ==============
                DECLARE_MMR_REGISTER(GCR, 0x0E00)  // DMA全局控制寄存器
                DECLARE_MMR_REGISTER(GSCR, 0x0E02) // DMA软件兼容性寄存器
                DECLARE_MMR_REGISTER(GTCR, 0x0E03) // DMA超时控制寄存器

        而我们需要重点关注的只有CCR寄存器和CSDP寄存器,前者是DMA通道控制寄存器决定,后者是源/目标参数寄存器。字段如下:

                    // DMA通道控制寄存器(DMACCR)
                    BEGIN_MMR_REGISTER(CCR, 0x0C01 + dma)

                        // [15:14] 目标地址模式 (DSTAMODE)
                        // 00b: 常量地址 (传输期间地址不变)
                        // 01b: 后递增 (根据数据类型自动增加地址)
                        // 10b: 单索引 (传输后地址增加元素索引值) 每次元素传输后地址 += 元素索引
                        // 11b: 双索引 (帧内用元素索引,帧间用帧索引)    帧内元素索引/帧间帧索引
                        DECLARE_MMR_BITS_FIELD(DST_AMODE, 2, 14)

                        // [13:12] 源地址模式 (SRCAMODE)
                        // 00b: 常量地址 (传输期间地址不变)
                        // 01b: 后递增 (根据数据类型自动增加地址)
                        // 10b: 单索引 (传输后地址增加元素索引值)
                        // 11b: 双索引 (帧内用元素索引,帧间用帧索引)
                        DECLARE_MMR_BITS_FIELD(SRC_AMODE, 2, 12)

                        // [11] 结束编程标志 (ENDPROG)
                        // 0: 配置寄存器可编程/编程中
                        // 1: 编程结束 (表示CPU已完成配置)
                        DECLARE_MMR_BIT(END_PROG, 11)

                        // [10] 保留位 (必须写0)
                        DECLARE_MMR_BIT(Reserved1, 10)

                        // [9] 重复模式 (REPEAT)
                        // 0: 仅在ENDPROG=1时重复 (需CPU握手)
                        // 1: 无条件重复 (忽略ENDPROG状态)
                        DECLARE_MMR_BIT(REPEAT, 9)

                        // [8] 自动初始化 (AUTOINIT)
                        // 0: 禁用自动初始化
                        // 1: 使能自动初始化 (块传输完成后自动重载)
                        DECLARE_MMR_BIT(AUTO_INIT, 8)

                        // [7] 通道使能 (EN)
                        // 0: 禁用通道  1: 启用通道
                        DECLARE_MMR_BIT(EN, 7)

                        // [6] 通道优先级 (PRIO)
                        // 0: 低优先级  1: 高优先级
                        DECLARE_MMR_BIT(PRIO, 6)

                        // [5] 帧同步模式 (FS)
                        // 0: 元素同步 (每个元素需事件触发)
                        // 1: 帧同步 (每帧只需一个事件触发)
                        DECLARE_MMR_BIT(FS, 5)

                        // [4:0] 同步事件选择 (SYNC)
                        // 00000b: 无同步事件 (立即启动)
                        // 其他值: 选择特定外设事件作为同步源
                        DECLARE_MMR_BITS_FIELD(SYNC, 5, 0)

                    END_MMR_REGISTER()
                    // 源/目标参数寄存器 (DMACSDP)
                    BEGIN_MMR_REGISTER(CSDP, 0x0C00 + dma)

                        // [15:14] 目的突发使能 (DSTBEN)
                        // 00b: 禁止突发  10b: 使能突发  其他: 保留
                        DECLARE_MMR_BITS_FIELD(DSTBEN, 2, 14)

                        // [13] 目的打包使能 (DSTPACK)
                        // 0: 不打包  1: 打包
                        DECLARE_MMR_BIT(DSTPACK, 13)

                        // [12:9] 目的端口选择 (DST)
                        // 0000b: SARAM  0001b: DARAM
                        // 0010b: 外部内存  0011b: 外设
                        DECLARE_MMR_BITS_FIELD(DST, 4, 9)

                        // [8:7] 源突发使能 (SRCBEN)
                        // 00b: 禁止突发  10b: 使能突发  其他: 保留
                        DECLARE_MMR_BITS_FIELD(SRCBEN, 2, 7)

                        // [6] 源打包使能 (SRCPACK)
                        // 0: 不打包  1: 打包
                        DECLARE_MMR_BIT(SRCPACK, 6)

                        // [5:2] 源端口选择 (SRC)
                        // 0000b: SARAM  0001b: DARAM
                        // 0010b: 外部内存  0011b: 外设
                        DECLARE_MMR_BITS_FIELD(SRC, 4, 2)

                        // [1:0] 数据尺寸 (DATATYPE)
                        // 00b: 8位  01b: 16位  10b: 32位  11b: 保留
                        DECLARE_MMR_BITS_FIELD(DataSize, 2, 0)

                    END_MMR_REGISTER()

        注意到CCR寄存器的低5位为SYNC字段,即同步事件。DMA传输数据是设置同步事件的,不然DMA不知道要传输什么,因为不是所有的外设都能使用DMA。

        同步事件定义如下:

                // DMA同步事件定义
                DECLARE_ATTRIBUTE(Event,
                                  None = 0x00,               // 00000b: 无同步事件
                                  McBSP0_Receive = 0x01,     // 00001b: McBSP0接收事件(REVTO)
                                  McBSP0_Transmit = 0x02,    // 00010b: McBSP0传输事件(XEVT0)
                                  McBSP1_Receive = 0x05,     // 00101b: McBSP1/MMC-SD1接收事件
                                  McBSP1_Transmit = 0x06,    // 00110b: McBSP1/MMC-SD1传输事件
                                  McBSP2_Receive = 0x09,     // 01001b: McBSP2/MMC-SD2接收事件
                                  McBSP2_Transmit = 0x0A,    // 01010b: McBSP2/MMC-SD2传输事件
                                  Timer0_Int = 0x0D,         // 01101b: Timer0中断事件
                                  Timer1_Int = 0x0E,         // 01110b: Timer1中断事件
                                  EXT_INT0 = 0x0F,           // 01111b: 外部中断0
                                  EXT_INT1 = 0x10,           // 10000b: 外部中断1
                                  EXT_INT2 = 0x11,           // 10001b: 外部中断2
                                  EXT_INT3 = 0x12,           // 10010b: 外部中断3
                                  EXT_INT4_or_I2C_Rec = 0x13,// 10011b: 外部中断4/或I2C接收事件
                                  I2C_Transmit = 0x14,       // 10100b: I2C传输事件(XEVT12C)
                                  _RSVD_00011b = 0x03,       // 00011b: 保留
                                  _RSVD_00100b = 0x04,       // 00100b: 保留
                                  _RSVD_00111b = 0x07,       // 00111b: 保留
                                  _RSVD_01000b = 0x08,       // 01000b: 保留
                                  _RSVD_01011b = 0x0B,       // 01011b: 保留
                                  _RSVD_01100b = 0x0C,       // 01100b: 保留
                                  _RSVD_Other = 0x15         // 10101b及以上: 保留
                )

4,初始化 

        由于DMA寄存器需要配置的参数很多,为便于配置,需要定义一个结构体来辅助初始化

            // DMA配置结构体
            struct Config
            {
                // --- CSDP寄存器配置 ---
                unsigned int dstBen; // [15:14] 目的突发使能
                unsigned int dstPack; // [13] 目的打包使能
                unsigned int dstPort; // [12:9] 目的端口选择 这个端口不能设置错误,否则DMA无法正常启动
                unsigned int srcBen; // [8:7] 源突发使能
                unsigned int srcPack; // [6] 源打包使能
                unsigned int srcPort; // [5:2] 源端口选择 这个端口不能设置错误,否则DMA无法正常启动
                unsigned int dataSize; // [1:0] 数据尺寸

                // --- CCR寄存器配置 ---
                unsigned int dstAddrMode; // [15:14] 目标地址模式
                unsigned int srcAddrMode; // [13:12] 源地址模式
                bool fs; // [5] 帧同步模式
                bool priority; // [6] 通道优先级
                bool autoinit; // [8] 自动初始化
                bool mode; // [9] 重复传输模式

                // 索引配置(二维数据传输)
                short srcElementIndex; // 源元素索引
                short srcFrameIndex; // 源帧索引
                short dstElementIndex; // 目标元素索引
                short dstFrameIndex; // 目标帧索引

                // 构造函数设置默认值
                Config() : dstBen(0), dstPack(0), dstPort(0),
                           srcBen(0), srcPack(0), srcPort(0),
                           dataSize(1), // 默认16位数据
                           dstAddrMode(0), srcAddrMode(0),
                           fs(false), priority(false), autoinit(false), mode(false),
                           srcElementIndex(0), srcFrameIndex(0),
                           dstElementIndex(0), dstFrameIndex(0)
                {
                }
            };

        那么初始化函数实际上就是对CCR寄存器和CSDP寄存器进行初始化(注释都很详细,不赘述了)。在前面的中断介绍里提到,中断使能分为两部分,这里可以提前使能CPU的DMA中断,后面只需要启闭DMA的中断使能寄存器即可控制DMA的中断使能。

            // 使用自定义配置初始化
            static void init(const Config &config)
            {
                // 禁用通道
                CCR_REG::EN::clear();

                // 配置CSDP寄存器
                CSDP_REG::DSTBEN::write_bits(config.dstBen);
                CSDP_REG::DSTPACK::write_bit(config.dstPack);
                CSDP_REG::DST::write_bits(config.dstPort);
                CSDP_REG::SRCBEN::write_bits(config.srcBen);
                CSDP_REG::SRCPACK::write_bit(config.srcPack);
                CSDP_REG::SRC::write_bits(config.srcPort);
                CSDP_REG::DataSize::write_bits(config.dataSize);

                // 配置索引寄存器
                CEI_REG::write(config.srcElementIndex);
                CFI_REG::write(config.srcFrameIndex);
                CDEI_REG::write(config.dstElementIndex);
                CDFI_REG::write(config.dstFrameIndex);

                // 配置CCR寄存器
                CCR_REG::DST_AMODE::write_bits(config.dstAddrMode);
                CCR_REG::SRC_AMODE::write_bits(config.srcAddrMode);
                CCR_REG::FS::write_bit(config.fs);  // 默认关闭帧同步,即使用元素同步,确保每个元素都按照时序传输
                CCR_REG::PRIO::write_bit(config.priority);
                CCR_REG::AUTO_INIT::write_bit(config.autoinit);
                CCR_REG::REPEAT::write_bit(config.mode);

                // 清除停止标志
                CCR_REG::END_PROG::clear();

                // 默认启用CPU中断(DMA的中断没开,所以实际上中断并不会触发)
                cpu::IER1::DMAC0::set();
                cpu::DBIER1::DMAC0::set();
            }

        如前面介绍的中断内容一样,以DMA0为例,也需要到中断向量表里注册中断向量,然后实现中断向量函数……

 5,启用DMA传输

        DMA传输需要设置传输的事件、源/目标地址和帧大小(帧数量先不考虑),尤为需要注意的是地址。TMS320C55xx的CPU对数据进行操作的是字地址,而DMA传输需要字节地址。简单来说就是,TMS320C55xx是16位机,字长是16位,它把一个字节当成了16位,而非我们传统意义上的8位,因此需要把地址左移1位(相当于乘以2),把字地址变成字节地址。

            // 启动DMA传输(多帧)
            static void start(
                detail::info::Event::Type event, // 同步事件
                uint32_t srcAddr, // 源地址(32位)
                uint32_t dstAddr, // 目标地址(32位) I/O地址空间和存储器地址必须都要左移一位
                uint16_t elementCount, // 每帧元素数
                uint16_t frameCount = 1// 帧数量
            )
            {
                // 禁用通道
                CCR_REG::EN::clear();

                // 配置地址寄存器
                srcAddr <<= 1; // I/O地址空间和存储器地址必须都要左移一位
                dstAddr <<= 1; // I/O地址空间和存储器地址必须都要左移一位
                CSSA_L_REG::write(srcAddr & 0xFFFF);
                CSSA_U_REG::write((srcAddr >> 16) & 0xFFFF);
                CDSA_L_REG::write(dstAddr & 0xFFFF);
                CDSA_U_REG::write((dstAddr >> 16) & 0xFFFF);

                // 配置传输数量
                CEN_REG::write(elementCount);
                CFN_REG::write(frameCount);

                // 设置同步事件
                CCR_REG::SYNC::write_bits(event);

                // 在AutoInit为1,REPEAT为0时,只要设置END_PROG=1(比如在中断里设置),就会自动开始新一轮(只有1轮)
                // 确保自动初始化握手完成
                if (CCR_REG::AUTO_INIT::read_bit()) {
                    CCR_REG::END_PROG::set();
                }

                // 使能通道
                CCR_REG::EN::set();
            }

6,McBSP的DMA传输

         前面介绍的是DMA的一般使用流程,先初始化,然后开始传输,需注意在DMA初始化前AIC23B就已经初始化好了。现在我们McBSP的DMA传输为实例(本文以DMA0为例):

①读取

        首先,先初始化DMA,这里我们配置DMA的目的是从AIC23B读取数据到内存里的一个数组,因此源端口配置为3,指向的是外设。由于数组等变量在cmd文件里被我放到了DARAM里,因此目标端口设置为1。注意!端口不能设置错误,否则DMA无法进行传输。

        // 配置DMA0用于McBSP0接收
        DMA0::Config cfg;
        cfg.srcAddrMode = 0;       // 源地址模式:常量地址(McBSP寄存器固定)
        cfg.dstAddrMode = 1;       // 目标地址模式:后递增(存入数组)
        cfg.dataSize = 1;          // 16位数据
        cfg.autoinit = true;       // 自动重新加载
        // 这两个端口不能设置错误,否则DMA无法正常启动
        cfg.srcPort = 3;           // 源端口:外设(McBSP)
        cfg.dstPort = 1;           // 目标端口:0:SARAM(内存) 1:DARAM

        DMA0::init(cfg);
        DMA0::enableIT(true);

        前面初始化后,还会启用DMA的中断,这是因为此行的目的是读取数据,那么读取完之后需要通知CPU,那么最好的方式是以中断来通知,因此开启是blockIE,即块传输完成后触发DMA中断。

            // 配置中断
            static void enableIT(
                bool blockIE = false, // 块传输完成中断
                bool lastIE = false, // 最后传输中断
                bool frameIE = false, // 帧传输完成中断
                bool halfIE = false, // 半帧传输中断
                bool dropIE = false, // 事件丢失中断
                bool timeoutIE = false // 超时中断
            )
            {
                CICR_REG::BLOCKIE::write_bit(blockIE);
                CICR_REG::LASTIE::write_bit(lastIE);
                CICR_REG::FRAMEIE::write_bit(frameIE);
                CICR_REG::HALFIE::write_bit(halfIE);
                CICR_REG::DROPIE::write_bit(dropIE);
                CICR_REG::TIMEOUTIE::write_bit(timeoutIE);
            }

        需注意,DMA中断触发后,需要手动清理标志位,而清理标志位的方式就是读取CSR寄存器即可。it_flags是DMA类里的一个成员变量,与DMA操作无关,作用是复制CSR寄存器的标志,用于在main函数主循环里通知DMA传输完成了

            // 清除中断标志
            static void clearInterruptFlags()
            {
                // 读取状态寄存器会自动清除标志位
                const volatile uint16_t status = CSR_REG::read();
                it_flags = status; // 对齐标志位
            }

        一切就绪后就可以开始DMA传输了

        // 启动DMA传输
        DMA0::start(
            dma::detail::info::Event::McBSP0_Receive,   // 同步事件
            mcbsp::regs::drr1::REG,                     // McBSP接收寄存器字节地址
            reinterpret_cast<uint32_t>(buf_adc),        // 缓冲区字节地址
            FFT_SIZE                                    // 元素数量
        );

        由于没有配置DMA循环重复传输,那么传输完成后就会自动停止。在main函数主循环里

可以读取it_flags里保存的CSR寄存器的值,进而判断是否传输完成。传输完成后就可以进行数据处理,如FFT,然后再重新传输。

int main()

{

    // 初始化……

    while(1)
    {

        if (DMA0::getITFlags(DMA0::IT_Flags::blockIE)) {
            LED::on(led::pin::LED_3);
            DMA0::start(
                dma::detail::info::Event::McBSP0_Receive,   // 同步事件
                mcbsp::regs::drr1::REG,                     // McBSP接收寄存器字节地址
                reinterpret_cast<uint32_t>(buf_adc),        // 缓冲区字节地址
                FFT_SIZE                                    // 元素数量
            );
        }

    }
}

        前面在DMA初始化过程还记得我们配置了autoinit吗?这个东西是自动初始化,意思是当DMA传输结束后,就可以自动把寄存器里的值配置为我们DMA初始化结束的时候(初始化结束的标志,就是把END_PROG置为1)。并且DMA传输结束后会自动把END_PROG清零,当我们把END_PROG寄存器置为1即可重新进行DMA传输,不需要重新调用DMA_start函数了

// zq_dma.h中

/**
* @brief 重新进行传输,当且仅当autoinit设置为1的情况下
*/
static void restart()
{
    CCR_REG::END_PROG::set();
}


// main.cpp中
int main()

{

    // 初始化……

    while(1)
    {

        if (DMA0::getITFlags(DMA0::IT_Flags::blockIE)) {
            LED::on(led::pin::LED_3);
            DMA0::restart;
        }

    }
}

效果如下:

 ②发送

        发送数据同理,只要交换源/目标端口就行,如果想要循环不断发送数据,那么不需要开启中断,并且把下面两个寄存器同时设置为1,这样开启传输后就会不断重复发送数据。

CCR_REG::AUTO_INIT::set();
CCR_REG::REPEAT::set();

        尤为需要注意的是,DMA接收数据虽然不像McBSP中断那样需要主动接收数据才能触发中断事件,但是DMA发送数据要。也就是说启动DMA传输后,还需要主动发送数据以产生发送同步事件,这个DMA才能正式启动传输

        // 启动DMA传输(如果是发送事件,必须让McBSP主动发送数据才能正确触发DMA)
        DMA0::start(
            dma::detail::info::Event::McBSP0_Transmit,   // 同步事件:McBSP发送
            reinterpret_cast<uint32_t>(buf_dac),      // 源地址:正弦波表
            mcbsp::regs::dxr1::REG,                     // 目标地址:McBSP发送寄存器
            FFT_SIZE                           // 元素数量
        );
        aic23::Control::write_data(0xFFFF,0xFFFF);// 对McBSP发送函数的一层封装

        发送方波的时候,效果如下(并非DMA的原因,而是AIC23B输出剧烈变化的电压时会有“回弹”现象):

 7,优化接口

        为了让DMA使用起来更加方便,在Drivers/zq_dma.h里还提供了另一套接口

使用效果如下(读取数据):

        // =============== 新接口初始化 ==================
        DMA0::config::baseInit();
        DMA0::config::srcAddrMode::constant();
        DMA0::config::dstAddrMode::increase();
        DMA0::config::dataSize::_16bit();
        DMA0::config::autoinit::enable();
        DMA0::config::srcPort::peripheral();
        DMA0::config::dstPort::dram();
        DMA0::config::itConfig::block::enable();

        // 启动DMA传输
        DMA0::start(
            dma::detail::info::Event::McBSP0_Receive,   // 同步事件
            mcbsp::regs::drr1::REG,                     // McBSP接收寄存器字节地址
            reinterpret_cast<uint32_t>(buf_adc),        // 缓冲区字节地址
            FFT_SIZE                                    // 元素数量
        );

为进一步提供便利,把平时常用的配置封装为了预设

        // =============== 预设初始化 ==================
        DMA0::config::mode::peripheralToMemory();// 使用预设
        
        // 启动DMA传输
        DMA0::start(
            dma::detail::info::Event::McBSP0_Receive,   // 同步事件
            mcbsp::regs::drr1::REG,                     // McBSP接收寄存器字节地址
            reinterpret_cast<uint32_t>(buf_adc),        // 缓冲区字节地址
            FFT_SIZE                                    // 元素数量
        );

由于AIC23B在使用DMA过程中,基本不会改变,于是做了进一步的封装(如果能使用C++11或者C++17,有了自动推导和静态检查,表达或许能更加简练且强大)

        aic23::Control::receiveDMA<DMA0>::init();
        aic23::Control::receiveDMA<DMA0>::start(buf_adc,FFT_SIZE);

使用typedef重命名后,就可以得到更加方便且不易错的形式

// 在文件的头部重命名
typedef bsp::aic23::Control::receiveDMA<zq::DMA0> aic23_ReceiveDMA;
typedef bsp::aic23::Control::transmitDMA<zq::DMA1> aic23_TransmitDMA;


// 在main函数里使用

int main()
{
    // 其他初始化……

    // DMA读取
    aic23_ReceiveDMA::init();
    aic23_ReceiveDMA::start(buf_adc,FFT_SIZE);

    // DMA发送
    aic23_TransmitDMA::init<true,true>();
    aic23_TransmitDMA::start(buf_dac,FFT_SIZE);


    while(true)
    {

        if (aic23_ReceiveDMA::isComplete()) {
            LED::on(led::pin::LED_3);
            aic23_ReceiveDMA::restart();
        }

        // 其他任务……
    }

}

8,实测过程中的一个问题 

         实测过程中,前0~3个数据可能会有异常,建议把数组大小扩大4个,处理数据时再忽略前4个即可

9,分享一个bug

         一天也写不了多少bug,这几天在使用std命名空间里的东西,总是会发生十分奇怪的问题,有时候是程序运行起来莫名奇妙,有些函数感觉没有加载一样。比如这次,这样写DMA0可以正常读取AIC23B数据

        for (int i = 0; i < 128; ++i)
        {
            buf_adc[i] = 0;
        }

        aic23_ReceiveDMA::init();
        aic23_ReceiveDMA::start(buf_adc,FFT_SIZE);

        for (int i = 0; i < 128; ++i)
        {
            buf_dac[i] = 19537 * std::sin(2 * 3.1415926 * i / 128);
        }

        aic23_TransmitDMA::init<true,true>();
        aic23_TransmitDMA::start(buf_dac,FFT_SIZE);

        这样写,不行

        for (int i = 0; i < 128; ++i)
        {
            buf_adc[i] = 0;
            buf_dac[i] = 19537 * std::sin(2 * 3.1415926 * i / 128);
        }

        aic23_ReceiveDMA::init();
        aic23_ReceiveDMA::start(buf_adc,FFT_SIZE);


        aic23_TransmitDMA::init<true,true>();
        aic23_TransmitDMA::start(buf_dac,FFT_SIZE);

        此外,std::memcpy等函数也是如此,发生奇奇怪怪的错误,偏偏程序也不会卡死

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值