简介:针对智能车竞赛备赛场景,这份源码与学习说明资料包提供了可直接使用的完整项目工程,适合计算机、数学、电子信息、自动化等专业的学生作为竞赛项目参考,也适合需要快速搭建智能车原型并深入理解实现细节的开发者。资料包共三百五十个文件,压缩后约五十点三二兆,内容以 C 源码和头文件为主,包含一百一十个头文件单元与七十六个 C 源文件,覆盖驱动、应用与算法模块;同时附有 xcl、pbi、icf 等工程配置与链接文件、eww、ewp 等集成开发环境工程,以及便于自动化管理的 bat 脚本和说明性 txt 文档,整体结构清晰,便于按模块查阅与二次开发。目前该资源已有五十二人学习浏览,可作为备赛起步阶段的参考依据。读者可从中获得完整赛题方案思路、底层驱动与上层应用代码的对照源码,以及基于 Kinetis 平台的工程配置经验;学习说明部分能帮助理解代码组织、外设调度与常见问题的排错逻辑,为后续结合自研算法和功能扩展打下基础。
1. 用SD卡日志破解智能车调参黑盒:从Kinetis K60到FatFs
智能车跑一圈,传感器全波形、控制量变化和速度曲线都在极短时间里发生,串口哪怕拉到921600也来不及回传完整过程。真正能用于赛后分析的方案,是把关键数据写到SD卡上,让车停下来之后再做离线回放。这个备赛源码包给的正是这样一套链路:以Kinetis K60主控(工程名里的FN15对应MK60FN1M0VLQ15,Cortex-M4内核,最高150MHz)为基础,IAR工程里包含了FatFs文件系统源码、SD卡应用层、OLED显示驱动和配套的调试批处理脚本。解压下来的zip直接打开就是一个可编译的完整工程,适合准备全国大学生智能车竞赛的学生把它当作学习资料,看懂代码结构后换成自己传感器的数据接口就能上车。
2. FatFs移植到Kinetis K60:ffconf配置与diskio底层对接
2.1 ff.c只做文件逻辑,寄存器差异全在diskio层
FatFs这套源码的职责划分很干净:ff.c实现FAT文件系统的目录项管理、簇链遍历、文件句柄和缓冲区逻辑,它看不到K60的SDHC寄存器,也不关心你用的是四线SDIO还是软件模拟SPI。真正和硬件绑定的部分集中在diskio.c里,只需要补上disk_initialize、disk_read、disk_write、disk_ioctl、disk_status和get_fattime这六个函数,ff.c就能在一个新主控上跑起来。
你拿到的这个包里,vcan_sd_app.c属于应用层,负责封装“初始化SD卡、挂载文件系统、打开文件、写入数据”这一串动作;ff.c是文件系统本体;而cc936.c、cc932.c这些是FatFs的选项模块,放在option目录下,编译时是否参与取决于ffconf.h里的FF_USE_LFN和FF_CODE_PAGE配置。学习的时候建议按这个层次拆着看,不要一上来就钻进ff.c的簇计算细节里。
2.2 底层回调与f_mount调用:先让文件系统跑起来
按K60最常见的接法,SD卡走SDHC外设的四线模式,核心板上的卡座把CMD、CLK、DATA0-3引到芯片对应引脚。底层初始化完成后,diskio里要做的事就是把SDHC驱动包装成FatFs能理解的回调,这里以读扇区为例:
DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { if (pdrv != DEV_SD) { return RES_PARERR; } if (SD_ReadBlocks(sector, buff, count) != SD_OK) { return RES_ERROR; } return RES_OK; }参数含义逐一说清楚:
- pdrv 是卷号,配合多卷使用时区分不同存储设备,这里只挂一个SD卡,固定传0即可。
- buff 是接收数据的缓冲区,SDHC外设做DMA传输时要求四字节对齐,最好声明为全局数组或用aligned属性修饰。
- sector 是逻辑块地址LBA,对应FAT文件系统里的簇号换算结果,磁盘第一个扇区是sector 0。
- count 是要连续读的扇区数,FatFs在读取大文件时一次可能请求几十个扇区,底层驱动必须支持多块读,不能只处理单块。
disk_initialize对应卡的上电时序和识别过程,如果SD_Init里已经完成SD卡命令初始化、读取CSD寄存器,这一层只需透传结果。disk_status返回STA_NOINIT、STA_PROTECT之类的状态位,FatFs挂载前会先查询它来判断卡是否就绪。
接下来是应用层的调用套路。常见做法是把挂载放在系统启动阶段:
FATFS g_fs; void sd_app_init(void) { FRESULT fr; fr = f_mount(&g_fs, "", 1); if (fr != FR_OK) { /* 挂载失败,说明卡未插好或分区格式不是FAT */ } }f_mount的第二个参数是挂载路径,字符串为空代表默认驱动器,也可以写成"0:";第三个参数opt传1表示立即挂载,传0则延迟到第一次访问文件时再挂载。挂载成功之后,读写文件就变成f_open、f_write、f_close三个标准调用的组合。
2.3 挂载失败时的排查顺序
很多备赛同学遇到的问题是能烧录但f_mount返回FR_DISK_ERR,这说明底层读指令已经发出,但返回的数据和FAT引导扇区对不上。常见原因有三个:
| 现象 | 直接原因 | 检查点 |
|---|---|---|
| FR_NOT_READY | disk_status返回未就绪 | 卡座供电是否3.3V,CMD上拉电阻是否焊接 |
| FR_DISK_ERR | 读MBR或引导扇区失败 | 用逻辑分析仪看CMD0/CMD1响应,确认卡检测引脚电平 |
| FR_NO_FILESYSTEM | 读到了扇区但不是FAT | 用读卡器把卡格式化成FAT32,而不是exFAT或NTFS |
第一项属于硬件问题,排查时要先确认SD_Init里有没有等待卡稳定。K60的SDHC初始化序列对时序要求比较严,上电后要给卡一段稳定时间,不能立刻发CMD0。
第二项最常见的是接线问题。如果核心板没有板载卡座而是自己飞线接模块,DATA线上的信号完整性和上拉电阻会直接影响多块读的稳定性。我一般会先降频测试,把SDHC时钟从25MHz降到10MHz,如果不再报错,就说明是布线或上拉导致的问题。
第三项需要检查卡的分区表。FatFs默认按FAT12/16/32识别,如果卡出厂是exFAT,需要把ffconf.h里的FF_FS_EXFAT设为1重新编译,或者干脆重新格式化成FAT32。竞赛场景用4GB到32GB的卡,格式化成FAT32最省事,簇大小保持默认4KB以上,文件系统开销可以忽略。
3. OLED显示与FatFs编码页:cc936/cc932/cc949/cc950的取舍
3.1 OLED.c的显存模型与字符绘制入口
OLED.c提供的是128×64分辨率的单色屏驱动,核心是一块128×8字节的显存缓冲,每个字节对应8个像素的列。绘制函数写入显存后,通过I2C或SPI把整块显存刷新到屏上。常见的函数分层是:OLED_Init初始化引脚、配置SSD1306控制器的显示时钟和对比度;OLED_Clear把显存清零;OLED_ShowChar按8×16或6×12的点阵字体画一个字符;OLED_ShowString循环调用ShowChar。
void OLED_ShowString(uint8_t x, uint8_t y, const int8_t *str) { while (*str != '\0') { OLED_ShowChar(x, y, *str); x += 8; if (x > 120) { x = 0; y += 2; } str++; } }x坐标按8像素对齐是因为一个字符占8像素宽;当x超过120就换行,y加2对应16像素高的字符在页地址模式下移动两页。如果要在OLED上显示浮点数,通常配合一个Format函数先转成字符串,再做逐字符输出。
3.2 四个代码页文件对应什么语言场景
cc936.c、cc932.c、cc949.c、cc950.c这四个文件都是FatFs在开启长文件名后需要的Unicode与OEM代码页双向转换表。文件系统内部处理文件名时使用Unicode,但FAT目录项里的字节序列必须按OEM代码页编码,所以f_open里传入的中文名要先经过这张转换表落到GBK或Big5字节。
各个文件的对应关系如下:
| 源文件 | 代码页 | 覆盖语言区域 | 典型用途 |
|---|---|---|---|
| cc936.c | 936 | 简体中文GBK | 国内竞赛最常见,日志名里写"第21届"这类中文 |
| cc932.c | 932 | 日文Shift-JIS | 从日版示例工程带过来的场景 |
| cc949.c | 949 | 韩文 | 韩版模块兼容 |
| cc950.c | 950 | 繁体中文Big5 | 繁体固件或港台地区设备 |
Flash空间紧张时这四个文件不能全编进去。每个表都有几十KB的转换数组,K60的1MB Flash看着富裕,但图像处理和控制算法库也会占用空间。我一般只保留cc936,其余三个从工程中移除,并把ffconf.h里的FF_CODE_PAGE固定为936,明确只支持简体中文。
移除之后有个副作用:如果SD卡目录里存在日文或韩文文件名,f_readdir返回的字符串会变成乱码或转换失败。对这个使用场景影响不大,因为日志文件本来就是程序自己命名,外部极少放其他语言的卡进来。
3.3 用f_readdir在OLED上列出中文日志文件
把f_opendir和f_readdir用起来,就能在OLED上看到SD卡里按日期生成的日志目录。先用f_mkdir创建一个以日期命名的目录,再把路径传给f_open写日志:
DIR dir; FILINFO fno; f_opendir(&dir, "0:/runlog"); for (;;) { if (f_readdir(&dir, &fno) != FR_OK || fno.fname[0] == 0) { break; } if (!(fno.fattrib & AM_DIR)) { OLED_ShowString(0, 0, (int8_t *)fno.fname); } } f_closedir(&dir);f_readdir每次返回一个目录项,长文件名放在fno.fname,短文件名放在fno.altname;当fname为空字符串的那一项表示目录遍历结束。判断fattrib的AM_DIR位可以区分目录和文件,避免把子目录名当成日志文件显示。
这里有一个容易踩的坑:如果FF_USE_LFN没打开,FILINFO里根本没有altname字段,fname只能放8.3短文件名,中文字符全变成问号。备赛源码里同时带cc936.c说明长文件名是开着的,但自己裁剪工程时如果把option文件删了,就得把FF_USE_LFN和FF_CODE_PAGE一起查一遍,否则文件名长度会被限制在8.3格式。
4. 从传感器到SD卡落盘:中断、DMA与f_write的时序编排
4.1 摄像头、编码器、电感:一个控制周期里的数据量
K60的FTM模块可以拿来做正交解码,编码器A、B相接在FTM的相位引脚上,计数器自动正反向增减,不需要CPU参与。摄像头的场中断和行中断把DMA搬运出来的图像行拼成完整帧,放在SRAM的帧缓冲里。电磁组的ADC采样用PDB定时触发,每500us扫一遍全部电感通道。
按典型配置估算一个控制周期的数据量:
| 数据源 | 单帧大小 | 产生频率 | 每秒字节 |
|---|---|---|---|
| 摄像头图像 | 188×120 | 50Hz | 约1.1MB |
| 编码器速度 | 8字节 | 1kHz | 8KB |
| 电感ADC值 | 24字节 | 1kHz | 24KB |
摄像头整帧1.1MB/s的写入量对FatFs来说不算大,但关键取决于写入是否连续。FAT文件系统在文件尾部追加写时,需要维护簇链和FAT表项;如果每写一次就调用f_open和f_close,文件系统元数据的更新开销会把实际吞吐打到很低。所以日志记录的核心原则是:数据先攒够一个块,再一次性写下去。
4.2 双缓冲日志缓冲区:把FatFs调用移出中断
f_write内部有自己的扇区缓冲,但调用后要等底层disk_write完成,这个时间在SD卡上可能是几百微秒到几毫秒。中断服务函数里做这种阻塞等待,会让控制周期变得不确定,直接破坏整车的稳定性。常见做法是双缓冲,中断只写内存,主循环检查到缓冲满了再落盘:
static uint8_t s_buf[2][512]; static volatile uint8_t s_cur = 0; static volatile uint16_t s_len = 0; void log_append(const uint8_t *data, uint16_t len) { memcpy(&s_buf[s_cur][s_len], data, len); s_len += len; if (s_len >= 512) { s_cur ^= 1; /* 切换缓冲区,通知主循环写盘 */ s_len = 0; } }变量s_cur的异或操作把写入指针切到另一块缓冲,主循环在后台把满了的缓冲用f_write写进日志文件。三个关键点:
第一,中断里只做memcpy,不做任何文件系统调用。f_open这类函数内部有状态切换和缓冲竞争,放进中断会明显增大响应抖动,还会和主循环的写盘操作互相干扰。第二,两块缓冲区交替使用,写入和落盘并行,等待SD卡响应的同时主循环不会丢数据。第三,512字节刚好等于SD卡一个扇区,整块写入能避免读-改-写带来的额外开销。
这样设计后,系统的时序预算大致如下:
| 阶段 | 耗时 | 说明 |
|---|---|---|
| 中断触发到缓冲写入 | 约2us | 512B的memcpy极快,必要时可用DMA代替 |
| SD卡多块写512B | 0.5-2ms | 取决于卡等级和SDHC时钟 |
| FAT表项更新 | 偶发10ms | 文件跨簇时才发生,不影响常规写入 |
4.3 PID参数与标定值用文本文件掉电保存
竞赛调车时,PID参数、摄像头阈值、速度环目标值都是反复试出来的。与其每次改代码烧写,不如在SD卡放一个param.txt,开机时读取覆盖默认值。用FatFs读配置文件比全片擦写FlexNVM方便,缺点是卡没插或文件损坏时参数会丢失。我一般在调车阶段用SD卡存参数,比赛前再把确定下来的参数写进代码里,避免现场因为卡接触不良导致参数加载失败。
配置解析直接逐行扫描:
int param_get_int(const char *key, int default_val) { FIL f; char line[64]; int val = default_val; if (f_open(&f, "0:param.txt", FA_READ) == FR_OK) { while (f_gets(line, sizeof(line), &f)) { if (sscanf(line, "%63[A-Za-z_]=%d", key, &val) == 2) { break; } } f_close(&f); } return val; }f_gets是FatFs提供的文本读取接口,底层自动处理换行符和EOF。sscanf的格式串限定在63个字符内,防止超长行溢出缓冲区。文本里每行写成"KP=12\n"这种格式,key和val之间不能有空格,解析最直接。如果后续要加入浮点参数,把%d换成%f,返回值类型改成float即可。
5. C-SPY批处理与日志后处理:调车效率最后一段路
5.1 把调试器做成命令行
vcan_Kinetis.FN15_Debug.cspy.bat 是IAR C-SPY调试器按当前调试配置导出的命令行脚本,里面记录了调试器类型、器件型号、flash loader路径和下载选项。双击它可以直接拉起调试会话,也可以在持续集成或批量烧录时把它当作命令来调用:
cspybat.exe -f vcan_Kinetis.FN15_Debug.cspy.bat -flash_downloadcspybat.exe位于IAR安装目录common/bin下。按我的使用习惯,赛前烧录十台备用板时会写一行for循环批量执行这个命令,省去逐台打开IDE点下载的时间。
5.2 构建产物清理与pbd.browse体积问题
vcan_Kinetis.pbd.browse 是IAR的浏览数据库,用于代码跳转和补全索引,不参与编译。跑一段时间后体积能涨到几百MB,提交代码或拷给队友时经常被它卡住。删除临时文件.bat的作用就是清掉这类中间产物:
@echo off del /s /q *.o *.out *.pbd.browse 2>nul rmdir /s /q Debug 2>nul echo clean done提示:.pbd.browse 删除后重新打开工程会自动重建,只是第一次代码跳转会比平时慢。删掉它不会影响固件编译结果。
5.3 用Python回放bin日志,量化转向滞后
线上跑完一圈后,把SD卡拔下来插读卡器,日志文件其实是前面代码里写的二进制帧:时间戳、速度、舵机占空比、中线误差各占固定字节。用Python解析比在OLED上一屏屏翻效率高得多:
import struct import matplotlib.pyplot as plt recs = [] with open("runlog.bin", "rb") as f: while chunk := f.read(16): t, speed, steer, err = struct.unpack("<Ihhh", chunk) recs.append((t, speed, steer, err)) t = [r[0] for r in recs] err = [r[3] for r in recs] steer = [r[2] for r in recs] plt.plot(t, err, label="center error") plt.plot(t, steer, label="steer pwm") plt.legend() plt.show()<Ihhh表示小端序16字节记录:uint32毫秒时间戳加三个int16数据。把中线误差和舵机输出两条曲线叠在一起,就能直观看出转向滞后是控制环参数没调好,还是舵机本身响应延迟。这个回放习惯比现场肉眼观察可靠得多,也方便赛后复盘把参数改动和曲线变化对应起来。
本文还有配套的精品资源,点击获取