总结——TI_音频信号分析仪

news2024/11/15 2:15:25

一、简介

设备:MSPM0G3507

:CMSIS-DSP        TI

数据分析:FFT

软件:CCS        CLion        MATLAB

目的:对音频信号进行采样(滤波+偏置处理),通过FFT获取信号的频率成分,得到频率谱、幅值谱和功率谱

二、可行性分析

1,MATLAB

①先设置一个序列模拟实际采样数据

        假设采样频率为20.48KHz,那么我们可以设置一个频率为1/20Hz的正弦函数来生成采样数据,采样数据所代表的波形为1.024KHz的正弦波

f = 1/20;    
X = (i*sin(2*pi*f*t) + 1000)*0.001; %电压

②将采样数据通过FFT转为频谱

Y = fft(X);

 ③对频谱进行取模处理,得到幅值谱

A = abs(Y); % 幅度谱

④通过帕斯瓦尔定理,将幅值谱转为功率谱

        根据帕斯瓦尔定理,可得\sum x[n]^{2} =\frac{\sum X[n]^{2}}{N},即幅值谱数据的平方除以2等于采样数据的平方。

        但由于只取正频率,那么还需要乘以2

P = A.^2 / (N/2);

 ⑤注意事项

        得到的功率谱由于是直接通过模长的平方得到,那么其单位应为V{^2}/Hz(采样数据的单位V),并非W/Hz

2,代码

clc;
clear all;
close all;

% 1. 构建一个序列x
N = 1024;
t = (0:N-1)'; % 创建时间向量
f = 1/20;     % 假设ADC采样频率为20.48KHz,那么这个序列为20.48/20KHz的正弦波

yuan=[2.5,10,22.5,40,62.5,90,122.5,160];%源自实际信号功率
j=1;

for i=100:100:800

X = (i*sin(2*pi*f*t) + 1000)*0.001; %电压

% 2. 转为频域
Y = fft(X);

% 3. 把Y转为幅值谱
A = abs(Y); % 幅度谱
frequencies = (1:1: N/2+1); % 频率轴
A = A(2:N/2+1); % 只取正频率部分,去除直流分量

% 4. 使用帕斯瓦尔定理转为功率谱
% Parseval定理表明时域能量等于频域能量
% 所以功率谱可以通过幅度谱的平方获得
P = A.^2 / (N/2); % 功率谱,归一化因子为N/2,因为我们只考虑正频率部分

% 画图
figure;
subplot(2,1,1);
plot(frequencies(2:end), A); % 从第二个点开始绘制,排除直流分量
title('Amplitude Spectrum');
xlabel('Frequency (Hz)');
ylabel('Magnitude');

subplot(2,1,2);
plot(frequencies(2:end), P); % 从第二个点开始绘制,排除直流分量
title('Power Spectrum');
xlabel('Frequency (Hz)');
ylabel('Power');

% 打印功率谱中的极大值
[maxP, indexMaxP] = max(P);
fprintf('%dmV:功率谱中的极大值位于频率 %d ,功率值为 %fV(或者V^2)\n', i,frequencies(indexMaxP), maxP);
fprintf('实际功率值为 %fmW,则阻抗为:%f\n\n',yuan(j),maxP/yuan(j)*1000);
j=j+1;

end

3,仿真结果

        图像中舍去了直流成分,正弦波的频率成分只有一个尖峰,很合理

三、代码设计

1,库函数介绍

        这次编程一共涉及到CMSIS-DSP的这两个函数,后面将以采样1024个数据为例。

        同时要注意哈,由于音频信号频率一般在几十Hz到10KHz以内,根据奈奎斯特定律,采样频率应为20KHz以上。由于采样数据为1024个,想要让分辨率整一点,所以选用20.48KHz。分辨率为20.48K/1024=20

arm:表示适用arm平台

cfft:c即complex(复数),fft:快速傅里叶变换,f32:float32数据类型

 mag:幅值?反正把频谱转为幅值谱

arm_cfft_f32(&arm_cfft_sR_f32_len1024, fftArray, IFFTFLAG, BITREVERSE);
 arm_cmplx_mag_f32(inputArray, outputArray, NUM_SAMPLES / 2);

①arm_cfft_f32

        第一个参数是个 CMSIS-DSP里已经定义好的实例,在arm_const_structs.h头文件里,且见名知义。由于采样数据是1024个(一般选用4的倍数,基4更快嘛),所以选用len1024

        第二个参数就是输入的数组,准确来说是复数数组,因此你需要一个更大的数组,1024*2的数组大小。每两个相邻数组分别存放复数的实部和虚部,实部就是ADC的采样数据,虚部放0.

        第三个参数是表明进行的是正变换还是逆变换。我们需要进行正变换,添0

        第四个参数是序列翻转,添1就行。详情请见

② arm_cmplx_mag_f32

        第一个参数是从arm_cfft_f32得到的复数数组(频谱)

        第二个参数是接收转换后的幅值谱,是实数数组,只不过数组大小为1024/2,因为对称性,除了直流成分,数组前半部分与后半部分对称。

        第三个参数就是数组大小,添1024/2

2,用户函数设计

        除了幅值谱外,我们还需要失真度分析、电压幅值谱、功率谱、幅值谱中极大值点等

①获取幅值谱中极大值点

        跟AI(智谱、通义)进行一顿交互、辩论得到的极大值寻找函数(不是最优),用于获取幅值谱中峰值的索引。窗口大小不能太小,否则容易误判,太大则会找不到几个极大值点。

