STM32智能药盒开发:从原理图到状态机与仿真完整实战 1. 项目概述很多人第一次接触STM32开发板做的不是流水灯就是温湿度计玩两天就吃灰了。去年我家里老人因为记性不好经常漏服降压药有时候上午吃了下午又吃一遍。我就在想能不能用手里这块吃灰的STM32F103C8T6做一套真正能落地的用药提醒设备于是就有了这个项目——一套完整的智能药盒/老人用药管理系统代码、原理图、仿真工程全部开源。这个项目的定位很清楚它不是那种只跑个流水灯的Demo而是把“定时提醒”“用药记录”“防误触”“低功耗待机”这些真实需求全部实现了的完整方案。你拿到的不是一堆零散代码而是从硬件原理图设计到Keil工程编译烧录再到Proteus仿真验证的一条龙资源包。它适合谁来参考正在准备STM32毕业设计的本科生这个项目覆盖面广写论文时“智能家居/医疗健康”方向站得住脚想给家里老人做点实物的嵌入式爱好者硬件成本压在50元以内焊接难度也不高想系统学习STM32“中断定时器低功耗外设驱动”这套组合拳的初学者这套代码比开发板自带的例程更接近真实产品逻辑。项目的核心功能拆开来看其实不复杂四个按键设置闹钟和药量RTC模块跑实时时钟到了设定时间就驱动蜂鸣器响铃、OLED弹窗显示“该吃药了”老人按下确认键后系统记录本次服药时间。整套逻辑用状态机管理没有操作系统代码量控制在2000行以内非常适合用来理解嵌入式程序的结构化设计思路。2. 硬件设计思路与原理图解读2.1 主控选型为什么是STM32F103C8T6而不是别的主控用的是蓝色药丸板上的那颗STM32F103C8T6Cortex-M3内核72MHz主频64KB Flash20KB SRAM。这个配置在今天看来不算是高性能但做药盒这个场景绰绰有余。更关键的是这颗芯片的生态太成熟了标准库资料遍地都是遇到问题随便一搜就有答案对初学者友好到近乎“溺爱”。为什么不选ESP32ESP32确实带WiFi可以把服药记录推送到手机但这个项目没有做云端同步加了无线模块只会徒增功耗和代码复杂度。为什么不选STM32L0系列超低功耗芯片L0系列省电是真的省但购买渠道没有F103那么普及而且Proteus仿真库里对L0的支持远不如F103完善。做项目最怕的不是功能做不出来而是环境搭不起来。F103C8T6是我反复权衡后认为“下限最高”的选择。2.2 模块级原理图拆解原理图的设计思路是“核心板外设模块”方案主控最小系统直接用现成的核心板不做底板这样省去了晶振电路和Boot引脚的麻烦。外设部分单独画了一块扩展板通过排针与核心板连接。电源部分用的是AMS1117-3.3稳压芯片输入支持5V/1A的Micro USB供电输出3.3V给主控和外设。板上预留了一个CR1220纽扣电池座给RTC模块提供备用电源。这里有个细节容易踩坑如果纽扣电池的型号选错了放电电流不够RTC时间在断电后会丢失。我用的是CR1220实测完全断电后维持RTC走时超过200小时足够应对短时间换电池的场景。显示模块用的是0.96寸I2C接口的SSD1306 OLED屏四根线搞定VCC、GND、SCL、SDA不占太多GPIO。I2C地址默认是0x3C如果你买到的模块地址是0x3D代码里只需要改一行宏定义就行。RTC模块使用的DS3231这是一颗带温度补偿的I2C实时时钟芯片年误差在±2分钟以内比DS1307那种外置晶振方案稳定太多。它的I2C地址是0x68注意和OLED的0x3C区分开总线挂两个设备只要地址不冲突就能共用一个I2C通道。人机交互部分用了四个独立按键分别对应“设置/确认”“加”“减”“消音/服药确认”用GPIO内部上拉模式按键按下时引脚电平被拉低通过轮询加去抖的方式处理没有用外部中断逻辑更简单可控。提醒输出部分有两个一个是有源蜂鸣器接三极管S8050驱动主控引脚只需输出高低电平就能控制蜂鸣器通断另一个是板载LED指示灯在蜂鸣器响的同时同步闪烁满足听力不好的老人的视觉提醒需求。2.3 一个值得细说的硬件细节蜂鸣器驱动电路蜂鸣器这块我说细一点因为我在第一版原理图里栽过跟头。一开始我直接把蜂鸣器正极接在STM32的PA15引脚上负极接地以为高电平就能驱动。结果蜂鸣器响倒是响了但声音发闷而且主控偶尔会出现死机重启的现象。后来用示波器看才发现有源蜂鸣器内部有振荡电路正常工作电流在25mA左右瞬间电流能达到40mASTM32单个GPIO的灌电流能力也就老在20mA附近直接驱动属于超规格使用。正确做法是在GPIO和蜂鸣器之间加一个NPN三极管S8050做开关。基极串联1kΩ限流电阻接GPIO发射极接地集电极接蜂鸣器负极蜂鸣器正极接3.3V。GPIO输出高电平时三极管饱和导通蜂鸣器得电工作GPIO输出低电平三极管截止蜂鸣器停止。这样主控GPIO只负责输出毫安级的控制信号大电流走的是电源轨。注意有源蜂鸣器带振荡电路和无源蜂鸣器需要外部PWM驱动的驱动方式完全不同。这个项目用的是有源蜂鸣器因为它的控制逻辑最直观——给电就响断电就停。如果你手头只有无源蜂鸣器那就得用定时器输出PWM信号来驱动代码逻辑会复杂不少。原理图里还有一个容易被忽略但很重要的元件——蜂鸣器反并联的续流二极管1N4148。蜂鸣器是感性负载断电瞬间会产生反向电动势如果没有续流二极管泄放能量高压尖峰可能会打坏三极管甚至主控GPIO。这个二极管在图纸上看着不起眼省了它可能就会炸管。3. 软件系统设计与状态机架构3.1 软件架构总览软件部分没有上操作系统用的是裸机“主循环中断”架构。主循环负责扫描按键、刷新OLED、执行状态机调度两个定时器中断分别负责时钟心跳和闹钟检测。这样拆分的好处是逻辑清晰初学者读代码时能一眼看出每个模块的职责边界。整个程序分成这么几个文件main.c入口函数硬件初始化和主循环bsp_oled.cSSD1306 OLED驱动包含字库和图形绘制函数bsp_ds3231.cDS3231 RTC驱动包含时间读取和设置函数bsp_key.c按键扫描与去抖app_system.c应用层逻辑状态机角色转换app_alarm.c闹钟管理和提醒逻辑这种分层方式最大的好处是“硬件驱动”和“业务逻辑”互不干扰。比如后期你想换一颗更便宜的RTC芯片只需要改bsp_ds3231.c里的读写函数上层逻辑完全不用动。3.2 状态机让程序“想清楚”再行动整个系统运行在五个状态下正常待机、设置时间、设置闹钟、闹钟响铃、服药记录查看。待机状态下OLED显示当前时间和今天的用药计划按键1进入设置模式按键3切换设置项按键2调整数值当闹钟时间到达时系统切到响铃状态此时蜂鸣器鸣叫、OLED弹出提醒界面等待老人按下“确认服药”按键。确认后系统记录服药时间到STM32内部Flash并回到待机状态。为什么要用状态机而不是用一堆if-else堆嵌套举个实际的例子在设置模式的子状态下按键2可能表示“数值加一”但在响铃状态下按键2又变成“最后十分钟再提醒一次”。如果用一堆标志位去控制按键响应逻辑代码会变得像意大利面条一样谁都看不懂。状态机的核心思路就是“限制系统的可能性”每个状态下只允许特定的事件发生事件触发后迁到新的状态这样程序的行为是可预测的。放一段状态机的核心调度逻辑typedef enum { SYS_STATE_IDLE, SYS_STATE_SET_TIME, SYS_STATE_SET_ALARM, SYS_STATE_RINGING, SYS_STATE_HISTORY } sys_state_e; sys_state_e current_state SYS_STATE_IDLE; void system_task_runner(void) { uint8_t key_event key_scan(); switch (current_state) { case SYS_STATE_IDLE: handle_state_idle(key_event); break; case SYS_STATE_SET_TIME: handle_state_set_time(key_event); break; case SYS_STATE_SET_ALARM: handle_state_set_alarm(key_event); break; case SYS_STATE_RINGING: handle_state_ringing(key_event); break; case SYS_STATE_HISTORY: handle_state_history(key_event); break; default: break; } }每个handle_xxx函数里只处理当前状态下的合法事件非法事件一律忽略或短鸣提示。这种写法排查Bug的体验非常舒服——出了问题时你只需要看是哪个状态下出现的问题然后把目光聚焦到对应的处理函数里不用把整份代码翻个底朝天。3.3 闹钟检测用定时器喂出来的心跳闹钟检测逻辑不是在主循环里加个if (now alarm_time)去轮询而是放在定时器中断里做的。我开了TIM4做1秒中断源每次中断递增一个全局计数器tick_second。系统每秒钟“醒”一次读取DS3231的当前时间和已设定的闹钟时间比对匹配就触发响铃。这里有个很多教程没提到的坑DS3231是I2C设备而I2C通信在中断服务函数里跑是有风险的操作。如果你的中断优先级配置不当在中断里做I2C读写可能会导致主循环卡死或者数据错乱因为I2C协议本身是带状态的被其他中断打断后很容易陷入总线挂死状态。我的解决方案是定时器中断只负责置一个标志位alarm_check_flag 1真正的I2C读取和闹钟比对放到主循环里做。主循环每个周期先检查这个标志位置位了就执行闹钟检查逻辑检查完再清标志位。这种“中断置标志、主循环做业务”的设计思路是嵌入式开发的黄金法则一定得记牢。3.4 服药记录的Flash存储每次老人确认服药系统要把时间戳记录到STM32内置Flash。F103C8T6的Flash是64KB程序固件编译出来大概14KB剩余空间足够存几百条服药记录。Flash写入比较简单关键是要处理“擦除对齐”问题。STM32的Flash是按页擦除的F103C8T6每页1KB你不能单独擦除某个字节必须先擦除整页再重新写入。我的策略是固定用最后两页Flash来存记录一页作为主存储区存满就擦除然后切换到备用页继续写。这样避免了频繁擦写同一页导致Flash寿命过早耗尽的问题。void flash_save_record(serv_time_t *record) { uint32_t addr FLASH_RECORD_START_ADDR record_index * sizeof(serv_time_t); // 如果当前记录索引到达页末擦除下一页 if ((addr - FLASH_RECORD_START_ADDR) FLASH_PAGE_SIZE) { FLASH_Unlock(); FLASH_ErasePage(FLASH_RECORD_START_ADDR FLASH_PAGE_SIZE); record_index 0; } FLASH_Unlock(); FLASH_ProgramWord(addr, record-timestamp); FLASH_Lock(); }提示Flash擦写是有寿命限制的F103的Flash擦写次数典型值是10,000次。一天记录5次服药理论上能用5年多对家用设备来说是够用的。但如果做成产品级建议外接一颗AT24C02 EEPROM来存记录EEPROM的寿命是1,000,000次几乎没有磨损问题。4. 核心代码实现要点4.1 RTC时间读取别踩BCD码的坑DS3231内部时间数据是以BCD码格式存放的。比如当前时间是23点31分DS3231寄存器里存放的是0x23和0x31看起来和十进制一样但你要直接把这俩值拿来加减运算结果就会错得莫名其妙。比如8点整BCD码是0x08数值8没问题但16点整BCD码是0x16十六进制0x16等于十进制22你用22去计算就完全偏了。读取后的第一步就是把BCD转成十进制uint8_t bcd_to_dec(uint8_t bcd) { return (bcd 4) * 10 (bcd 0x0F); } uint8_t dec_to_bcd(uint8_t dec) { return ((dec / 10) 4) | (dec % 10); }这个转换函数虽小却是整个系统时间准确性的第一道关卡。我在驱动里统一封装了ds3231_get_time()函数返回一个结构体里面全是转好的十进制数值业务逻辑里永远不会直接碰BCD码。还有一个细节DS3231读时间时最好做一次连续读操作把秒、分、时、日、月、星期、年一次性读出来。如果先把“秒”读出来过几分钟再读“分”中间穿越了整分钟边界读出来的一组时间就是错的。4.2 OLED刷新从闪烁到流畅第一版OLED刷新代码我是在主循环里每次都全屏重绘结果画面持续闪烁数字跳动时还带“拖影”。后来发现效率问题是需要从头逻辑优化的不能只调参数。核心优化有两点第一只在数值变化时才重绘对应区域。比如待机界面下时钟分钟的数值变了我就只用oled_ShowString()刷新分钟那两块字符的位置而不是整屏重绘。SSD1306的I2C时钟是400kHz全屏刷新大约要花30ms而局部刷新只需要几毫秒体感差别非常明显。第二初始化时将静态界面元素绘制到显存里之后每次刷新只更新动态部分。SSD1306内部有一块1KB的显存128×64像素正好1KB我操作的是这块显存的镜像改哪块就往屏幕同步哪块比先清屏再全部重绘高效太多。// 仅在分钟变化时刷新时间区域 if (new_min ! old_min) { char buf[3]; sprintf(buf, %02d, new_hour); oled_show_string(20, 2, buf, 1); sprintf(buf, %02d, new_min); oled_show_string(60, 2, buf, 1); old_min new_min; }4.3 按键处理消抖的两种姿势机械按键必然存在物理抖动按下和松开的瞬间引脚电平会来回跳变大约5到20ms。如果不对抖动作处理一次按键可能被识别成好几次设置闹钟时数值嗖嗖乱跳能把人气死。我用了最简单的软件延时消抖方案检测到引脚电平变化后延时20ms再次检测电平状态如果状态一致就确认是有效按键事件否则判定为抖动不做响应。防重复触发用了边沿检测而不是电平检测——只有检测到“按键从按下变到松开”这个边沿时才算一次完整的按键事件。如果走电平检测手按着键不放系统会每隔一个主循环周期就触发一次还是会出现数值乱跳的问题。static uint8_t key_prev_state 0xFF; uint8_t key_scan(void) { uint8_t key_level (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) RESET) ? 1 : 0; delay_ms(20); // 一次消抖 uint8_t key_latch (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) RESET) ? 1 : 0; if (key_latch !key_prev_state) { key_prev_state key_latch; return KEY_PRESSED; } key_prev_state key_latch; return KEY_NONE; }4.4 定时器配置TIM2的PWM输出如果你手头的蜂鸣器是无源型号就需要用定时器PWM来驱动。我用TIM2的CH1通道输出4kHz方波信号占空比50%驱动蜂鸣器鸣叫。定时器配置的代码看着长核心就三件事预分频器值决定计数时钟频率自动重装载值决定PWM周期比较寄存器决定占空比。系统主频72MHz预分频器PSC71则定时器计数时钟为72MHz/(711)1MHz即每计数一次耗时1微秒。自动重装载值ARR249时PWM周期为250微秒对应频率4kHz。比较寄存器CCR125时占空比50%。void pwm_buzzer_init(void) { TIM_TimeBaseInitTypeDef TIM_InitStruct; TIM_OCInitTypeDef TIM_OCInitStruct; // 使能TIM2时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 时间基准配置 TIM_InitStruct.TIM_Prescaler 71; // 72MHz / 72 1MHz TIM_InitStruct.TIM_CounterMode TIM_CounterMode_Up; TIM_InitStruct.TIM_Period 249; // 1MHz / 250 4kHz TIM_InitStruct.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, TIM_InitStruct); // PWM通道配置 TIM_OCInitStruct.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStruct.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStruct.TIM_Pulse 125; // 占空比 50% TIM_OCInitStruct.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM2, TIM_OCInitStruct); TIM_Cmd(TIM2, ENABLE); }提醒频率选择4kHz是有讲究的。人耳对2kHz到5kHz频段的声音最敏感4kHz正好落在这个区间里穿透力强隔着卧室门也能听清又不像蜂鸣器最高音量那样尖锐刺耳对老人相对友好。5. 仿真验证电脑上跑通逻辑再动手焊板子5.1 Proteus仿真的配置过程Proteus仿真这个环节很多人觉得“有那时间不如直接焊板子”但我劝你先在电脑上把逻辑跑通。仿真阶段发现逻辑错误、改代码的成本是零而实物焊接后一根线接错了排查半天心态直接崩掉。Proteus工程里需要放置的元件有STM32F103C8T6芯片、0.96寸OLED屏Proteus库里叫OLED_SSD1306_I2C、DS3231芯片、有源蜂鸣器BUZZER、四个按键、若干电阻和LED。元件放好后按原理图连线这颗芯片的晶振配置我建议直接用内部RC振荡器在代码里改一下系统时钟初始化函数。Proteus仿真的时候外部晶振模型经常出莫名奇妙的起振问题内部RC时钟稳定性足够这个应用场景。烧录方面Proteus的STM32模型可以直接加载Keil生成的HEX文件。在芯片属性里找到Program File选项选择Keil工程Output目录下编译生成的SmartPillBox.hex文件即可。点击运行按钮仿真就会开始执行你的固件代码。5.2 仿真时容易卡住的三个环节仿真里最容易卡住的时间点是OLED初始化。SSD1306上电需要一个短暂的手动复位序列在代码里体现为GPIO拉低再拉高。Proteus的I2C模型对时序敏感的初始化序列有时会“丢字节”表现为仿真界面黑屏。这时候不要急着改代码先把OLED模块的供电电压改成和I2C上拉电压一致。还有个诊断技巧在oled_init()函数里加一个UART打印初始化完成后往串口发个字符。如果串口有输出但OLED还是黑屏说明问题出在I2C波形时序上调慢I2C时钟到100kHz标准模式八成就能好了。仿真运行速度也要注意。Proteus全速运行时模拟的处理器速度远低于实际硬件表现在OLED上就是刷新速度很慢。遇到这种情况不用慌属于正常的仿真降速重点观察逻辑流程是否走对了界面刷新快慢不影响验证价值。5.3 从仿真到实物的“最后一公里”仿真通过后实物调试还是有一定概率遇到新问题。我的经验是分三步走第一步先做最小系统调试。只接电源、SWD下载器、OLED屏下载固件确认屏幕上能显示时间这一步把“主控能跑显示通路正常”这两个前提确定下来。第二步接上DS3231和按键验证时间设置和闹钟设定逻辑。这一步可以用调试器在线仿真在handle_state_set_alarm()函数里设断点观察按键事件是否按预期触发状态切换。第三步接上蜂鸣器做完整的五状态联调。这一步主要是观察响铃状态是否正常触发和消除有没有出现“该响的时候不响不该响的时候乱响”的情况。提示实物调试时建议在电脑上开着串口助手通过UART输出LOG信息。我习惯在关键路径上打印简短的日志比如“STATE - RINGING”“ALARM_TIME_SET 08:30”这样设备出问题时看一眼日志就能定位到是哪个环节出了问题不用拿万用表每个引脚去量。6. 常见问题与排查技巧实录6.1 常见问题速查表问题现象直接原因解决办法OLED黑屏无任何显示I2C地址不匹配或接线错误确认模块地址是0x3C还是0x3D万用表通断档测SCL/SDA是否接反OLED显示乱码字库取模方式不对重新用PCtoLCD2002取模设置为“逐行式”和“阴码”蜂鸣器不响三极管接反或GPIO输出能力不够检查S8050的EBC引脚顺序确认GPIO模式设置为推挽输出按键无反应内部上拉未启用或引脚复用冲突检查GPIO初始化的GPIO_Mode_IPU确认引脚没被JTAG占用时间每次断电都错DS3231备用电池没装或没焊好检查CR1220电池座正负极确认VBAT引脚连接3V纽扣电池闹钟到点不响定时器中断标志被长期占用确认在中断服务函数里只置标志不做耗时操作仿真时OLED白屏Proteus模型I2C时序不兼容I2C时钟降低到100kHz在初始化序列中增加延时程序下载失败ST-Link驱动或接线问题检查SWDIO/SWCLK接线长度确认ST-Link固件版本6.2 我踩过最深的一个坑JTAG引脚冲突调试过程中最让我郁闷的问题是程序烧录成功后OLED和按键都正常但蜂鸣器怎么都不工作。用示波器量GPIO引脚发现电平能正常翻转但电压幅度只有0.8V明显偏低。排查了很久才发现我把蜂鸣器控制脚接到了PB3上。这颗引脚在STM32F103的默认功能里是JTDO属于JTAG调试端口的一部分。芯片上电默认启用JTAG功能PB3受JTAG逻辑控制普通GPIO输出模式根本控制不了它输出电压被钳制。解决办法是在GPIO初始化之前先禁用JTAG复用功能RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);第一行打开AFIO时钟第二行关闭JTAG功能但保留SWD调试口。这样PB3、PB4和PA15这三个被JTAG占用的引脚就全部释放成普通GPIO了。以后做项目选引脚时默认避开这三根省得给后面埋雷。6.3 老人使用场景下的几个真实设计教训这个项目实测过让家里的老人用了一周暴露了几个实验室环境里完全发现不了的问题。第一个是蜂鸣器响铃时间太短。我最初设置的响铃时间是30秒但老人从卧室走到客厅找到药盒再倒杯水30秒早就过了。后来我把响铃策略改成“响30秒停30秒循环三次”总共约3分钟的提醒窗口实测效果好多了。第二个是OLED屏幕上的字太小。0.96寸OLED默认的6×8点阵字符年轻人看着清楚老人戴上老花镜都得眯着眼。我后来把待机界面上的“该吃药了”这几个字单独提取出来用16×16点阵大字显示醒目程度完全不一样。SSD1306驱动里自带的字库最大只到16×16如果你需要更大字号得自己用取模工具生成32×32的字模。第三个是服药确认按键容易被误触。老人放回药盒的时候手掌可能会压到按键导致还没吃药就误点了确认。这个问题的处理方案是按键确认需要长按2秒才生效并且确认成功后蜂鸣器会发出“滴-滴-”两声短鸣作为反馈。代码里用标志位记录按键按下的起始时间主循环里持续检测超过2秒才触发确认事件。7. 资源使用说明与后续扩展方向7.1 开源资源包怎么用项目资源包包含三个核心目录Hardware/目录下是原理图和PCB文件用的嘉立创EDA专业版打开导出Gerber后可以直接下单打样Firmware/目录下是Keil5工程源码标准外设库版本打开后先改一下芯片型号确认是STM32F103C8T6Simulation/目录下是Proteus仿真工程文件版本要求8.9以上打开后加载HEX文件即可运行。下载完代码后不要急着烧录。先做三件事第一件事确认你的开发板启动引脚BOOT0接低电平、BOOT1接低电平处于从主Flash启动的模式第二件事确认ST-Link能正常识别芯片在Keil的Options for Target里Settings页能看到芯片IDCODE第三件事在工程里把宏定义USE_STDPERIPH_DRIVER保留这是标准外设库编译的开关删了会报一堆函数未定义错误。7.2 代码上拉深一步的优化方向这个项目在功能逻辑上可以顺利落地但离“产品级”还有明显的差距如果你有兴趣做二次开发我推荐两个方向方向一加入实时联网功能。把主控换成ESP32C3或ESP32-S3通过MQTT协议把服药记录同步到云端子女可以通过小程序远程查看老人每天的用药情况。这个方向的价值在于解决“空巢老人”的核心痛点但功耗、断网重连、数据安全这些工程问题需要额外投入精力。方向二加入重量传感器判断“药是否真的被取走”。在药盒底部加一颗HX711称重模块闹钟响铃后如果重量没有变化说明老人还没打开药盒取药系统会发送二次提醒。这个方案比按键确认更可靠能判断吃药的真实行为。代码架构上我特意预留了扩展性app_alarm.c里的闹钟触发函数alarm_trigger_service()是个独立的回调你可以在这个函数里插入短信模组的AT指令或者WiFi模块的MQTT发布动作不用改动上层状态机逻辑。7.3 我个人的一个体会调试这个项目的过程中我最深的感受是嵌入式开发里真正花时间的不是让功能“跑起来”而是让功能“跑得稳”。LED灯亮一瞬间大家都会但把一颗蜂鸣器的驱动电路改到不干扰主控运行把按键逻辑改到不误触不丢失这些细节才是从“做了一个东西”到“做成一个作品”的差别。这个智能药盒项目对我个人来说最大的收获不是那几行代码或者板子上的器件选型而是建立起了一套“从需求倒推设计”的思路——先想清楚老人真正需要什么、会在什么场景下使用、哪些误操作会导致安全问题然后再去选芯片、画原理图、写代码。这套流程是可以迁移到其他任何嵌入式项目上的。

相关新闻

最新新闻

深入解析mbed OS:从HAL层到RTOS内核的嵌入式系统源码剖析

深入解析mbed OS:从HAL层到RTOS内核的嵌入式系统源码剖析

1. 这个项目到底在看什么1.1 先搞清楚 mbed OS 是什么,以及为什么要读它的源码mbed OS 是 Arm 官方推出的物联网嵌入式操作系统,面向 Cortex-M 系列微控制器,内置了实时操作系统内核、HAL 硬件抽象层、设备驱动框架和完整的测试体系。简单说&…

2026/9/6 10:11:30
无sudo环境下用RIOT OS native模式跑通网络吞吐测试

无sudo环境下用RIOT OS native模式跑通网络吞吐测试

1. 为什么会在没有 sudo 的环境里折腾 RIOT1.1 受管 Linux 环境下的真实痛点先说背景。我手头这台 Ubuntu 机器不是自己的实验室主机,而是公司统一运维的受管服务器,账号本身在sudo组里,但每次执行sudo apt install都会弹出“该操作需管理员审…

2026/9/6 10:11:30
Linux Platform总线机制与设备树匹配:i.MX6ULL驱动开发核心解析

Linux Platform总线机制与设备树匹配:i.MX6ULL驱动开发核心解析

1. Platform总线机制到底解决了什么问题做嵌入式Linux驱动开发,绕不开i.MX6ULL这颗芯片。它是NXP(原Freescale)的Cortex-A7系列处理器,在工业控制、物联网网关、教学开发板这些场景里出镜率极高。用这颗芯片写驱动,最常…

2026/9/6 10:11:30
tmux 会话管理实战:让 AI 编程长任务不再因断线而丢失

tmux 会话管理实战:让 AI 编程长任务不再因断线而丢失

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/6 10:11:30
i.MX6ULL设备树与Platform驱动匹配机制详解

i.MX6ULL设备树与Platform驱动匹配机制详解

上周帮朋友调一块 i.MX6ULL 的板子,遇到一个很典型的问题:驱动代码 insmod 进去之后,dmesg 干干净净,probe 根本没跑。查了半天,最终问题出在设备树节点 compatible 跟 of_match_table 里写的不一致。这类问题在嵌入式…

2026/9/6 10:11:30
非科班转行工程师:从自学到入职的完整路径与实战经验

非科班转行工程师:从自学到入职的完整路径与实战经验

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/6 10:06:29