跳转至

结课设计

  • 本程序实现了一个较为完善的音乐盒程序,可以通过三按键切歌,并且可以使用数码管显示当前温度和光强。
  • 这份代码适用于SDCC工具链,与Keil C51不兼容。

程序代码

// main.c
#include <stc15.h>
#define ulong unsigned long
#define uint unsigned int
#define uchar unsigned char

#define CST_DIG_BEGIN 0 // 数码管扫描起始位
#define CST_DIG_END 7   // 数码管扫描结束位

/*---------全局变量定义---------*/
// 音乐播放相关
const uchar *music_track_ptr;   // 指向当前播放的乐谱数据
volatile uint music_current_note_idx;   // 当前乐谱中的音符索引
volatile uchar music_current_bpm;   // 当前乐曲的BPM
volatile uint music_tick_counter;   // Timer2节拍计数器
enum MusicPlayerState { // 音乐播放器状态
    MUSIC_STATE_INITIAL_PAUSE,  // 曲目开始前的长暂停
    MUSIC_STATE_PLAYING_NOTE,
    MUSIC_STATE_NOTE_PAUSE  // 音符之间的短暂停
};
volatile enum MusicPlayerState music_current_state;

// 数码管显示及ADC相关
uchar ucT0_ms_count = 0;    // Timer0中断计数

uchar ucDigtmp0 = 0;
uchar ucDigtmp1 = 0;
uchar ucDigtmp2 = 0;
uchar ucDigtmp3 = 0;
uchar ucDigtmp4 = 0;
uchar ucDigtmp5 = 0;
uchar ucDigtmp6 = 0;
uchar ucDigtmp7 = 0;

__bit adc_sample_state = 1; // ADC采样状态位 1:光, 0:温度

int  temperature_value = 0; // 温度值
uint temperature_abs_val;   // 温度绝对值
uint photo_adc_raw_val = 0; // 光敏ADC原始值

uchar temp_digit_100 = 0, temp_digit_10 = 0, temp_digit_1 = 0;
uchar photo_digit_100 = 0, photo_digit_10 = 0, photo_digit_1 = 0;

// 温度查表 (ADC值到实际温度的映射)
__code int arrThermLUT[] = {
    /* 内容此处省略 */
};

// Timer1重装载值,用于产生不同音高
__xdata uint reset_timer1_pitch_values[]= {
    /* 内容此处省略 */
    /* 音符索引对照表,由低音到高音升序
    C    C#   D    D#   E    F    F#   G    G#   A    A#   B
    1   ,2   ,3   ,4   ,5   ,6   ,7   ,8   ,9   ,10  ,11  ,12   1
    13  ,14  ,15  ,16  ,17  ,18  ,19  ,20  ,21  ,22  ,23  ,24   2
    25  ,26  ,27  ,28  ,29  ,30  ,31  ,32  ,33  ,34  ,35  ,36   3
    37  ,38  ,39  ,40  ,41  ,42  ,43  ,44  ,45  ,46  ,47  ,48   4
    49  ,50  ,51  ,52  ,53  ,54  ,55  ,56  ,57  ,58  ,59  ,60   5
    */
};

// 乐谱数据
__code uchar track1_data[]= {   // un secret
    190,    // BPM 每分钟节拍数
    35,2,   //乐谱 格式为一个音调加持续的时长的组合
    /* 内容此处省略 */
    0,0     // 停止标记
};

__code uchar track2_data[]= {   // 彩豆森林
    /* 内容此处省略 */
};

__code uchar track3_data[]= {   // mopemope
    /* 内容此处省略 */
};

/*--------- 音乐播放核心函数 ---------*/
void music_load_next_note_event(void);  // 前向声明
void music_init_track(const uchar* selected_track_data) {
    EA = 0; // 关键操作时关闭中断
    TR1 = 0;    // 确保Timer1停止发声
    music_track_ptr = selected_track_data;
    music_current_bpm = music_track_ptr[0];
    music_current_note_idx = 1; // 指向第一个音高数据
    music_current_state = MUSIC_STATE_INITIAL_PAUSE;
    music_tick_counter = 400;   // 曲目开始前的2秒暂停
    EA = 1;
}