static inline void find_peaks(const float32_t *fftOutput, uint16_t fftLength, uint16_t *peaks, uint32_t NumPeaks, uint16_t windowSize)
{
    float32_t maxVal;
    uint16_t maxIdx;
    uint16_t halfWindowSize = windowSize / 2;//以一般理性而言,不会为0
    uint16_t numPeaks = 1;                   //从一次谐波开始

    //重置
    for (uint16_t i = 0; i <NumPeaks ; ++i)
    {
        peaks[i] = 0;
    }
    // 开始寻找交流成分
    for (uint16_t i = halfWindowSize; i < 512 - halfWindowSize; i++)
    {
        maxVal = fftOutput[i];
        maxIdx = i;

        // 在滑动窗口内查找最大值
        for (uint16_t j = i - halfWindowSize; j <= i + halfWindowSize; j++)
        {
            if (fftOutput[j] > maxVal)
            {
                maxVal = fftOutput[j];
                maxIdx = j;
            }
        }

        // 检查窗口中心点是否为窗口内的最大值
        if (maxIdx == i)
        {
            // 检查是否与最近的极大值足够远
            if (peaks[numPeaks] == 0 || (i - peaks[numPeaks - 1]) > windowSize)
            {
                peaks[numPeaks] = i;//peaks存的是索引
                ++numPeaks;

                //存满以后就退出
                if (numPeaks == NumPeaks)
                    return;
            }
        }
    }
}

②功率谱

        根据前面MATLAB分析,只需对幅值谱(准确说是幅值谱中的极大值)进行平方和归一化处理即可

Tips:

        之前做功率谱分析时,错误地将已转换为电压幅值谱当做输入。应将经由arm_cmplx_mag_f32得到的幅值谱直接转换为功率谱,不必经由第三方。

static inline float32_t powerCalculate(const float32_t *fft_Array, const uint16_t *peaks, uint16_t NumPeaks, float *power)
{
    float sum = 0;
    //总功率应去除直流量
    for (uint16_t i = 0; i <NumPeaks ; ++i)
    {
        power[i]=0;
    }

    for (uint16_t i = 1; i < NumPeaks; i++)
    {
        if (peaks[i] == 0)
        {
            break;//说明谐波已经取完
        }
        power[i] = fft_Array[peaks[i]] * fft_Array[peaks[i]] * 2 / NUM_SAMPLES;//由于只取正频率,故进行归一化处理
        sum += power[i];
    }
    return sum;
}

③幅值谱转为对应的电压幅值谱

        听起来拗口,其实还挺变扭。就是把幅值谱中的幅值转为实际上的电压,因为幅值谱中的幅值代表的物理意义并非是电压,不过它与电压有个对应关系(注释里)。

static inline void voltageAmplitude_Convert(float32_t inputArray[], float32_t outputArray[])
{
    //假设原始信号的峰值为A,那么FFT的结果的每个点(除了第一个点直流分量之外)的模值就是A的N/2倍。而第一个点就是直流分量,它的模值就是直流分量的N倍。
    /**将幅值谱转为电压幅值谱*/
    for (uint16_t i = 1; i < NUM_SAMPLES / 2; ++i)
    {
        //理论上模值为峰峰值的N/2倍,实测中还应再除以0.75,后来发现就不需要了
        fft_outputbuf[i] = (float32_t) (fft_outputbuf[i] * 2 / NUM_SAMPLES);
    }
    fft_outputbuf[0] /= NUM_SAMPLES;
}

④失真度计算

        二次及二次以上的谐波成分的平方和除以基波的平方,然后再开方。

        此时你会发现,谐波应为基波的整数倍,也就是说除了通过查找多个峰值的办法,还可以先找到第一个峰值的索引(需先去除直流量),然后把这个索引乘以相应倍数获得对应谐波。CMSIS-DSP有这个找峰值的函数,叫arm_max_f32,记得要去除直流成分。

static inline void signalDistortionDegree(const float32_t *fft_Array, const uint16_t *peaks, uint16_t NumPeaks, float *thd)
{
    float sum = 0;
    //从二次谐波开始计算平方和
    for (uint16_t i = 2; i < NumPeaks; i++)
    {
        if (peaks[i] == 0)
        {
            break;//说明谐波已经取完
        }
        sum += fft_Array[peaks[i]] * fft_Array[peaks[i]];//平方和
    }
    sum /= (fft_Array[peaks[1]] * fft_Array[peaks[1]]);//除以1次谐波的平方
    *thd = sqrtf(sum) * 100;                           //计算出失真度,并转为百分比表示
}

3,代码

        这里面需要关注的主体函数是 void SignalAnalyzer_handler();这个函数是放到while大循环里的(MSPM0的内存实在太小,只能先放弃RTOS)

        里面可能有部分注释忘记修改,还请见谅。

//
// Created by 34753 on 2024/7/18.
//

#include "SignalAnalyzer.h"
#if SignalAnalyzer_Open
#include "ADC.h"
#include "LED.h"
#include "OPA.h"
#include "arm_const_structs.h"
#include "arm_math.h"

/**宏定义*/
#define NUM_SAMPLES 1024//采样点
#define AV 1           //放大增益
#define NUM_PEAKS 9     //取9-1个基波和谐波成分

#define IFFTFLAG 0  //正变换
#define BITREVERSE 1//逆序排列


/**变量*/
int16_t ADC_Data[1024];                  //ADC采样数据
float32_t fft_inputBuff[NUM_SAMPLES * 2];//存储复数的数组
float32_t fft_outputbuf[NUM_SAMPLES / 2];//存储实数的数组,由于奈奎斯特的特性,需要除以2

// 为了某种目的,包含了直流成分
uint16_t peaks[NUM_PEAKS];//幅值谱的极大值点
float power[NUM_PEAKS];   //功率谱
float totalPower;         //总功率单位为V^2
float thd;                //失真度

volatile bool waitADCData_Flag = true;//用于检测是否需要等待


void SignalAnalyzer_Init()
{
    OPA_Init();
    ADC_DMA_Init(ADC_Data, 1024);
}

#if 0
//计算失真度
void THD(void)
{
    thd_basic = fft_outputbuf[10];

    u[0] = fft_outputbuf[20];
    u[1] = fft_outputbuf[30];
    u[2] = fft_outputbuf[40];
    u[3] = fft_outputbuf[50];
    u[4] = fft_outputbuf[60];

    arm_power_f32(u, 4, &sum);
    arm_sqrt_f32(sum, &thd_high);

    thd = thd_high / thd_basic;
}

