拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32F407智能报警系统毕设工程详解:从RCC到CAN的外设协同

简介面向计算机科学与技术、电子信息工程等专业的毕设或课程作业这是一套完整的STM32F407智能报警系统项目包涵盖从需求分析、硬件搭建到固件编写、系统联调与测试优化的全流程。项目以Cortex-M4内核的STM32F407为主控涉及传感器信号采集、阈值判断、蜂鸣器/LED报警触发及用户交互等核心功能适合需要参考嵌入式系统设计或快速搭建原型的学生。压缩包共191个文件约6.24MB以C语言源码52个h、51个c、Keil工程文件uvprojx/uvoptx及编译生成的hex、axf、map等调试文件为主另有LCD驱动、STM32标准外设库和批处理脚本便于直接查看、编译与烧录验证。目前已有89人学习下载。通过这份资源读者可以拿到完整工程结构、核心驱动代码和报警逻辑实现理解HAL库/标准库调用、时钟及外设配置方式并参考实物联调与排错思路为独立完成同类设计提供扎实基础。1. STM32F407 智能报警系统从工程文件看这类毕设的真实构成压缩包解开来先看到的不是 main.c而是一串 stm32f4xx_tim.c、stm32f4xx_rtc.c、stm32f4xx_rcc.c、lcd.c、stm32f4xx_adc.c、stm32f4xx_flash.c、stm32f4xx_can.c 这样的外设驱动文件。这说明项目把最花时间的部分落在底层时钟树怎么配、ADC 怎么走 DMA、RTC 怎么打时间戳、CAN 怎么发报文、Flash 怎么存报警记录全都单独拆成了模块。报警主流程反而通常写在 main.c 里把传感器读数、阈值比较、蜂鸣器和 LCD 显示串起来。对于电子信息工程、计算机类毕设来说价值就在这个地方一片 STM32F407 同时处理模拟量采集、人机交互和远程通信每一段驱动都能抽出来复用到下个工程。想从点灯过渡到外设协同的嵌入式开发者以及正在做 STM32 智能报警系统课程设计的人这份源码的参考意义比单纯看一块开发板例程大得多。2. 报警系统的底层顺序RCC 时钟、TIM 定时与 RTC 时间戳智能报警系统上电后CPU 真正跑业务逻辑之前必须先解决三件事系统时钟是否稳定、定时器是否按预期产生中断、RTC 是否走了正确时间。工程里的 stm32f4xx_rcc.c、stm32f4xx_tim.c、stm32f4xx_rtc.c 恰恰对应这三件事。它们看着像“标准外设库的搬运工”实际上决定报警系统后续所有模块的协同基础。2.1 先看 RCC外设时钟不是默认全开的STM32F407 上电后默认使用内部 HSI 时钟外设时钟大部分是关闭的。如果一上来就操作 GPIO、ADC、CAN寄存器写进去也不生效因为对应的总线时钟门控还没打开。所以报警系统的初始化顺序永远先是 RCC再谈其他。这套工程用的是标准外设库文件命名和正点原子、野火的早期例程一致结构比 HAL 库更直白适合毕设阅读和答辩时讲流程。以外部 8MHz 晶振为例标准外设库下的系统时钟配置通常长这样void Stm32_Clock_Init(u32 pll_m, u32 pll_n, u32 pll_p, u32 pll_q) { RCC_DeInit(); RCC_HSEConfig(RCC_HSE_ON); // 打开外部高速晶振 while (RCC_WaitForHSEStartUp() ! SUCCESS); // 等待起振稳定 RCC_PLLConfig(RCC_PLLSource_HSE, pll_m, pll_n, pll_p, pll_q); RCC_PLLCmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) RESET); // 等 PLL 锁定 RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); // 主时钟切到 PLL RCC_HCLKConfig(RCC_SYSCLK_Div1); // AHB 168MHz RCC_PCLK1Config(RCC_HCLK_Div4); // APB1 42MHz RCC_PCLK2Config(RCC_HCLK_Div2); // APB2 84MHz }调用时传(8, 336, RCC_PLLP_Div2, 7)相当于把外部 8MHz 经过 PLL 倍频到 168MHz。后续 TIM、CAN、ADC 的时钟频率都以这组分频为基准。比如 APB1 定时器时钟会自动倍频到 84MHzAPB2 定时器时钟到 168MHzCAN 和 UART 挂在 APB1 上ADC 挂在 APB2 上。这类毕设最常见的翻车点就是 PLL 没配好LED 不闪、LCD 白屏先看系统时钟而不是先查外设初始化。总线分频输出频率挂载外设定时器时钟AHB1168MHzGPIO、DMA、Flash—APB1442MHzUART、I2C、CAN、DAC84MHzAPB2284MHzADC、SPI、TIM1/TIM8168MHz如果工程改用了 HAL 库SystemClock_Config()里对应的RCC_OscInitTypeDef也是干同一件事。看驱动时先确认是哪套库再决定查哪份参考手册。2.2 TIM 定时器决定采样周期和报警节奏工程里挂着 stm32f4xx_tim.c多数情况不是为了用 PWM 驱动一个无源蜂鸣器就是为了做周期性采样触发。智能报警系统需要对烟雾或温度传感器持续读取不能一个while(1)死循环无脑刷否则 ADC 值抖动、LCD 刷新频繁整个系统的实时性都会被拖垮。我一般会把 TIM2 配成 1ms 中断作为系统心跳再用计数变量派生 10ms、100ms 等事件void TIM2_Init(void) { TIM_TimeBaseInitTypeDef t; NVIC_InitTypeDef nvic; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); t.TIM_Prescaler 8400 - 1; // 84MHz / 8400 10kHz t.TIM_CounterMode TIM_CounterMode_Up; t.TIM_Period 10 - 1; // 10kHz / 10 1kHz即 1ms 中断 t.TIM_ClockDivision TIM_CKD_DIV1; t.TIM_RepetitionCounter 0; TIM_TimeBaseInit(TIM2, t); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); TIM_Cmd(TIM2, ENABLE); nvic.NVIC_IRQChannel TIM2_IRQn; nvic.NVIC_IRQChannelPreemptionPriority 1; nvic.NVIC_IRQChannelSubPriority 0; nvic.NVIC_IRQChannelCmd ENABLE; NVIC_Init(nvic); }预分频 8400 是因为这里假设 TIM2 挂在 APB1 定时器时钟 84MHz 上先分到 10kHz再在自动重装载里数 10 下得到 1kHz。如果要让蜂鸣器输出不同频率可以把 TIM 切到 PWM 模式修改TIM_Pulse和TIM_Period就能产生类似报警音的方波。注意有源蜂鸣器只需要电平驱动无源蜂鸣器必须靠 PWM 或翻转 GPIO 才能发声选型不同驱动代码差很多。2.3 RTC 给报警信息打时间戳报警系统不能只报“有火警”还要报“何时发生”。STM32F407 的 RTC 位于备份域靠 VBAT 引脚供电主电源掉电后如果能接纽扣电池时间仍然可以继续走。工程里出现 stm32f4xx_rtc.c说明作者至少考虑了时间戳功能。标准外设库下 RTC 初始化重点是先开 PWR 和备份域访问权限void RTC_Init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); PWR_BackupAccessCmd(ENABLE); RCC_BackupResetCmd(ENABLE); RCC_BackupResetCmd(DISABLE); RCC_LSEConfig(RCC_LSE_ON); // 外部 32.768kHz 晶振 while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) RESET); RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); RCC_RTCCLKCmd(ENABLE); RTC_WaitForSynchro(); }读取时间时标准库自带接口很直接RTC_TimeTypeDef rtc_time; RTC_GetTime(RTC_Format_BIN, rtc_time); printf(%02d:%02d:%02d\r\n, rtc_time.RTC_Hours, rtc_time.RTC_Minutes, rtc_time.RTC_Seconds);调试时如果发现时间不走优先查 LSE 是否起振再看备份域是否被复位过。很多同学在调试程序时反复按复位键备份域可能被代码里的RCC_BackupResetCmd(ENABLE)清掉时间就会归零这是 RTC 模块排错时最容易忽略的地方。3. ADC 采集、阈值状态机与 LCD 界面刷新报警系统真正做的事是把传感器电压变成数字量再拿数字量和阈值比较最后在 LCD 上显示状态。工程里的 stm32f4xx_adc.c 和 lcd.c 覆盖的就是这条数据链路。很多课程作业只做到“ADC 读到值串口打印出来”但一个能演示的毕设必须把数据显示到屏幕并带报警状态切换这一章就按这个完整链路展开。3.1 ADC 多通道采集与 DMA烟雾传感器、气体传感器、热敏电阻这类模拟量器件输出信号通常直接进 ADC 引脚。STM32F407 的 ADC 是 12 位量程 0-3.3V。如果接多个传感器最省 CPU 的做法是 ADC 扫描模式加 DMA转换结果自动搬到内存数组里主循环只读数组就够了。#define ADC_CH_NUM 3 u16 adc_buf[ADC_CH_NUM]; void ADC1_DMA_Init(void) { GPIO_InitTypeDef gpio; ADC_InitTypeDef adc; DMA_InitTypeDef dma; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA | RCC_AHB1Periph_DMA2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); gpio.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2; gpio.GPIO_Mode GPIO_Mode_AN; gpio.GPIO_PuPd GPIO_PuPd_NOPULL; GPIO_Init(GPIOA, gpio); adc.ADC_Resolution ADC_Resolution_12b; adc.ADC_ScanConvMode ENABLE; adc.ADC_ContinuousConvMode ENABLE; adc.ADC_ExternalTrigConvEdge ADC_ExternalTrigConvEdge_None; adc.ADC_DataAlign ADC_DataAlign_Right; adc.ADC_NbrOfConversion ADC_CH_NUM; ADC_Init(ADC1, adc); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_84Cycles); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_84Cycles); ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_84Cycles); dma.DMA_Channel DMA_Channel_0; dma.DMA_PeripheralBaseAddr (u32)ADC1-DR; dma.DMA_Memory0BaseAddr (u32)adc_buf; dma.DMA_DIR DMA_DIR_PeripheralToMemory; dma.DMA_BufferSize ADC_CH_NUM; dma.DMA_PeripheralInc DMA_PeripheralInc_Disable; dma.DMA_MemoryInc DMA_MemoryInc_Enable; dma.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; dma.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; dma.DMA_Mode DMA_Mode_Circular; dma.DMA_Priority DMA_Priority_High; DMA_Init(DMA2_Stream0, dma); DMA_Cmd(DMA2_Stream0, ENABLE); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_SoftwareStartConv(ADC1); }这段代码里有三个关键点。第一STM32F407 的 ADC1 DMA 请求固定在 DMA2 Stream0外设寄存器地址是ADC1-DR这个映射关系换型号就不一样。第二ADC_NbrOfConversion必须和ADC_RegularChannelConfig里设定的通道数量一致否则 DMA 搬运尺寸和转换序列对不上读到的数据位置会错位。第三ADC_SampleTime_84Cycles表示单次采样时间更长对阻抗较高的传感器更稳代价是转换速率变慢在报警系统里通常足够。有些同学的工程会把 DMA buffer 放在 STM32F407 的 CCM RAM 里以为速度快。这个思路本身没毛病但 CCM RAM 不能给 DMA 访问adc_buf一旦被放到 0x10000000 段DMA 搬运就会卡死或者数据全是 0。报警系统里凡是 DMA 要碰的 buffer都老老实实放普通 SRAM。3.2 LCD 显示与界面刷新工程里的 lcd.c 一般对应一块 TFT 彩屏可能走 FSMC 并行接口也可能走模拟 SPI。F407 接并口屏常用 FSMC把 LCD 当成外部存储器写速度快接 SPI 屏则更省引脚。不同屏初始化序列差异很大但上层显示函数基本一致最后都会落到填点和画色块上。一个简化的报警界面刷新不关心具体屏时序逻辑上是这样的void LCD_ShowAlarmPage(u8 alarm_flag, u16 adc_value) { LCD_Clear(WHITE); LCD_ShowString(10, 10, STM32F407 ALARM, RED, WHITE); LCD_ShowNum(10, 40, adc_value, 4, BLUE, WHITE); if (alarm_flag) LCD_ShowString(10, 70, ALARM!, RED, WHITE); else LCD_ShowString(10, 70, NORMAL, BLACK, WHITE); }实际工程里不会每毫秒刷一次屏因为 TFT LCD 写一整屏要好几毫秒主循环会被拖死。常见的做法是刷屏标志位比如在 TIM 中断里置一个lcd_refresh 1主循环检测到再调用LCD_ShowAlarmPage。如果这套 LCD 还带了电阻触摸屏网上常见的四点校准法也能用不过工程里没有 touch.c说明作者只做了显示这种更稳的主线功能答辩时也更容易跑通。3.3 阈值比较不是只写一个 if报警功能最怕误报。传感器输出有噪声采样值可能在阈值附近抖来抖去如果不加处理蜂鸣器会一阵一阵响。更稳的做法是加连续计数和滞回比较。我一般在主循环里放一个轻量状态机typedef enum { ST_IDLE 0, ST_ARMED, ST_ALARM } AlarmState; void Alarm_Task(void) { static AlarmState state ST_ARMED; static u8 confirm_cnt 0; u16 adc_now adc_buf[0]; switch (state) { case ST_ARMED: if (adc_now ALARM_THRESHOLD) { if (confirm_cnt 5) // 连续 5 次超过阈值才报警 { confirm_cnt 0; state ST_ALARM; Buzzer_On(); LCD_ShowAlarmPage(1, adc_now); } } else { confirm_cnt 0; } break; case ST_ALARM: if (adc_now ALARM_THRESHOLD - HYSTERESIS) { if (confirm_cnt 20) // 连续 20 次低于回差值才解除 { confirm_cnt 0; state ST_ARMED; Buzzer_Off(); LCD_ShowAlarmPage(0, adc_now); } } else { confirm_cnt 0; } break; } }这个状态机解决两件事。第一连续 5 次采样仍超阈值过滤掉单点毛刺第二报警解除时用ALARM_THRESHOLD - HYSTERESIS做滞回防止信号在阈值附近反复进入报警状态。阈值和滞回量怎么设取决于传感器在正常环境和报警环境下的电压范围。检查ADC 读数 × 3.3 / 4096可以换算出实际电压再根据传感器手册里的报警浓度对应电压来定阈值比拍脑袋写一个 3000 更有说服力答辩时老师一问也能答得上。状态进入条件离开条件输出ST_ARMED上电默认连续 5 次超阈值蜂鸣器关显示 NORMALST_ALARM连续 5 次超阈值连续 20 次低于阈值-滞回蜂鸣器开显示 ALARM4. 掉电保存与 CAN 远程报警Flash 和通讯模块的扩展报警系统如果在现场工作必须考虑掉电后参数还在、报警记录可查、远程能看到状态。工程里的 stm32f4xx_flash.c 和 stm32f4xx_can.c 正好对应这两个扩展点。这一章把它们展开顺便讲一下工程里那个 keilkilll.bat 是干什么用的。4.1 片内 Flash 保存阈值和报警日志STM32F407 的片内 Flash 有 1MB足以保存报警阈值、校准值和时间戳。用内部 Flash 的好处是不需要外挂 EEPROM坏处是擦除要按扇区来而且写入次数有限。工程里出现 stm32f4xx_flash.c我猜测它的用途就是存日志或阈值参数。写 Flash 的标准流程是三段式解锁、擦除扇区、编程。以下以扇区 11 为例#define ALARM_LOG_SECTOR 11 #define ALARM_LOG_ADDR 0x080E0000 typedef struct { u32 timestamp; u16 adc_value; u16 reserved; } AlarmLog_t; // 保证 4 字节对齐 void Flash_WriteLog(AlarmLog_t *log) { u32 *src; int i; FLASH_Unlock(); while (FLASH_GetStatus() FLASH_BUSY); // 等上次操作结束 FLASH_EraseSector(ALARM_LOG_SECTOR, VoltageRange_3); src (u32 *)log; for (i 0; i sizeof(AlarmLog_t) / 4; i) { FLASH_ProgramWord(ALARM_LOG_ADDR i * 4, src[i]); } FLASH_Lock(); }这里要注意sizeof(AlarmLog_t)必须是 4 的倍数否则按u32指针遍历会越界读取。其次擦除扇区期间不能发生中断里也去访问 Flash否则可能进入 Busy 死等甚至造成数据损坏。报警日志写入在代码里应该放在主循环或事件处理中绝对不要放在 RTC 中断里。调试时另一个坑是 Flash 写次数。毕设演示期间反复擦写同一个扇区短时间不明显但如果做成长期跑的系统建议用轮询方式分散写入区域。STM32F407 的 CCM RAM 和普通 SRAM 都可以给报警状态变量用但 Flash 缓冲区不能放 CCM RAM这点和 DMA buffer 的限制类似。4.2 CAN 总线远程报警和传感器节点级联工程里的 stm32f4xx_can.c 是这个报警系统里比较有意思的部分。智能报警系统如果只在一块板子上CAN 用处不明显但要是多个房间、多个检测点CAN 总线比串口更稳距离更远节点也能通过过滤器隔离无关帧。对于毕设来说CAN 很容易扩展成“一主多从”的演示场景。标准外设库下 CAN1 初始化需要把 PB8/PB9 复用为 CAN 引脚波特率由 APB1 时钟和分频参数共同决定。示例配置如下void CAN1_Init(void) { GPIO_InitTypeDef gpio; CAN_InitTypeDef can; CAN_FilterInitTypeDef filter; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); GPIO_PinAFConfig(GPIOB, GPIO_PinSource8, GPIO_AF_CAN1); GPIO_PinAFConfig(GPIOB, GPIO_PinSource9, GPIO_AF_CAN1); gpio.GPIO_Pin GPIO_Pin_8 | GPIO_Pin_9; gpio.GPIO_Mode GPIO_Mode_AF; gpio.GPIO_Speed GPIO_Speed_50MHz; gpio.GPIO_OType GPIO_OType_PP; gpio.GPIO_PuPd GPIO_PuPd_UP; GPIO_Init(GPIOB, gpio); can.CAN_TTCM DISABLE; can.CAN_ABOM ENABLE; can.CAN_AWUM DISABLE; can.CAN_NART DISABLE; can.CAN_RFLM DISABLE; can.CAN_TXFP ENABLE; can.CAN_Mode CAN_Mode_Normal; can.CAN_SJW CAN_SJW_1tq; can.CAN_BS1 CAN_BS1_8tq; can.CAN_BS2 CAN_BS2_7tq; can.CAN_Prescaler 6; CAN_Init(CAN1, can); filter.CAN_FilterNumber 0; filter.CAN_FilterMode CAN_FilterMode_IdMask; filter.CAN_FilterScale CAN_FilterScale_32bit; filter.CAN_FilterIdHigh 0x0000; filter.CAN_FilterIdLow 0x0000; filter.CAN_FilterMaskIdHigh 0x0000; filter.CAN_FilterMaskIdLow 0x0000; filter.CAN_FilterFIFOAssignment CAN_FIFO0; filter.CAN_FilterActivation ENABLE; CAN_FilterInit(filter); }APB1 时钟 42MHzPrescaler6后位时间 1 8 7 16 TQ最终波特率约 437.5Kbps能够满足大多数报警节点通信。发送报警帧时填充报文 ID 和数据然后CAN_TransmitCanTxMsg tx; u8 alarm_type 0x01; u16 adc_value adc_buf[0]; tx.ExtId 0x181; tx.IDE CAN_Id_Extended; tx.RTR CAN_RTR_Data; tx.DLC 5; tx.Data[0] alarm_type; tx.Data[1] adc_value 8; tx.Data[2] adc_value 0xFF; CAN_Transmit(CAN1, tx);CAN 通信调试时最容易忘的是终端电阻。两根 CAN 线要在总线两端各接一个 120Ω 终端电阻否则波形反射严重报文时通时断。另外滤波器如果设成全部屏蔽接收缓冲区会收到大量无关帧建议在裸机代码里先屏蔽所有 ID再逐个放行需要处理的报警 ID。4.3 工程维护keilkilll.bat 与中间文件压缩包里那个 keilkilll.bat 是作者用来清理 Keil 中间文件的。ST project 编译一次会产生 .o、.crf、.d、.htm、.dep、.axf 一类文件打包发网盘时体积会变大整理源码时也容易把垃圾文件代入。一个简单的清理脚本长这样echo off del /s /q *.o *.d *.crf *.htm *.dep *.lst *.tmp 2nul del /s /q Listings\*.* Objects\*.* 2nul rmdir /s /q Listings Objects 2nul echo Keil project cleaned.使用这段命令时要小心如果工程目录里只有一份源文件清理后重新编译没问题但如果.axf被误删调试器下一次下载会报找不到可执行文件在 Keil 里点一次 Rebuild 就好。另外.uvguix.Administrator是 Keil 编辑器窗口布局文件只记录打开哪些文件、断点位置不参与编译删了也不影响功能。5. 用 ITEMP.axf 反查工程编译调试中的三个实用技巧最后这一章不写大逻辑只写三个我拆这类 STM32F407 工程时实测过的技巧。拿到了别人编译好的 ITEMP.axf不一定要重新整个编译才能判断问题。第一个技巧是直接用 ARM 的 fromelf 工具反汇编 axf 文件确认当前程序烧录后执行路径是否符合预期。Keil 安装目录下自带 fromelf命令行可以这样用fromelf --text -c -d -o dis.txt ITEMP.axf打开 dis.txt 后搜索Alarm_Task、TIM2_IRQHandler这些函数名可以快速确认代码里有没有被编译器优化掉关键逻辑。比如报警状态机如果被整体优化成死代码反汇编里基本找不到对应的函数体再比如中断处理函数名写错向量表里没有入口反汇编里也能看到符号缺失。这种验证方式比在源码里找半天更快尤其适合拿到手的是已经编译过的工程。第二个技巧是调试 RTC 时间戳类功能时不要用复位键打断。STM32F407 的备份域可以在主电源复位后继续工作但工程初始化代码里如果执行了RCC_BackupResetCmd(ENABLE)每次上电都会把 RTC 时间清零。我会在调试阶段直接注释掉这一行再单独写一个RTC_SetTime设置好时间之后断点随便打时间戳都能连续。等到真正交作业或演示前再把备份域复位逻辑恢复回去保证掉电后时间不掉电不丢。第三个技巧和 Keil 工程清理有关。工程编译时如果报“Error: L6218E: Undefined symbol”先不要急着找源码大概率是某个.c文件没被加进工程或者keilkilll.bat把Listings和Objects目录删掉后编译配置里的输出路径不存在了。这时候在 Keil 里重新全量编译一次右键 Main Group选择 Add Existing Files to Group把不存在的 stm32f4xx_tim.c 等文件重新加回来。需要验证 axf 是否最新可以看 Keil 输出窗口里编译结束后那行Program Size: Code... RO-data...如果 Code 尺寸和源文件改动对不上说明编译不是增量完成的。我自己拆这种毕设工程时通常不会一上来就把所有代码通读一遍而是先看ITEMP.axf的时间戳和文件大小再打开.uvguix.Administrator看作者最后编辑过哪些文件然后从RCC → TIM → RTC → ADC → LCD → Flash → CAN的初始化顺序往下追。这套顺序本身就是智能报警系统的数据流也是报警触发链路的完整骨架。拿到手无需重新造轮子照着这个顺序逐层验证比从 main.c 死磕一行行读更省时间。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门