// 根据当前乐谱位置加载音符或休止符信息,并设置播放状态和计时器
void music_load_next_note_event(void) {
    uchar pitch, duration_val;

    pitch = music_track_ptr[music_current_note_idx];
    duration_val = music_track_ptr[music_current_note_idx + 1];

    if (pitch == 0 && duration_val == 0) {  // 乐曲结束标记
        music_init_track(music_track_ptr);  // 重新播放当前曲目
        return;
    }

    // 设置Timer1以产生音高对应的方波 (T1CLKO)
    if (pitch != 0 && reset_timer1_pitch_values[pitch] != 0) {
        TH1 = (65536 - reset_timer1_pitch_values[pitch]) / 256;
        TL1 = (65536 - reset_timer1_pitch_values[pitch]) % 256;
        TR1 = 1;    // 启动Timer1发声
    } else TR1 = 0; // 休止符或无效音高,停止Timer1

    // 根据BPM和时值计算音符持续的Timer2节拍数
    // 2000为一个经验值,用标准值3000误差很明显
    music_tick_counter = 2000U / music_current_bpm * duration_val;
    music_current_note_idx += 2;    // 指向下一个音符的音高
    music_current_state = MUSIC_STATE_PLAYING_NOTE;
}

// Timer2中断服务程序,驱动音乐播放状态转换
void music_state_machine_tick(void) __interrupt(12) __using(0) {
    if (music_tick_counter > 0) music_tick_counter--;

    if (music_tick_counter == 0) {
        uchar duration_val_for_pause;
        switch (music_current_state) {
            case MUSIC_STATE_INITIAL_PAUSE: // 初始暂停结束
                music_load_next_note_event();
                break;

            case MUSIC_STATE_PLAYING_NOTE:  // 当前音符播放结束
                TR1 = 0;    // 停止发声
                music_current_state = MUSIC_STATE_NOTE_PAUSE;
                // 获取刚播放完音符的时值,用于计算音符后暂停的长度
                // music_current_note_idx 已经指向了下一个音符的音高,所以上一个音符的时值在 music_current_note_idx - 1
                duration_val_for_pause = music_track_ptr[music_current_note_idx - 1]; 

                // 检查是否是乐曲结束标记
                if (duration_val_for_pause == 0 && music_track_ptr[music_current_note_idx - 2] == 0) {
                    music_init_track(music_track_ptr);  // 重新开始
                    return;
                }

                // 根据BPM和时值计算音符后暂停的Timer2节拍数
                music_tick_counter = 1000U / music_current_bpm * duration_val_for_pause;

                break;

            case MUSIC_STATE_NOTE_PAUSE:    // 音符间暂停结束
                music_load_next_note_event();
        }
    }
}


/*---------数码管与发光二极管显示函数--------*/
void Seg7LedDisplay(uchar s, uchar e) {
    unsigned char arrSegSelect[] = {
        0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f, 0x40, 0x00
    };
    static uchar seg_scan_digit_idx = 0;    // 数码管位选扫描索引

    P2 = (P2 & 0xf0) | seg_scan_digit_idx;
    switch(seg_scan_digit_idx) {
        case 0: P0 = arrSegSelect[ucDigtmp0]; break;
        case 1: P0 = arrSegSelect[ucDigtmp1]; break;
        case 2: P0 = arrSegSelect[ucDigtmp2]; break;
        case 3: P0 = arrSegSelect[ucDigtmp3]; break;
        case 4: P0 = arrSegSelect[ucDigtmp4]; break;
        case 5: P0 = arrSegSelect[ucDigtmp5]; break;
        case 6: P0 = arrSegSelect[ucDigtmp6]; break;
        case 7: P0 = arrSegSelect[ucDigtmp7]; break;
        default: P0 = 0x00;
    }
    if(++seg_scan_digit_idx > e) seg_scan_digit_idx = s;
}

/*---------数码管显示数据更新函数--------*/
void Seg7LedUpdate(void) {
    if (temperature_value < 0) ucDigtmp0 = 10;  // '-'
    else ucDigtmp0 = temp_digit_100;
    ucDigtmp1 = temp_digit_10;
    ucDigtmp2 = temp_digit_1;
    ucDigtmp3 = 11; // 熄灭
    ucDigtmp4 = 11;
    ucDigtmp5 = photo_digit_100;
    ucDigtmp6 = photo_digit_10;
    ucDigtmp7 = photo_digit_1;
}

/*---------初始化ADC函数--------*/
void InitAdcTherm(void) {
    P1ASF = 0x18;
    ADC_RES = 0;
    ADC_RESL = 0;
    ADC_CONTR = 0x8b;   // P1.3 热敏电阻
    CLK_DIV = 0x20;
}
void InitAdcPhoto(void) {
    P1ASF = 0x18;
    ADC_RES = 0;
    ADC_RESL = 0;
    ADC_CONTR = 0x8c;   // P1.4 光敏电阻
    CLK_DIV = 0x20;
}