//计算总功率和各频率分量的频率和功率
void MW(void)
{
    thd_basic = fft_outputbuf[10];

    u[0] = fft_outputbuf[20];
    u[1] = fft_outputbuf[30];
    u[2] = fft_outputbuf[40];
    u[3] = fft_outputbuf[50];
    u[4] = fft_outputbuf[60];
    arm_power_f32(&u[0], 4, &sum);

    MW_total = sum / 5;
}
#endif

/**
 * @brief   频率谱转为电压幅值谱
 * @param inputArray 经fft转换后的频率谱数组
 * @param outputArray 输出的电压幅值谱数组
 * @note    原本需要在实测中除以0.75,后来就不需要了,原因未知
 */
static inline void voltageAmplitude_Convert(float32_t inputArray[], float32_t outputArray[])
{
    //假设原始信号的峰值为A,那么FFT的结果的每个点(除了第一个点直流分量之外)的模值就是A的N/2倍。而第一个点就是直流分量,它的模值就是直流分量的N倍。
    /**将幅值谱转为电压幅值谱*/
    for (uint16_t i = 1; i < NUM_SAMPLES / 2; ++i)
    {
        //理论上模值为峰峰值的N/2倍,实测中还应再除以0.75,后来发现就不需要了
        fft_outputbuf[i] = (float32_t) (fft_outputbuf[i] * 2 / NUM_SAMPLES);//384
    }
    fft_outputbuf[0] /= NUM_SAMPLES;
}

static inline void Amplitude_Convert(float32_t inputArray[], float32_t outputArray[])
{
    //计算信号的幅度谱  第一个参数指定了需要计算复数模的数组指针,第二个参数指定了计算结果存放的数组指针,第三个参数是需要计算复数模的数据个数。
    // 分辨率=fs(采样频率)/N(采样点数)  output输出数组,索引*分辨率=频率成分
    arm_cmplx_mag_f32(inputArray, outputArray, NUM_SAMPLES / 2);
}


/**
 * @brief   ADC数据转为频谱
 * @param ADCdata   ADC采样数据
 * @param fftArray   频谱数组
 */
static inline void ADCdataToSpectrum(const int16_t ADCdata[], float32_t fftArray[])
{
    /**将实数序列转为复数序列*/
    for (uint32_t i = 0; i < NUM_SAMPLES; ++i)
    {
        fftArray[i * 2] = (float) (ADCdata[i] * 3.3 / (4095 * AV));//转为实际电压,单位为V
        fftArray[i * 2 + 1] = 0;                                   //虚部为零
    }

    假设ADC里的就是电压值V,可以为mV,不过功率的单位应该为uW
    //    for (uint32_t i = 0; i < NUM_SAMPLES; ++i)
    //    {
    //        //w[i]=0.5*(1-arm_sin_f32(2*PI*i/(NUM_SAMPLES-1)));
    //        fftArray[i * 2] = (float) (ADCdata[i] * 0.001);
    //        //  fftArray[i * 2]=fftArray[i * 2]*w[i];
    //        fftArray[i * 2 + 1] = 0;
    //    }

    /**
 * 对ADC数据进行1024点的复数快速傅里叶变换(FFT)。
 *
 * ADC数据被转换到频域,存储了1024个复数样本。
 * 每个元素存储复数的实部和虚部,实部存储在偶数索引位置,虚部存储在奇数索引位置。
 *
 * FFT输出存储在ADC_DataOut中,包含了每个频率分量的幅度信息。
 * 返回ADC_DataOut中幅度最大的频率分量的索引,即第5个FFT点(0索引)。
 *
 * @param ADC_Data      输入的ADC数据数组
 * @param ADC_DataOut   输出的FFT结果数组
 * @param arm_max_q15    用于在ADC_DataOut中找到最大幅度值及其索引的函数
 */

    arm_cfft_f32(&arm_cfft_sR_f32_len1024, fftArray, IFFTFLAG, BITREVERSE);
}


/**
 * @brief   寻找峰值
 * @param fftOutput
 * @param fftLength
 * @param peaks 存储极大值点的数组
 * @param numPeaks  极大值点个数
 * @param windowSize 窗口大小应为奇数,太小会错过极大值,太大会判断错误极大值
 */
static inline void find_peaks(const float32_t *fftOutput, uint16_t fftLength, uint16_t *peaks, uint32_t NumPeaks, uint16_t windowSize)
{
    float32_t maxVal;
    uint16_t maxIdx;
    uint16_t halfWindowSize = windowSize / 2;//以一般理性而言,不会为0
    uint16_t numPeaks = 1;                   //从一次谐波开始

    //重置
    for (uint16_t i = 0; i <NumPeaks ; ++i)
    {
        peaks[i] = 0;
    }
    // 开始寻找交流成分
    for (uint16_t i = halfWindowSize; i < 512 - halfWindowSize; i++)
    {
        maxVal = fftOutput[i];
        maxIdx = i;

        // 在滑动窗口内查找最大值
        for (uint16_t j = i - halfWindowSize; j <= i + halfWindowSize; j++)
        {
            if (fftOutput[j] > maxVal)
            {
                maxVal = fftOutput[j];
                maxIdx = j;
            }
        }

        // 检查窗口中心点是否为窗口内的最大值
        if (maxIdx == i)
        {
            // 检查是否与最近的极大值足够远
            if (peaks[numPeaks] == 0 || (i - peaks[numPeaks - 1]) > windowSize)
            {
                peaks[numPeaks] = i;//peaks存的是索引
                ++numPeaks;

                //存满以后就退出
                if (numPeaks == NumPeaks)
                    return;
            }
        }
    }
}

/**
 * @brief  计算失真度
 * @param fft_Array
 * @param peaks
 * @param NumPeaks
 * @param thd
 * @note    失真度的单位是百分比
 */

static inline void signalDistortionDegree(const float32_t *fft_Array, const uint16_t *peaks, uint16_t NumPeaks, float *thd)
{
    float sum = 0;
    //从二次谐波开始计算平方和
    for (uint16_t i = 2; i < NumPeaks; i++)
    {
        if (peaks[i] == 0)
        {
            break;//说明谐波已经取完
        }
        sum += fft_Array[peaks[i]] * fft_Array[peaks[i]];//平方和
    }
    sum /= (fft_Array[peaks[1]] * fft_Array[peaks[1]]);//除以1次谐波的平方
    *thd = sqrtf(sum) * 100;                           //计算出失真度
}


