芯片: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中找到你需要的寄存器字段声明,如下RIN0在IER0的第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等函数也是如此,发生奇奇怪怪的错误,偏偏程序也不会卡死