void InitADC_Alternating(void) {
    if(adc_sample_state) InitAdcPhoto();
    else InitAdcTherm();
    adc_sample_state = !adc_sample_state;
    EADC = 1;   // 允许ADC中断
    ADC_CONTR |= 0x08;  // ADC_START = 1, 启动ADC转换
}

/*---------数据转换子函数--------*/
void Convert_ThermToDigit(void) {   // 将温度信息按位转换
    if(temperature_value < 0) temperature_abs_val = -temperature_value;
    else temperature_abs_val = temperature_value;

    temp_digit_100 = temperature_abs_val / 100;
    temp_digit_10 = (temperature_abs_val % 100) / 10;
    temp_digit_1 = temperature_abs_val % 10;

    if (temperature_abs_val < 100) temp_digit_100 = 11; // 熄灭百位
    if (temperature_abs_val < 10 && temperature_value >=0) temp_digit_10 = 11;  // 熄灭十位
}
void Convert_PhotoToDigit(void) {   // 将光照信息按位转换
    photo_digit_100 = photo_adc_raw_val / 100;
    photo_digit_10 = (photo_adc_raw_val % 100) / 10;
    photo_digit_1 = photo_adc_raw_val % 10;

    if (photo_adc_raw_val < 100) photo_digit_100 = 11;
    if (photo_adc_raw_val < 10) photo_digit_10 = 11;
}

/*---------ADC中断服务函数--------*/
void ADC_Interrupt_Process(void) __interrupt(5) __using(2) {
    EA = 0; // 保护性关闭中断

    if(adc_sample_state == 0) { // 上次启动的是光敏 (adc_sample_state从1变为0) -> 这是光敏结果
        photo_adc_raw_val = (ADC_RES << 8) | ADC_RESL;
        Convert_PhotoToDigit();
    } else {    // 上次启动的是热敏 (adc_sample_state从0变为1) -> 这是热敏结果
        temperature_value = arrThermLUT[((ADC_RES << 8) | ADC_RESL) / 4 - 1];
        Convert_ThermToDigit();
    }
    ADC_CONTR &= ~0x10; // 清ADC_FLAG中断标志
    EA = 1; // 恢复中断
}

/*---------T0定时器中断服务处理函数--------*/
void Timer0_ISR_Display_ADC(void) __interrupt(1) __using(1) {
    TL0 = 0xCD; // 重装初值 for ~1ms @ 11.0592MHz/1T
    TH0 = 0xD4;

    ucT0_ms_count++;
    Seg7LedDisplay(CST_DIG_BEGIN, CST_DIG_END);

    if(!ucT0_ms_count) {    // 每溢出一次更新ADC和显示数据
        InitADC_Alternating();  // 启动下一次ADC采样 (温度/光照交替)
        Seg7LedUpdate();        // 更新数码管显示缓冲区
    }
}

/*---------主函数---------*/
void main(void) {
    // 硬件IO口模式初始化
    P0M1 = 0x00;
    P0M0 = 0xff;    // P0推挽输出 (数码管段选)
    P2M1 = 0x00;
    P2M0 = 0x08;    // P2.3推挽输出

    // Timer0 初始化 (用于数码管扫描和ADC触发)
    AUXR |= 0x80;   // Timer0/1工作在1T模式
    TL0 = 0xCD;     // ~1ms 定时
    TH0 = 0xD4;
    ET0 = 1;        // 使能Timer0中断
    TR0 = 1;        // 启动Timer0

    // Timer1 初始化 (用于音乐发声 T1CLKO)
    INT_CLKO |= (1<<1); // 使能T1CLKO输出到P3.4
    P3M0 |= (1<<4);     // P3.4设为推挽输出
    P3M1 &= ~(1<<4);

    // Timer2 初始化 (用于音乐节拍定时)
    AUXR |= 0x04;           // 定时器时钟1T模式
    T2L = 0x00;             // 设置定时初始值
    T2H = 0x28;             // 设置定时初始值
    AUXR |= 0x10;           // 定时器2开始计时
    IE2 |= 0x04;            // 使能定时器2中断

    EA = 1; // 打开总中断

    music_init_track(track1_data);

    while(1) {
        // 按键检测,用于切换曲目
        if (!P17) {         // K3 (P1.7)
            while(!P17); // 等待按键释放
            music_init_track(track1_data);
        } else if (!P33) {  // K2 (P3.3)
            while(!P33);
            music_init_track(track2_data);
        } else if (!P32) {  // K1 (P3.2)
            while(!P32);
            music_init_track(track3_data);
        }
    }
}