/**
 * @brief  计算功率
 * @param fft_Array
 * @param peaks
 * @param NumPeaks
 * @param power
 * @return
 * @note    为了整齐划一,我把直流量放在power[0]中。同时要注意
 * ①由于数据是采集的电压,那么计算出的功率的单位其实是V^2(不考虑功率谱密度),若想得到mW,那么需要再除以阻抗
 * ②想通过帕斯瓦尔定理得到功率,那么必须是直接从频率谱转换得到幅值谱,而非是电压幅值谱
 * ③若电压的单位是mV,那么计算出的功率的单位是uW
 */

static inline float32_t powerCalculate(const float32_t *fft_Array, const uint16_t *peaks, uint16_t NumPeaks, float *power)
{
    float sum = 0;
    //总功率应去除直流量
    for (uint16_t i = 0; i <NumPeaks ; ++i)
    {
        power[i]=0;
    }

    for (uint16_t i = 1; i < NumPeaks; i++)
    {
        if (peaks[i] == 0)
        {
            break;//说明谐波已经取完
        }
        power[i] = fft_Array[peaks[i]] * fft_Array[peaks[i]] * 2 / NUM_SAMPLES;//由于只取正频率,故进行归一化处理
        sum += power[i];
    }
    return sum;
}
///**
// * @brief  计算指定频率下的功率
// * @param fft_Array
// * @param fft_output
// */
//
//static inline void powerSpecialCalculate(const float32_t *fft_Array, float *fft_output)
//{
//    float sum = 0;
//    for (uint16_t i = 50; i < 500; i+=50)
//    {
//        power[i] = fft_Array[i] * fft_Array[i] * 2 / NUM_SAMPLES;//由于只取正频率,故进行归一化处理
//        sum += power[i];
//    }
//    return sum;
//}

/**
 * @brief  转为实际功率
 * @param totalPower
 * @param power
 * @param Z
 * @param NumPeaks
 */
static inline void realPower_Convert(float *totalPower, float *power, float Z, uint16_t NumPeaks)
{
    for (uint16_t i = 0; i < NumPeaks; i++)
    {
        power[i] /= Z;
    }
    *totalPower /= Z;
}


void SignalAnalyzer_handler()
{
    while (waitADCData_Flag)
        ;
    waitADCData_Flag = true;

    //    LED_ON(LED2_Blue);//表明转换开启

    ADCdataToSpectrum(ADC_Data, fft_inputBuff);//将ADC数据经FFT 转为频谱

    Amplitude_Convert(fft_inputBuff, fft_outputbuf);//转为幅值谱

    find_peaks(fft_outputbuf, NUM_SAMPLES / 2, peaks, NUM_PEAKS, 41);//寻找极大值点

    totalPower = powerCalculate(fft_outputbuf, peaks, NUM_PEAKS, power);//计算出功率V^2

    realPower_Convert(&totalPower, power, 1788.1, NUM_PEAKS);//转为实际功率W

    voltageAmplitude_Convert(fft_inputBuff, fft_outputbuf);//把频率谱转为电压幅值谱(可以不用加,因为交流成分彼此成齐次性)

    signalDistortionDegree(fft_outputbuf, peaks, NUM_PEAKS, &thd);//计算出失真度

    //    LED_OFF(LED2_Blue);
    //    __BKPT(1);
}

/**ISR中断服务*/
void ADC12_0_INST_IRQHandler(void)
{
    switch (DL_ADC12_getPendingInterrupt(ADC12_0_INST))
    {
        case DL_ADC12_IIDX_DMA_DONE:
            /**开启数据分析*/
            ADC_DMA_Stop();
            waitADCData_Flag = false;
            LED_Toggle(LED2_Blue);
            break;
        default:
            break;
    }
}
#if 0
/***********************************************
找最大值,次大值……对应的频率,分析波形
*************************************************/
void select_max(float *f, float *a)
{
    int i, j;
    float k, k1, m;
    float aMax = 0.0, aSecondMax = 0.0, aThirdMax = 0.0, aFourthMax = 0.0;
    float fMax = 0.0, fSecondMax = 0.0, fThirdMax = 0.0, fFourthMax = 0.0;
    int nMax = 0, nSecondMax = 0, nThirdMax = 0, nFourthMax = 0;
    for (i = 1; i < NUM_SAMPLES / 2; i++)//i必须是1,是0的话,会把直流分量加进去!!!!
    {
        if (a[i] > aMax)
        {
            aMax = a[i];
            nMax = i;
            fMax = f[nMax];
        }
    }
    for (i = 1; i < NUM_SAMPLES / 2; i++)
    {
        if (nMax == i)
        {
            continue;//跳过原来最大值的下标,直接开始i+1的循环
        }
        if (a[i] > aSecondMax && a[i] > a[i + 1] && a[i] > a[i - 1])
        {
            aSecondMax = a[i];
            nSecondMax = i;
            fSecondMax = f[nSecondMax];
        }
    }
    for (i = 1; i < NUM_SAMPLES / 2; i++)
    {
        if (nMax == i || nSecondMax == i)
        {
            continue;//跳过原来最大值的下标,直接开始i+1的循环
        }
        if (a[i] > aThirdMax && a[i] > a[i + 1] && a[i] > a[i - 1])
        {
            aThirdMax = a[i];
            nThirdMax = i;
            fThirdMax = f[nThirdMax];
        }
    }
    for (i = 1; i < NUM_SAMPLES / 2; i++)
    {
        if (nMax == i || nSecondMax == i || nThirdMax == i)
        {
            continue;//跳过原来最大值的下标,直接开始i+1的循环
        }
        if (a[i] > aFourthMax && a[i] > a[i + 1] && a[i] > a[i - 1])
        {
            aFourthMax = a[i];
            nFourthMax = i;
            fFourthMax = f[nFourthMax];
        }
    }
    k = fabsf(2 * fMax - fSecondMax);
    k1 = fabsf(3 * fMax - fSecondMax);
    m = fabsf((float) (aMax - 3.0 * aSecondMax));
    //    if(k<=5)
    //        LCD_ShowString(275,230,12*4,12,12,"JvChi  ");
    //    else if(k1<=5&&m<0.4)
    //        LCD_ShowString(275,230,12*4,12,12,"Fang   ");
    //    else if(k1<=5&&m>=0.4)
    //        LCD_ShowString(275,230,12*4,12,12,"SanJiao");
    //    else LCD_ShowString(275,230,12*4,12,12,"Sin    ");
}
#endif

