嵌入式开发实战:SmartRF06EB BSP中SD卡与UART驱动API详解与避坑指南

嵌入式开发实战:SmartRF06EB BSP中SD卡与UART驱动API详解与避坑指南 1. 项目概述与核心价值在嵌入式开发领域尤其是物联网节点、无线传感器网络这类资源受限的设备上如何高效、可靠地管理本地数据和与外界通信是每个开发者都会遇到的经典问题。数据需要存储可能是传感器日志、配置参数甚至是固件升级包设备需要“说话”无论是通过串口打印调试信息还是与上位机进行数据交换。这时候SD卡和UART就成了最得力的左膀右臂。SD卡提供了大容量、可插拔的存储方案而UART则是嵌入式世界最古老也最可靠的“对话”通道。然而直接操作硬件寄存器来驱动这些外设对新手来说门槛不低对老手而言也是重复劳动。这正是板级支持包BSP的价值所在。它封装了底层硬件的复杂性提供一套清晰、统一的API让我们能像调用库函数一样操作硬件。今天我们就以德州仪器的经典无线开发平台——SmartRF06EB搭配CC2538无线微控制器为例深入拆解其BSP中SD卡通过SPI和UART背通道驱动的API设计与使用精髓。这份文档就像一份地图而我将结合自己多年在TI平台上的踩坑经验带你走通从初始化到数据读写的完整路径并分享那些数据手册里不会写的实战技巧。2. SD卡驱动SPI模式深度解析与实战SD卡驱动是许多数据采集项目的基石。SmartRF06EB BSP的SD卡驱动采用SPI模式进行通信这是一种在引脚资源紧张时的经典选择虽然速度不如SDIO模式但胜在接口简单、稳定可靠。2.1 驱动架构与初始化流程揭秘驱动文件主要位于bsp/srf06eb_cc2538/drivers/source/目录下核心是sdcard_srf06eb.c和sdcard.h。SPI模式的SD卡通信遵循一套标准的命令-响应协议。驱动层为我们隐藏了所有繁琐的细节例如命令发送CMD0, CMD8, ACMD41等、响应等待、CRC校验以及初始化的复杂时序。初始化的核心sdCardInit()这个函数是使用SD卡所有功能的“钥匙”。它的内部逻辑可以概括为以下几个关键阶段SPI低速模式探测首先以低速通常低于400kHz的SPI时钟向卡发送复位命令CMD0使卡进入SPI模式。这里有个关键点不同的卡SDSC, SDHC, SDXC在上电后的响应时间可能不同驱动内部需要包含足够长的延时和重试机制。电压范围校验发送CMD8命令查询卡支持的电压范围确保我们的MCU供电符合要求。卡初始化和容量识别通过ACMD41命令循环查询直到卡初始化完成。此过程会获取卡的操作条件寄存器OCR并由此判断卡的类型标准容量SDSC或高容量SDHC/SDXC。这是第一个容易出问题的地方如果电源不稳定或者PCB板上的上拉电阻不匹配卡可能永远无法跳出“busy”状态。读取CSD/CID寄存器初始化成功后驱动会读取卡特定数据CSD和卡识别CID寄存器。CSD寄存器包含了块大小、卡容量等关键信息CID则包含制造商ID、产品名等。实操心得关于sdCardInit的返回值永远不要假设sdCardInit()一次就能成功。在实际产品中尤其是电池供电设备上电瞬间电压可能有波动我的做法是在应用层用一个循环进行有限次重试比如3-5次。#define SD_INIT_RETRY_COUNT 5 uint8_t retry 0; uint8_t initStatus SDCARD_ERROR; while (retry SD_INIT_RETRY_COUNT initStatus ! SDCARD_SUCCESS) { initStatus sdCardInit(); if(initStatus ! SDCARD_SUCCESS) { // 可以在这里加入一个短暂的延时比如50ms delayMs(50); retry; } } if (initStatus ! SDCARD_SUCCESS) { // 初始化失败处理逻辑 }这种简单的重试机制能显著提升在复杂电磁环境或一般电源条件下的初始化成功率。2.2 数据读写API详解与底层机制初始化之后我们就可以进行核心的数据读写操作了。驱动提供了块读写接口这是与SD卡交互的主要方式。sdCardBlockRead(uint32_t ui32Block, uint8_t *pui8Buffer)功能从SD卡的指定逻辑块地址LBA读取一个块的数据到缓冲区。参数解析ui32Block是逻辑块号对于SDSC卡一个块通常是512字节地址是字节地址除以512对于SDHC/SDXC卡块地址就是直接寻址的块号。驱动内部会帮你处理这个转换这是API设计得很好的一个点对上层透明。底层操作函数内部会发送读单块命令CMD17然后等待数据起始令牌接着通过SPI接收512字节的数据和2字节的CRC。这里有一个重要的细节驱动是否校验CRC根据SD物理层规范在SPI模式下CRC是可以被禁用的通过CMD59。很多简易驱动为了速度会禁用CRC但这就牺牲了一些数据可靠性。我们需要查看驱动源码确认其行为。sdCardBlockWrite(uint32_t ui32Block, const uint8_t *pui8Buffer)功能将缓冲区中的数据写入SD卡的指定逻辑块。关键流程发送写块命令CMD24→ 发送数据起始令牌 → 发送512字节数据 → 发送2字节CRC如果启用→ 等待卡响应数据接收状态 → 等待卡完成编程操作SDCARD_BLOCKLENGTH定义了这个块长度固定为512。注意事项写操作比读操作耗时得多。卡在接收到数据后内部需要执行擦除和编程Flash存储单元的操作这段时间被称为“编程时间”。在此期间发送新的命令可能会失败。好的驱动会在sdCardBlockWrite内部通过发送“CMD13 - SEND_STATUS”来轮询卡是否忙直到写操作完成。如果驱动没有实现这个等待就需要应用层在每次写操作后主动延时延时时间可能从几毫秒到几百毫秒不等取决于卡的质量和磨损程度。2.3 信息获取与高级功能除了读写驱动还提供了一系列获取卡信息的函数这在创建文件系统或显示设备信息时非常有用。sdCardGetSize(void)返回卡的总容量单位是KiB (1024字节)。这个值是从CSD寄存器计算出来的。对于大于4GB的卡计算时要注意CSD版本CSD v1.0 和 v2.0 的容量计算公式不同。sdCardGetBlockSize(void)返回块大小固定为512字节。这是SD卡的标准块大小。sdCardGetCsd(uint8_t *pui8Csd)/sdCardGetCid(tSdCardCid *psCid)获取原始的CSD和CID寄存器数据。如果你想获取更详细的信息如制造商名称、产品版本、序列号、生产日期等就需要解析这些原始数据。tSdCardCid是一个结构体驱动头文件应该已经定义好了其成员如manfid,prod_name,serial等。sdCardGetStatusReg(uint8_t *pui8Buffer)读取SD卡状态寄存器。这个寄存器包含了卡当前状态、是否写保护、是否有错误等丰富信息可用于高级错误诊断。2.4 实战编程示例与避坑指南官方示例给出了一个最简框架但在实际项目中我们需要考虑更多。#include bsp.h #include sdcard.h // 定义读写缓冲区必须至少512字节对齐以提升访问效率某些架构要求 __attribute__((aligned(4))) static uint8_t s_readBuffer[SDCARD_BLOCKLENGTH]; __attribute__((aligned(4))) static uint8_t s_writeBuffer[SDCARD_BLOCKLENGTH]; bool sd_card_test(void) { // 1. 初始化SPI控制器这是前置条件驱动不负责 // 注意必须根据硬件连接正确配置SPI的引脚复用、时钟极性和相位(CPOL/CPHA) // SDSPI模式通常模式0 (CPOL0, CPHA0) 或模式3 (CPOL1, CPHA1) bspSpiInit(BSP_SPI_CLK_SPD); // 确保时钟速度在初始化阶段是低速 // 2. 初始化SD卡带重试机制 if(sdCardInit() ! SDCARD_SUCCESS) { // 初始化失败可能是卡未插入、损坏、或电源问题 // 可以尝试重新初始化SPI或检查硬件连接 return false; } // 3. 可选打印卡信息调试用 uint32_t size_kib sdCardGetSize(); printf(“SD Card Detected, Size: %lu KiB (~%lu MB)\n”, size_kib, (size_kib / 1024)); tSdCardCid cid; if(sdCardGetCid(cid) SDCARD_SUCCESS) { // 可以解析cid.manfid, cid.prod_name等 } // 4. 进行块读写测试 // 先读取块0通常包含MBR或引导扇区小心不要写坏它 if(sdCardBlockRead(0, s_readBuffer) ! SDCARD_SUCCESS) { printf(“Failed to read block 0\n”); return false; } // 假设我们想写入块1000确保这个地址在你的卡容量范围内且不是重要数据区域 // 填充测试数据 for(int i0; iSDCARD_BLOCKLENGTH; i) { s_writeBuffer[i] (uint8_t)(i 0xFF); } if(sdCardBlockWrite(1000, s_writeBuffer) ! SDCARD_SUCCESS) { printf(“Failed to write block 1000\n”); return false; } printf(“Write succeeded. Verifying...\n”); // 清空读缓冲区然后重新读取刚写入的块进行验证 memset(s_readBuffer, 0, SDCARD_BLOCKLENGTH); if(sdCardBlockRead(1000, s_readBuffer) ! SDCARD_SUCCESS) { printf(“Failed to read back block 1000\n”); return false; } // 内存比较验证 if(memcmp(s_writeBuffer, s_readBuffer, SDCARD_BLOCKLENGTH) 0) { printf(“Data verification PASSED!\n”); return true; } else { printf(“Data verification FAILED!\n”); return false; } }避坑技巧SPI时钟速度管理在sdCardInit()阶段SPI必须使用低速通常400kHz。初始化成功后为了提高读写性能我们可以将SPI时钟切换到高速模式例如CC2538的SPI可以跑到几MHz甚至更高。但是切换时序很重要。必须在卡完成初始化收到SDCARD_SUCCESS后且在发送任何后续读写命令之前调用BSP提供的SPI速度设置函数例如bspSpiSetSpeed如果存在或直接配置SPI时钟分频器。错误的时序可能导致通信失败。3. UART背通道驱动开发全攻略UART是嵌入式开发的“瑞士军刀”用于调试日志、命令行接口、与PC或其他微控制器通信。SmartRF06EB BSP的UART驱动设计为中断驱动、带缓冲区的形式这比轮询方式更高效能解放CPU。3.1 驱动模型与缓冲区管理核心这个驱动将UART操作抽象为三个层次配置层、数据传输层和工具层。其核心在于双缓冲区机制一个发送缓冲区TX Buffer和一个接收缓冲区RX Buffer。数据发送时应用将数据放入TX缓冲区驱动在中断服务程序ISR中自动将数据搬移到UART硬件发送寄存器数据接收时硬件收到字节触发中断ISR将字节从接收寄存器搬移到RX缓冲区应用再从RX缓冲区读取。初始化的黄金顺序bspUartBufInit-bspUartOpenbspUartBufInit(...)这是第一步也是很多新手会忽略其重要性的一步。你需要为驱动提供两个缓冲区数组及其大小。缓冲区大小选择这是一个权衡。缓冲区越大能缓存的数据越多越不容易丢失数据尤其是在高波特率下但消耗的RAM也越多。对于调试输出TX缓冲区128-256字节通常足够对于数据接收RX缓冲区大小取决于你一次期望处理的最大数据包长度。我的经验是RX缓冲区至少应能容纳两倍于最大预期数据包的长度以应对中断被临时关闭或处理不及时的情况。缓冲区生命周期传递给bspUartBufInit的缓冲区指针必须是全局变量或静态变量确保在UART整个使用周期内有效。绝不能使用栈上的局部数组。// 推荐的缓冲区定义方式 #define UART_TX_BUF_SIZE 256 #define UART_RX_BUF_SIZE 128 static uint8_t s_uartTxBuf[UART_TX_BUF_SIZE]; static uint8_t s_uartRxBuf[UART_RX_BUF_SIZE];bspUartOpen(uint32_t ui32BaudRate)配置UART硬件参数并开启串口。参数ui32BaudRate必须是预定义的枚举值之一如eBaudRate115200。这个函数内部会配置UART的波特率发生器、数据位、停止位并使能UART模块和接收中断如果使用了BSP_UART_ALLOCATE_ISR。3.2 数据收发API的阻塞与非阻塞行为数据收发是UART驱动的核心理解其行为模式至关重要。发送数据bspUartDataPut行为将数据从用户缓冲区复制到内部的TX缓冲区。如果TX缓冲区空间足够数据会全部被复制进去函数立即返回复制的字节数同时UART发送中断被激活开始在后台通过硬件发送数据。这是一种非阻塞操作。BSP_UART_ALL_OR_NOTHING宏的影响如果定义了这个宏函数行为变为“全有或全无”。只有当TX缓冲区有足够空间容纳全部请求发送的字节时才会执行复制并返回请求的长度否则一个字节也不复制直接返回0。这适用于需要保证数据包完整性的场景。未定义该宏时函数会尽可能多地复制数据直到缓冲区满返回实际复制的字节数这可能导致数据包被拆分。接收数据bspUartDataGet行为从内部的RX缓冲区复制数据到用户提供的缓冲区。同样受BSP_UART_ALL_OR_NOTHING宏影响。典型用法在应用的主循环中定期检查bspUartRxCharsAvail()当有足够数据例如收到一个完整的数据包结束符如\n时再调用bspUartDataGet读取。工具函数bspUartRxCharsAvail()查询RX缓冲区中可读的字节数。这是实现非阻塞接收的关键。bspUartTxSpaceAvail()查询TX缓冲区中剩余的空闲空间。在发送大量数据前检查一下可以避免bspUartDataPut立即返回0。bspUartFlushRx() / bspUartFlushTx()清空缓冲区。在协议解析错误或需要重新同步时非常有用。3.3 中断处理与系统集成这是驱动中最需要小心处理的部分。中断服务程序ISR注册 驱动提供了bspUartIsrHandler()函数来处理UART中断包括发送完成、接收就绪等。但是默认情况下驱动不会将这个函数挂载到MCU的UART中断向量上。你需要自己在应用代码中做这件事。例如对于CC2538你需要在中断向量表中注册UART中断并在对应的ISR中调用bspUartIsrHandler()。// 假设使用UART0 #pragma vector UART0_RX_TX_VECTOR __interrupt void uart0_isr(void) { bspUartIsrHandler(); // 调用BSP的UART中断处理函数 // 注意bspUartIsrHandler内部会清除已处理的中断标志 }简化方案使用BSP_UART_ALLOCATE_ISR如果你在包含bsp_uart.h前定义了BSP_UART_ALLOCATE_ISR宏驱动可能会在bspUartOpen函数内部自动完成中断向量的注册具体实现需查看驱动源码。这大大简化了开发但牺牲了一些灵活性例如你无法在同一个ISR中处理多个中断源。重要警告中断与缓冲区的线程安全这是一个高级但关键的话题。bspUartDataPut和bspUartDataGet这些函数会访问共享的缓冲区。如果它们在主循环中被调用同时UART中断bspUartIsrHandler也在修改这些缓冲区就可能发生数据竞争。简单的BSP驱动可能没有实现保护机制如关中断或使用互斥锁。安全的做法是在调用这些可能访问缓冲区的函数时临时关闭UART中断操作完成后再打开。或者确保你的数据收发操作都在同一个执行上下文例如都在主循环或都在一个低优先级任务中完成。3.4 完整实战示例实现一个简单的命令回显下面是一个比官方示例更完整、更健壮的UART应用示例它实现了命令接收和回显并包含了错误处理。#include bsp.h #include bsp_uart.h #include string.h // 用于memcpy, strlen // 定义宏让驱动自动分配ISR简化 #define BSP_UART_ALLOCATE_ISR // 定义缓冲区 #define TX_BUF_SIZE 128 #define RX_BUF_SIZE 64 static uint8_t s_txBuf[TX_BUF_SIZE]; static uint8_t s_rxBuf[RX_BUF_SIZE]; // 接收行缓冲区用于存储一行命令 static uint8_t s_cmdLine[RX_BUF_SIZE]; static uint16_t s_cmdIndex 0; void uart_echo_init(void) { // 1. 初始化缓冲区 if(bspUartBufInit(s_txBuf, TX_BUF_SIZE, s_rxBuf, RX_BUF_SIZE) ! BSP_UART_SUCCESS) { // 缓冲区初始化失败通常是参数错误如指针为NULL while(1); // 死循环实际项目中应触发错误恢复 } // 2. 打开UART波特率115200 if(bspUartOpen(eBaudRate115200) ! BSP_UART_SUCCESS) { // 打开失败可能是波特率不支持或硬件故障 while(1); } // 3. 发送欢迎信息非阻塞方式 const char *welcomeMsg “\r\nUART Echo Demo Ready.\r\n “; bspUartDataPut((uint8_t*)welcomeMsg, strlen(welcomeMsg)); } void uart_echo_process(void) { // 这个函数应被主循环定期调用 // 检查接收缓冲区是否有数据 uint16_t bytesAvailable bspUartRxCharsAvail(); if(bytesAvailable 0) { // 一次读取所有可用字节但不超过我们的命令行缓冲区剩余空间 uint16_t bytesToRead bytesAvailable; if((s_cmdIndex bytesToRead) (RX_BUF_SIZE - 1)) { bytesToRead (RX_BUF_SIZE - 1) - s_cmdIndex; } if(bytesToRead 0) { uint16_t bytesRead bspUartDataGet(s_cmdLine[s_cmdIndex], bytesToRead); s_cmdIndex bytesRead; // 检查是否收到换行符‘\n’或‘\r’表示命令结束 for(uint16_t i s_cmdIndex - bytesRead; i s_cmdIndex; i) { if(s_cmdLine[i] ‘\n’ || s_cmdLine[i] ‘\r’) { // 找到行结束符处理命令 s_cmdLine[i] ‘\0’; // 替换为字符串结束符 // 回显接收到的命令 bspUartDataPut((uint8_t*)“\r\nYou typed: “, 13); bspUartDataPut(s_cmdLine, strlen((char*)s_cmdLine)); bspUartDataPut((uint8_t*)“\r\n “, 4); // 重置命令行缓冲区 s_cmdIndex 0; memset(s_cmdLine, 0, sizeof(s_cmdLine)); break; // 处理完一行跳出循环 } } // 防止缓冲区溢出如果缓冲区快满了还没收到结束符清空并提示错误 if(s_cmdIndex (RX_BUF_SIZE - 1)) { bspUartDataPut((uint8_t*)“\r\nError: Command too long!\r\n “, 30); s_cmdIndex 0; memset(s_cmdLine, 0, sizeof(s_cmdLine)); bspUartFlushRx(); // 清空可能还在缓冲区的无效数据 } } } // 注意这里没有处理发送缓冲区满的情况。 // 在高速率或连续发送大量数据时应检查 bspUartTxSpaceAvail() // 如果空间不足需要等待或采用其他策略如丢弃数据、流控。 } int main(void) { // 硬件初始化时钟、GPIO等 bspInit(); // 初始化UART回显功能 uart_echo_init(); // 主循环 while(1) { // 处理UART数据 uart_echo_process(); // 这里可以执行其他任务... // 为了降低功耗可以在此处加入空闲延时或进入低功耗模式 // 但需确保UART中断能唤醒MCU } }4. 高级话题与疑难杂症排查在实际项目集成中仅仅调用API是不够的。你会遇到各种奇怪的问题下面是我总结的一些常见问题及其排查思路。4.1 SD卡驱动常见问题排查表问题现象可能原因排查步骤与解决方案sdCardInit()始终返回失败1. 硬件连接问题CS、CLK、MISO、MOSI、电源。2. SPI时钟相位/极性(CPHA/CPOL)设置错误。3. 上电时序或电源不稳。4. 卡不支持SPI模式或已损坏。1. 用示波器或逻辑分析仪检查SPI四根线在初始化阶段的波形。确认CS线在通信期间为低电平。2. SD卡SPI模式通常为模式0 (CPOL0, CPHA0) 或模式3 (CPOL1, CPHA1)。尝试切换。3. 确保在发送CMD0前有至少74个时钟周期的延时驱动应已处理。检查电源电压是否在2.7V-3.6V之间且纹波小。4. 换一张已知好的SD卡最好是不同品牌、小容量测试。可以初始化但读写数据失败1. SPI时钟速度在初始化后未切换到高速模式。2. 块地址计算错误SDSC vs SDHC。3. 写操作后未等待卡编程完成。4. 文件系统冲突卡已被格式化为文件系统。1. 确认在sdCardInit成功后调用了设置高速SPI时钟的函数。2. 确保你使用的块地址在卡容量范围内。对于SDSC卡地址是字节地址/512驱动通常已处理。3. 在sdCardBlockWrite后增加一个延时如100ms或检查驱动源码是否包含忙等待。4. 读写测试时避免使用已被PC格式化的卡的前几个扇区可能包含分区表。从靠后的扇区如第10000个块开始测试。读写数据不稳定偶尔出错1. SPI总线受到干扰长导线、无屏蔽。2. 电源噪声。3. 中断干扰了SPI通信时序。4. 缓冲区地址未对齐某些MCU要求4字节对齐。1. 缩短连接线增加上拉电阻通常在10k-100kΩ。2. 在SD卡电源引脚就近放置一个10uF和一个0.1uF的电容。3. 在SPI通信的关键序列发送命令、接收数据期间临时关闭全局中断。4. 使用__attribute__((aligned(4)))定义读写缓冲区。4.2 UART驱动常见问题排查表问题现象可能原因排查步骤与解决方案发送数据正常但接收不到任何数据1. RX引脚连接错误或配置错误。2. 中断未正确使能或中断服务程序未注册。3. 波特率不匹配。4. 接收缓冲区溢出数据被覆盖。1. 用示波器检查RX引脚是否有数据波形。确认MCU的UART RX引脚与USB转串口芯片的TX引脚相连。2. 确认在bspUartOpen后UART接收中断已使能。检查中断向量表是否正确指向了包含bspUartIsrHandler的ISR。3. 精确计算波特率分频值确保MCU系统时钟与bspUartOpen传入的波特率枚举值匹配。4. 增大RX缓冲区大小或提高应用层读取数据的频率。接收数据出现乱码或断帧1. 波特率轻微不匹配时钟误差累积。2. 中断响应延迟导致数据丢失。3. 流控未启用上位机发送过快。4. 地线噪声或共问题。1. 检查MCU系统时钟精度晶振误差。尝试降低波特率测试如从115200降到57600。2. 避免在UART中断服务程序中执行耗时操作。确保中断优先级设置合理不被其他高优先级中断长时间阻塞。3. 如果数据量很大考虑启用硬件流控RTS/CTS但BSP驱动可能不支持。替代方案使用软件流控XON/XOFF或在应用层实现确认机制。4. 确保MCU与通信对方如PC有良好的共地。使用屏蔽线缆。bspUartDataPut返回的字节数少于预期1. 发送缓冲区空间不足。2. 定义了BSP_UART_ALL_OR_NOTHING宏且缓冲区空间不足以容纳全部数据。1. 在调用bspUartDataPut前先检查bspUartTxSpaceAvail()。2. 如果必须保证数据包完整发送可以循环调用bspUartDataPut直到所有数据发送完毕或者增大TX缓冲区。系统运行一段时间后UART死锁1. 中断服务程序未正确清除中断标志导致反复进入中断。2. 缓冲区管理出现逻辑错误导致读写指针混乱。3. 内存越界破坏了缓冲区或驱动控制结构。1. 确认bspUartIsrHandler被正确调用并且驱动内部清除了所有已处理的中断标志。2. 检查应用层代码确保没有在中断上下文和主循环中同时并发调用bspUartDataGet/Put而未加保护。考虑临时关中断。3. 使用调试器或添加哨兵值检查缓冲区边界。4.3 性能优化与资源权衡在资源紧张的嵌入式系统中驱动配置需要权衡。SD卡驱动如果项目只读不写或者写操作频率极低可以考虑在初始化后不切换SPI到最高速以降低功耗和EMI。如果对写速度要求高除了提高SPI时钟还可以实现多块写入命令CMD25但BSP驱动可能未提供此接口需要自己扩展。UART驱动中断驱动的UART虽然高效但每个字节的收发都会产生一次中断。在超高波特率如921600下中断频率可能成为系统负担。此时可以考虑使用DMA进行UART数据传输但这超出了此BSP驱动的范畴需要修改底层驱动或使用更高级的HAL库。4.4 与RTOS集成在实时操作系统如FreeRTOS、TI-RTOS中使用这些驱动时需要特别注意同步问题。信号量/队列可以将UART驱动包装成一个任务。接收任务阻塞在一个信号量或队列上当bspUartRxCharsAvail()指示有数据时由中断服务程序或一个高优先级任务释放信号量接收任务再读取数据。发送亦然。互斥锁确保对UART API的调用是线程安全的。如果多个任务都可能调用bspUartDataPut需要使用互斥锁mutex保护。SD卡访问SD卡的读写操作是相对慢速的阻塞操作。在RTOS中应将SD卡访问放在一个独立的、较低优先级的任务中避免阻塞高优先级任务如网络响应、传感器采样。对SD卡驱动函数的调用也需要用互斥锁保护因为SPI总线是共享资源。我个人在多个基于CC2538和SmartRF06EB的Zigbee网关项目中正是通过这样深入理解BSP驱动、妥善处理中断与缓冲区、并做好错误处理和资源管理才使得设备的本地数据存储和远程调试通信变得稳定可靠。驱动是基石理解其原理和细节才能在上面构建出稳固的应用大厦。