#endif

四、总结(吐槽)

        从STM32换成TI开发挺不习惯的,在STM32里可以各种手搓寄存器,结果到TI这里有一种处处受限的感觉。最直接的体现就是创建工程,充分让你体会到了什么叫不得自由,什么工程必须从模板库里创建否则用不了sysconfig(除非想与TI库绝缘)、想使用gcc编译器就必须下载9.2版本等等,稍微对工程做了一点点修改就直接崩溃了,还是修不好的那种。同时TI又不支持OpenOCD,想使用GDB但板子似乎只支持TI官方的调试工具(总之很难),导致CLion直接被禁。然后就是几天的挣扎,好不容易才得到一个可以运行、稍微符合开发习惯的工程,没敢改太多。

        然后就是TI的驱动库,虽然模板、文档很多,但是国内网上的教程过少(无论中英文),处处是坑。最让我无法理解的就是GPIO的配置,在stm32中使用寄存器配置GPIO时,只需要找到对应端口和引脚数就行,结果TI里还需要下面这个东西,what can I say?这除了sysconfig和直接查芯片,根本不知道啊

#define GPIO_OLED_DC_IOMUX (IOMUX_PINCM35)

         吐槽归吐槽,经过一段时间的熟悉后,CCS还是很好用的,比如那个图形化展示数据,分析信号时简直犹如神助。当初觉得难估计是从stm32猛地转ti导致的,反过来大概也是一样的吧。

        除此之外,再说一下浅显的个人经验,再使用sysconfig初始化工程时,由于习惯了stm32的开发,对于这种纯图形化反而不适应(与CubeMX不同),有时看着配置选项并不如直接看代码清晰,偏偏偏置选项和生成的代码还有些反差萌。所以和之前使用CubeMX一样,只是把它当做一个学习的工具:

        导入示例工程,咔咔乱改,生成符合需求的可用代码,把代码添加到自己新建的文件中,并标上注释,一气呵成。

        就比如下面的SPI.c/h文件:

//
// Created by 34753 on 2024/7/20.
//

#ifndef ISB_TI_SPI_H
#define ISB_TI_SPI_H
#include "ZQ_Conf.h"
#if USE_SPI
#include "ti_init.h"

#define SPI_0_INST SPI1


void SPI_Init(void);
void SPI_DMA_Init(void);/*不可用*/
void SPI_DMA_Send(uint8_t *data, uint16_t len);/*不可用*/
void SPI_DMA_Repeat_SendWord(uint16_t data, uint16_t times);/**不可用*/
void SPI_DMA_WriteByte(uint8_t data);

__STATIC_INLINE void SPI_WriteByte(uint8_t data)
{
    while (DL_SPI_isBusy(SPI_0_INST))
        ;
    DL_SPI_transmitData8(SPI_0_INST, data);
}

//void SPI_WriteWord(uint16_t data);
void SPI_SendData(uint8_t *data, uint16_t len);
#endif
#endif//ISB_TI_SPI_H
//
// Created by 34753 on 2024/7/20.
//

#include "SPI.h"
#if USE_SPI
#include "LED.h"

/* Defines for SPI_0 */

#define SPI_0_INST_IRQHandler SPI1_IRQHandler
#define SPI_0_INST_INT_IRQN SPI1_INT_IRQn
#define GPIO_SPI_0_PICO_PORT GPIOB
#define GPIO_SPI_0_PICO_PIN DL_GPIO_PIN_8
#define GPIO_SPI_0_IOMUX_PICO (IOMUX_PINCM25)
#define GPIO_SPI_0_IOMUX_PICO_FUNC IOMUX_PINCM25_PF_SPI1_PICO
#define GPIO_SPI_0_POCI_PORT GPIOB
#define GPIO_SPI_0_POCI_PIN DL_GPIO_PIN_7
#define GPIO_SPI_0_IOMUX_POCI (IOMUX_PINCM24)
#define GPIO_SPI_0_IOMUX_POCI_FUNC IOMUX_PINCM24_PF_SPI1_POCI

/* GPIO configuration for SPI_0 */
#define GPIO_SPI_0_SCLK_PORT GPIOB
#define GPIO_SPI_0_SCLK_PIN DL_GPIO_PIN_16
#define GPIO_SPI_0_IOMUX_SCLK (IOMUX_PINCM33)
#define GPIO_SPI_0_IOMUX_SCLK_FUNC IOMUX_PINCM33_PF_SPI1_SCLK

#define GPIO_SPI_0_CS1_PORT GPIOB
#define GPIO_SPI_0_CS1_PIN DL_GPIO_PIN_17
#define GPIO_SPI_0_IOMUX_CS1 (IOMUX_PINCM43)
#define GPIO_SPI_0_IOMUX_CS1_FUNC IOMUX_PINCM43_PF_SPI1_CS1_POCI1


/* Defines for DMA_CH1 */
#define DMA_CH1_CHAN_ID (1)
#define SPI_0_INST_DMA_TRIGGER (DMA_SPI1_TX_TRIG)

static volatile bool SPI_DMA_Mode_Flag = false;//默认模式


static const DL_SPI_Config gSPI_0_config = {
        .mode = DL_SPI_MODE_CONTROLLER,
        .frameFormat = DL_SPI_FRAME_FORMAT_MOTO4_POL1_PHA1,
        .parity = DL_SPI_PARITY_NONE,
        .dataSize = DL_SPI_DATA_SIZE_8,
        .bitOrder = DL_SPI_BIT_ORDER_MSB_FIRST,
        .chipSelectPin = DL_SPI_CHIP_SELECT_1,
};

static const DL_SPI_ClockConfig gSPI_0_clockConfig = {
        .clockSel = DL_SPI_CLOCK_BUSCLK,
        .divideRatio = DL_SPI_CLOCK_DIVIDE_RATIO_1};


static const DL_DMA_Config gDMA_CH1Config = {
        .transferMode = DL_DMA_SINGLE_TRANSFER_MODE,//不可使用RepeatSingle,否则会出现乱波
        .extendedMode = DL_DMA_NORMAL_MODE,
        .destIncrement = DL_DMA_ADDR_UNCHANGED,
        .srcIncrement = DL_DMA_ADDR_INCREMENT,
        .destWidth = DL_DMA_WIDTH_BYTE,
        .srcWidth = DL_DMA_WIDTH_BYTE,
        .trigger = SPI_0_INST_DMA_TRIGGER,
        .triggerType = DL_DMA_TRIGGER_TYPE_EXTERNAL,
};


void SPI_Init(void)
{
    DL_SPI_reset(SPI_0_INST);
    DL_SPI_enablePower(SPI_0_INST);

    /**GPIO初始化*/
    DL_GPIO_initPeripheralOutputFunction(GPIO_SPI_0_IOMUX_SCLK, GPIO_SPI_0_IOMUX_SCLK_FUNC);
    DL_GPIO_initPeripheralOutputFunction(GPIO_SPI_0_IOMUX_PICO, GPIO_SPI_0_IOMUX_PICO_FUNC);
    DL_GPIO_initPeripheralOutputFunction(GPIO_SPI_0_IOMUX_CS1, GPIO_SPI_0_IOMUX_CS1_FUNC);
    DL_SPI_Config gSPI_0_config = {
            .mode = DL_SPI_MODE_CONTROLLER,
            .frameFormat = DL_SPI_FRAME_FORMAT_MOTO4_POL0_PHA0,
            .parity = DL_SPI_PARITY_NONE,
            .dataSize = DL_SPI_DATA_SIZE_8,
            .bitOrder = DL_SPI_BIT_ORDER_MSB_FIRST,
            .chipSelectPin = DL_SPI_CHIP_SELECT_1,
    };

    DL_SPI_setClockConfig(SPI_0_INST, (DL_SPI_ClockConfig *) &gSPI_0_clockConfig);

    DL_SPI_init(SPI_0_INST, &gSPI_0_config);

    /* Configure Controller mode */
    /*
     * Set the bit rate clock divider to generate the serial output clock
     *     outputBitRate = (spiInputClock) / ((1 + SCR) * 2)
     *     500000 = (32000000)/((1 + 31) * 2)
     */
    //  TFT_4SPI最快为  BUS_CLK:80MHz,不分频;SPI: 2分频(添1)
    //  OLED_4SPI最快为    BUS_CLK:80MHz,不分频;SPI: 4分频(添3)
    DL_SPI_setBitRateSerialClockDivider(SPI_0_INST, 3);
    /* Set RX and TX FIFO threshold levels */
    DL_SPI_setFIFOThreshold(SPI_0_INST, DL_SPI_RX_FIFO_LEVEL_1_2_FULL, DL_SPI_TX_FIFO_LEVEL_1_2_EMPTY);

    /* Enable module */
    DL_SPI_enable(SPI_0_INST);
}



//void SPI_WriteWord(uint16_t data)
//{
//    while (DL_SPI_isBusy(SPI_0_INST))
//        ;
//    DL_SPI_transmitData16(SPI_0_INST, data);
//}
void SPI_SendData(uint8_t *data, uint16_t len)
{
    uint16_t i = 0;
    for (i = 0; i < len; i++)
    {
        while (DL_SPI_isBusy(SPI_0_INST))
            ;
        DL_SPI_transmitData8(SPI_0_INST, data[i]);
    }
}

void SPI_DMA_Init(void)
{
#if !USE_TI_SYSCONFIG
    DL_SPI_reset(SPI_0_INST);
    DL_SPI_enablePower(SPI_0_INST);

    /**GPIO初始化*/
    DL_GPIO_initPeripheralOutputFunction(GPIO_SPI_0_IOMUX_SCLK, GPIO_SPI_0_IOMUX_SCLK_FUNC);
    DL_GPIO_initPeripheralOutputFunction(GPIO_SPI_0_IOMUX_PICO, GPIO_SPI_0_IOMUX_PICO_FUNC);
    DL_GPIO_initPeripheralInputFunction(GPIO_SPI_0_IOMUX_POCI, GPIO_SPI_0_IOMUX_POCI_FUNC);
    DL_GPIO_initPeripheralOutputFunction(GPIO_SPI_0_IOMUX_CS1, GPIO_SPI_0_IOMUX_CS1_FUNC);

    /**SPI初始化*/
    DL_SPI_setClockConfig(SPI_0_INST, (DL_SPI_ClockConfig *) &gSPI_0_clockConfig);

    DL_SPI_init(SPI_0_INST, (DL_SPI_Config *) &gSPI_0_config);

    /* Configure Controller mode */
    /*
     * Set the bit rate clock divider to generate the serial output clock
     *     outputBitRate = (spiInputClock) / ((1 + SCR) * 2)
     *     5000000 = (80000000)/((1 + 7) * 2)
     */
    DL_SPI_setBitRateSerialClockDivider(SPI_0_INST, 7);

    /* Enable SPI TX interrupt as a trigger for DMA */
    DL_SPI_enableDMATransmitEvent(SPI_0_INST);
    /* Set RX and TX FIFO threshold levels */
    DL_SPI_setFIFOThreshold(SPI_0_INST, DL_SPI_RX_FIFO_LEVEL_ONE_FRAME, DL_SPI_TX_FIFO_LEVEL_ONE_FRAME);
    DL_SPI_enableInterrupt(SPI_0_INST, (DL_SPI_INTERRUPT_DMA_DONE_TX |
                                        DL_SPI_INTERRUPT_TX_EMPTY));

    /* Enable module */
    DL_SPI_enable(SPI_0_INST);

    /**DMA初始化*/
    DL_DMA_initChannel(DMA, DMA_CH1_CHAN_ID, (DL_DMA_Config *) &gDMA_CH1Config);
#endif
    /**开启SPI中断*/
    NVIC_EnableIRQ(SPI_0_INST_INT_IRQN);
}

void SPI_DMA_Send(uint8_t *data, uint16_t len)
{
    DL_DMA_setSrcAddr(DMA, DMA_CH1_CHAN_ID, (uint32_t) data);
    DL_DMA_setDestAddr(DMA, DMA_CH1_CHAN_ID, (uint32_t) (&SPI_0_INST->TXDATA));
    DL_DMA_setTransferSize(DMA, DMA_CH1_CHAN_ID, len);

    DL_DMA_enableChannel(DMA, DMA_CH1_CHAN_ID);
}

void SPI_DMA_WriteByte(uint8_t data)
{
    DL_DMA_setSrcAddr(DMA, DMA_CH1_CHAN_ID, (uint32_t) data);
    DL_DMA_setDestAddr(DMA, DMA_CH1_CHAN_ID, (uint32_t) (&SPI_0_INST->TXDATA));
    DL_DMA_setTransferSize(DMA, DMA_CH1_CHAN_ID, 1);

    DL_DMA_enableChannel(DMA, DMA_CH1_CHAN_ID);
}

/**
 * @brief   重复传送某字节
 * @param data
 * @param times
 * @note    首先会把模式调为单次模式,然后再把数据写入DMA,最后再开启DMA传输结束后注意把模式切换回来。
 */
void SPI_DMA_Repeat_SendWord(uint16_t data, uint16_t times)
{
    DL_DMA_Config gDMA_CH1Config_temp = {
            .transferMode = DL_DMA_SINGLE_TRANSFER_MODE,//不可使用RepeatSingle,否则会出现乱波
            .extendedMode = DL_DMA_NORMAL_MODE,
            .destIncrement = DL_DMA_ADDR_UNCHANGED,
            .srcIncrement = DL_DMA_ADDR_UNCHANGED,
            .destWidth = DL_DMA_WIDTH_HALF_WORD,
            .srcWidth = DL_DMA_WIDTH_HALF_WORD,
            .trigger = SPI_0_INST_DMA_TRIGGER,
            .triggerType = DL_DMA_TRIGGER_TYPE_EXTERNAL,
    };
    SPI_DMA_Mode_Flag = true;//模式改变
    DL_DMA_initChannel(DMA, DMA_CH1_CHAN_ID, &gDMA_CH1Config_temp);

    DL_DMA_setSrcAddr(DMA, DMA_CH1_CHAN_ID, (uint32_t) &data);
    DL_DMA_setDestAddr(DMA, DMA_CH1_CHAN_ID, (uint32_t) (&SPI_0_INST->TXDATA));
    DL_DMA_setTransferSize(DMA, DMA_CH1_CHAN_ID, times);

    DL_DMA_enableChannel(DMA, DMA_CH1_CHAN_ID);
}

void SPI_RecoverMode(void)
{
    DL_DMA_initChannel(DMA, DMA_CH1_CHAN_ID, (DL_DMA_Config *) &gDMA_CH1Config);
}

void SPI_0_INST_IRQHandler(void)
{
    switch (DL_SPI_getPendingInterrupt(SPI_0_INST))
    {
        case DL_SPI_IIDX_DMA_DONE_TX:
            LED_OFF(LED2_Green);
            LED_ON(LED2_Red);
            break;

        case DL_SPI_IIDX_TX_EMPTY:
            //重置DMA模式
            if (SPI_DMA_Mode_Flag)
                SPI_RecoverMode();
            LED_OFF(LED2_Red);
            break;

        default:
            break;
    }
}

#endif

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1943456.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

7.23模拟赛总结 [数据结构优化dp] + [神奇建图]

目录 复盘题解T2T4 复盘 浅复盘下吧… 7:40 开题 看 T1 &#xff0c;起初以为和以前某道题有点像&#xff0c;子序列划分&#xff0c;注意到状态数很少&#xff0c;搜出来所有状态然后 dp&#xff0c;然后发现这个 T1 和那个毛关系没有 浏览了一下&#xff0c;感觉 T2 题面…

前端(1)HTML

1、标签 创建1.html文件&#xff0c;浏览器输入E:/frontheima/1.html&#xff0c;可以访问页面 页面展示 在VSCODE安装IDEA的快捷键&#xff0c;比如ctld复制一行、ctrlx剪切 <p id"p1" title"标题1">Hello,world!</p> <p id"p2"…

Java | Leetcode Java题解之第268题丢失的数字

题目&#xff1a; 题解&#xff1a; class Solution {public int missingNumber(int[] nums) {int n nums.length;int total n * (n 1) / 2;int arrSum 0;for (int i 0; i < n; i) {arrSum nums[i];}return total - arrSum;} }

[排序]hoare快速排序

今天我们继续来讲排序部分&#xff0c;顾名思义&#xff0c;快速排序是一种特别高效的排序方法&#xff0c;在C语言中qsort函数&#xff0c;底层便是用快排所实现的&#xff0c;快排适用于各个项目中&#xff0c;特别的实用&#xff0c;下面我们就由浅入深的全面刨析快速排序。…

PHP接入consul,注册服务和发现服务【学习笔记】

PHP接入consul,注册服务和发现服务 consul安装 链接: consul安装 启动consul C:\Users\14684>consul agent -dev安装TP5 composer create-project topthink/think5.0.* tp5_pro --prefer-dist配置consul 创建tp5_pro/application/service/Consul.php <?php /*****…

环境变量配置文件中两种路径添加方式

环境变量配置文件中两种路径添加方式 文章目录 环境变量配置文件中两种路径添加方式代码示例区别和作用 代码示例 export HBASE_HOME/opt/software/hbase-2.3.5 export PATH$PATH:$HBASE_HOME/binexport SPARK_HOME/opt/software/spark-3.1.2 export PATH$SPARK_HOME/bin:$PAT…

【MySQL是怎样运行的 | 第一篇】Explain执行计划上

文章目录 1.查询优化-Explain语句详解上1.1前言1.2执行计划输出各列详解1.2.1 table1.2.2 id1.2.3 select_type1.2.4 partitions1.2.5 type1.2.6 possible_keys和key1.2.7 key_len1.2.8 ref1.2.9 rows 世纪晚霞 1.查询优化-Explain语句详解上 1.1前言 一条查询语句在经过 MyS…

在 CentOS 7 上安装 Docker 并安装和部署 .NET Core 3.1

1. 安装 Docker 步骤 1.1&#xff1a;更新包索引并安装依赖包 先安装yum的扩展&#xff0c;yum-utils提供了一些额外的工具&#xff0c;这些工具可以执行比基本yum命令更复杂的任务 sudo yum install -y yum-utils sudo yum update -y #更新系统上已安装的所有软件包到最新…

视频分帧【截取图片】(YOLO目标检测【生成数据集】)

高效率制作数据集【按这个流程走&#xff0c;速度很顶】 本次制作&#xff0c;1059张图片【马路上流动车辆】 几乎就是全自动了&#xff0c;只要视频拍得好&#xff0c;YOLO辅助制作数据集就效率极高 视频中的图片抽取&#xff1a; 【由于视频内存过大&#xff0c;遇到报错执行…

2024导游资格考试,这些材料提前准备✅

2024年导游考试报名本月开始&#xff01; &#x1f499;大家提前准备好报名材料 1、个人近期白底1寸证件照。 2、身份证照片 3、学历照片 4、健康证明或健康承诺书 5、其他需要上传的材料 &#x1f499;照片文件不通过原因汇总&#xff0c;记得避开这些坑&#xff01; &#x1…

网络驱动移植(RTL8189)

1、把驱动放到内核文件夹中&#xff08;linux/drivers/net/wireless&#xff09;&#xff0c;对应的驱动可以在网上下载 2、修改该目录下的Kconfig和Makefile文件 3、配置内核&#xff08;make menuconfig&#xff09; 配置支持IEEE 802.11&#xff0c;选中8189模块&#xff0…

程序员 被辞退后如何赚钱

为啥现在大厂大量裁员&#xff1f;35以上的程序员为啥不被认可&#xff0c;很难找工作&#xff1f; 技术更新换代迅速&#xff1a; 技术领域发展极快&#xff0c;新的编程语言、框架和技术不断涌现。如果大龄程序员未能及时学习新技术和跟上行业变化&#xff0c;可能会被新一代…

SAP第二季度财报和进一步裁员计划

7月22日公布了截至 2024 年 6 月 30 日的第二季度财务报告。以下位总体指标 当前云计算在手订单达 148 亿欧元&#xff0c;按名义货币和固定汇率计算均增长 28%云收入增长 25%&#xff0c;其中云 ERP 套件收入增长 33%&#xff0c;均按名义货币和固定汇率计算总收入增长 10%&a…

【数据结构初阶】复杂度

目录 一、时间复杂度 1、时间复杂度的概念 2、大O的渐进表示法 3、常见的时间复杂度计算举例 二、空间复杂度 1、空间复杂度的概念 2、常见的空间复杂度计算举例 三、常见复杂度对比 正文开始—— 前言 一个算法&#xff0c;并非越简洁越好&#xff0c;那该如何衡量一个算法…

Mamba-yolo|结合Mamba注意力机制的视觉检测

一、本文介绍 PDF地址&#xff1a;https://arxiv.org/pdf/2405.16605v1 代码地址&#xff1a;GitHub - LeapLabTHU/MLLA: Official repository of MLLA Demystify Mamba in Vision: A Linear AttentionPerspective一文中引入Baseline Mamba&#xff0c;指明Mamba在处理各种高…

零基础入门:创建一个简单的Python爬虫管理系统

摘要&#xff1a; 本文将手把手教你&#xff0c;从零开始构建一个简易的Python爬虫管理系统&#xff0c;无需编程基础&#xff0c;轻松掌握数据抓取技巧。通过实战演练&#xff0c;你将学会设置项目、编写基本爬虫代码、管理爬取任务与数据&#xff0c;为个人研究或企业需求奠…

回溯题目的套路总结

前言 昨天写完了LeeCode的7&#xff0c;8道回溯算法的题目&#xff0c;写一下总结&#xff0c;这类题目的共同特点就是暴力搜索问题&#xff0c;排列组合或者递归&#xff0c;枚举出所有可能的答案&#xff0c;思路很简单&#xff0c;实现起来的套路也很通用&#xff0c;一…

poi库简单使用(java如何实现动态替换模板Word内容)

目录 Blue留言&#xff1a; Blue的推荐&#xff1a; 什么是poi库&#xff1f; 实现动态替换 第一步&#xff1a;依赖 第二步&#xff1a;实现word模板中替换文字 模板word&#xff1a; 通过以下代码&#xff1a;&#xff08;自己建一个类&#xff0c;随意取名&#xf…

SpringBoot框架学习笔记(五):静态资源访问、Rest风格请求处理、配置视图解析器、接收参数的相关注解详解

1 WEB开发-静态资源访问 1.1 基本介绍 &#xff08;1&#xff09;只要静态资源放在类路径的以下目录&#xff1a;/static、/public、/resources、/META-INF/resources 可以被直接访问。maven项目的类路径即为main/resources目录--对应SpringBoot源码为WebProperties.java类 …

nginx如何开启优先访问压缩文件

nginx输出gzip有很多条件&#xff1a; 开启了gzip&#xff1a;gzip on;gzip_types定义了content-type&#xff0c;需要注意的是text/html是强制性的&#xff0c;不需要也不能再添加这个响应输出的content-type在gzip_types里输出的content-length大于等于nginx配置的gzip_min_…