ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

手把手教你用中微SC8F6790实现段码LCD驱动与恒温碗开发

手把手教你用中微SC8F6790实现段码LCD驱动与恒温碗开发 1. 从恒温碗到LCD驱动这个项目到底在做什么做小家电的工程师应该对这个场景很熟一款恒温碗核心功能是给辅食保温面板上需要显示当前温度、设定温度、加热状态、电量这些信息。主控选型上MCU要能驱动LCD、采集NTC温度、控制加热丝还要把成本压住那就绕不开国产MCU。中微SC8F6790就是在这类产品里很常见的一颗料Cortex-M0内核内置LCD驱动模块Flash和RAM的容量对这类应用来说也够用。拿到项目标题的时候我就知道这不是单纯的“写点代码点亮屏幕”的事。恒温碗的LCD驱动牵扯到段码屏的工作原理、MCU内部LCD模块的寄存器配置、定时刷新机制的建立、低功耗与显示效果的平衡还有和温度控制逻辑的配合。标题里说“手把手”那就得把这些细节全部摊开来讲从硬件连接到软件框架从寄存器配置到实际调试最好再附上可以直接改着用的完整代码。适合来看这篇东西的人我估摸着有两类一类是刚转来做小家电固件的工程师想看看段码LCD驱动到底怎么落地另一类是学生或者在实验室里做项目的人手里有SC8F6790或者类似的中微芯片要做带显示的控制板想找一个完整的参考。这篇博文就是冲着这两个需求去的尽量少讲虚的把我在实际项目里整理出来的思路和代码结构交代清楚。还有个事得先说明白芯片的具体寄存器名称、位定义不同批次、不同参考手册可能略有差异我写代码时用的是常规定义方式你拿到手后第一件事应该是翻开对应型号的数据手册和用户手册核对一遍。这个习惯能帮你少踩很多坑。2. 整体方案设计与硬件连接拆解2.1 恒温碗功能需求分析恒温碗这东西听起来简单拆开看需求并不少。最基本的功能是加热保温用户通过按键设定一个目标温度比如45℃NTC热敏电阻实时检测碗底温度MCU控制加热丝通断让温度稳定在设定值附近。这个控制逻辑在嵌入式里就是典型的滞回控制到温度就断开低于阈值就重新加热。显示部分承担的任务更细。段码LCD上要显示的信息大概有这么几类当前温度值两位数加一个小数点有时还要带符号位。设定温度值和当前温度共用一组数码管通过某个图标区分。加热状态指示比如加热丝图标在加热时点亮保温时闪烁。电量低报警、按键锁定等其他状态。把这些信息映射到LCD的COM和SEG引脚上是整个显示驱动的基础工作。一开始如果没规划好后面换屏或者改版的时候就会非常痛苦。按键部分一般就两三个键升温、降温、开关机或者模式切换。因为恒温碗使用场景是给婴幼儿辅食保温操作要尽量简单按键处理用状态机或者简单的扫描就可以不需要多任务系统。SC8F6790在这类项目里的优势主要体现在集成度。芯片内部直接带了LCD驱动电路外部只需要按数据手册配置好偏压电阻和电容就能驱动段码屏不用额外挂HT1621这类驱动IC。这一点对小体积、低成本的产品太重要了BOM能省下几毛钱产线贴片也少一颗料。2.2 SC8F6790与LCD屏的硬件连接方式段码LCD的连接说穿了就是用COM公共端和SEG段选端两组信号交叉控制每个显示段。比如一个1/4占空比的屏有4个COM32个SEG那最多能控制4×32128个段。SC8F6790的LCD模块可配置的COM和SEG数量是有限的具体组合方式要看数据手册中的引脚复用表我这边用的配置是4COM×24SEG。从硬件连接上说LCD的COM和SEG引脚直接连到MCU对应的LCD引脚。这里有一个特别容易犯的错很多段码屏的引脚顺序不是按照COM、SEG的顺序排的。我遇到过一块屏PCB Layout的工程师按照自己的习惯把引脚顺序换了结果我按数据手册初始化的显示内容全乱套排查了半天发现是物理连接和软件映射对不上。所以拿到一块新屏第一步是用万用表或者厂商提供的引脚图把每个引脚的COM或SEG编号确认清楚最好整理成一张表。LCD偏压电路这块也得留心。SC8F6790驱动LCD需要用电阻分压产生几档偏压或者用内部电荷泵产生负压。具体采用哪种方式取决于屏的规格和MCU的LCD模块设计。如果是电池供电的恒温碗还要考虑电池电压波动对LCD对比度的影响必要时软件上做分档补偿。硬件上一般会在VLCD引脚附近放几个电容用来稳定偏压。2.3 加热与测温模块的配合显示和加热是两条线上的事但在代码里必须耦合起来。NTC测温一般通过ADC采集分压电压换算成电阻值再用查表或者公式转成温度。温度值一方面送给PID或者滞回控制算法决定加热丝的开关另一方面要刷新到LCD上。这里有个时序上的考量ADC采样和LCD刷新如果安排不当会在显示上出现可见的跳动或者闪烁。尤其是市电供电的加热丝在通断瞬间会引入干扰如果ADC采样正好发生在加热丝开关切换的时刻采集到的电压就可能偏大或偏小显示温度也会跟着上下跳。我在项目里习惯的做法是把加热丝的通断控制放在PWM中断或者定时器中断里ADC的采样触发放在另一个固定时刻两个事件之间留出足够的间隔。具体间隔多长要看电源纹波的衰减时间一般几百微秒到几毫秒量级。这样虽然不能完全消除干扰但能把影响控制在一个可以接受的范围。3. LCD显示驱动的核心原理与代码实现3.1 段码LCD的扫描与显示机制段码LCD的显示原理说穿了就是给每个段施加一个合适的交流电压。液晶本身不能加直流电压长时间加直流会电解老化所以驱动波形必须是交流的。MCU的LCD模块做的事情就是周期性切换COM端和SEG端的电平在选中的段上形成正确的压差在未选中的段上保持零压差或者很小的电压。这里要弄清楚几个关键概念占空比Duty1/4占空比表示4个COM轮流被选通一次也就是一帧画面分4个时隙扫描完成。偏压Bias1/3偏压表示不选通时段的COM或SEG电压被拉到电源电压的1/3或2/3用来降低未选中段的电压差。选中段和未选中段之间的电压差比值决定了对比度。帧频率Frame Rate完整扫描一遍所有COM的频率一般段码屏在30Hz到100Hz之间比较合适。太低了会闪烁太高了功耗增加而且LCD的响应速度可能跟不上。SC8F6790的LCD模块工作时会按照配置好的占空比和偏压自动产生COM和SEG的扫描波形。我们需要做的是把要显示的数据写入显示缓冲区模块会自动扫描并把内容呈现在屏上。这和8位单片机用软件模拟扫描的方式相比省了非常多CPU时间而且波形更稳定。帧频率需要重点检查一下。很多段码屏的资料会标注推荐的帧频率范围配置时钟源和分频的时候要按这个范围来算。如果发现屏幕有肉眼可见的闪烁大概率是帧频率太低如果屏幕对比度异常则要检查偏压配置和VLCD电压设置。3.2 显示缓冲区与段码映射表的设计写LCD驱动最有意思的部分就是把“显示什么内容”转换成“哪些段要被点亮”。这个转换需要一个映射表通常是数组。数组的索引可以定义为某个数字或者某个图标的编号数组的值是SEG数据位。拿显示数字0到9来说段码屏上的每一位数字通常由7个段组成加上小数点共8个段。这8个段会分布在不同的SEG引脚上具体哪一段接在哪个SEG的哪一位完全由屏的Layout决定。开发LCD驱动的第一步就是画出段码分布图然后建立一张表// 段码表bit0~bit7对应SEG引脚数据位的0~7 // 具体每一位对应数字的哪一段a/b/c/d/e/f/g/dp以屏厂图纸为准 const uint8_t SEG_NumTable[] { 0x3F, // 0 0x06, // 1 0x5B, // 2 0x4F, // 3 0x66, // 4 0x6D, // 5 0x7D, // 6 0x07, // 7 0x7F, // 8 0x6F, // 9 0x00, // 熄灭 };实际项目中这个映射表不是凭空想出来的而是从屏的引脚分布图推导出来的。我的做法是在表格里建立一个二维数组SPoint[common][segment]把每个段在显示缓冲区里的位置标出来。写代码的时候只需要定义类似DispDigit(position, number, showDot)这样的接口底层通过查表修改缓冲区的对应位就能把数字显示到指定位置。这套设计的好处是换屏的时候只需要更新映射表应用层的显示代码基本不用动。我之前吃过不建映射表的亏直接在显示函数里写死位操作结果换了个屏厂所有代码推倒重来。用映射表属于花半天时间省后面两周时间的事。3.3 实际初始化流程与关键代码SC8F6790的LCD模块初始化按照数据手册来看主要做这么几件事配置LCD时钟源和分频、设定COM和SEG的引脚复用、配置占空比和偏压、使能LCD输出。下面这段代码是我在项目里实际用过的初始化函数去掉了和具体芯片型号强相关的头文件引用思路可以直接参考#include sc8f6790.h // 定义显示缓冲区大小为COM数 * SEG字节数 #define LCD_COM_NUM 4 #define LCD_SEG_NUM 3 // 24个SEG按每字节8位算需要3字节 static uint8_t lcd_buf[LCD_COM_NUM][LCD_SEG_NUM]; // LCD引脚复用配置 // 根据数据手册将P0、P1等引脚配置为COM/SEG功能 void LCD_GPIO_Config(void) { // 以实际芯片引脚定义为准 // P0M0/P0M1等寄存器控制引脚功能 // 将COM0~COM3对应的引脚设为LCD_COM功能 // 将SEG0~SEG23对应的引脚设为LCD_SEG功能 } // LCD模块初始化 void LCD_Init(void) { // 1. 配置GPIO复用 LCD_GPIO_Config(); // 2. 配置LCD时钟与帧频率 // LCDCLK 系统时钟 / 分频系数 // 目标帧频率 LCDCLK / (占空比 * 每段计数值) // 假设系统时钟16MHz经过分频后得到LCDCLK为4kHz左右 // 4COM * 1/4占空比帧频率 4kHz / 4 100Hz满足屏的推荐范围 LCD-CLKCFG LCD_CLK_SOURCE_SYSCLK | LCD_CLK_DIV_4; // 3. 配置占空比与偏压 LCD-MODE LCD_DUTY_1_4 | LCD_BIAS_1_3; // 4. 清空显示缓冲区 memset(lcd_buf, 0, sizeof(lcd_buf)); LCD_UpdateDisplay(); // 5. 使能LCD模块 LCD-CR | LCD_ENABLE; }初始化顺序上我的建议是先配GPIO复用再配LCD模块本身最后开使能。如果顺序反了有时候会出现模块已经使能了但引脚功能还没切换过去输出的电平在线上乱跳屏幕显示花掉。帧频率的计算值得多说一句。SC8F6790的LCD时钟源可以由系统时钟分频得到分频系数直接决定了扫描频率。在16MHz系统时钟下如果分频后得到4kHz的LCDCLK4个COM轮流扫描一帧就是1kHz这里容易算错。正确的理解是一次完整的扫描需要4个时隙每个时隙对应一个COM被选通所以每秒钟能完成的扫描次数是LCDCLK/4。若要帧频60HzLCDCLK需要240Hz左右这个推算要看模块内部是否还有额外的分频级所以我强烈建议你对着数据手册里的计算公式过一遍别套我的数值。3.4 刷新显示还是要定时器的初始化配好之后LCD模块会自己在后台扫描我们不需要在循环里频繁写显存。但显示数据得更新这就涉及什么时候往缓冲区里写新数据的问题。最简单的方案是把LCD_UpdateDisplay()放在主循环里调用每次都把整个缓冲区写过去。这种方式代码最简单但有几个问题主循环里可能存在延时操作比如按键消抖、NTC滤波这些延时会让显示更新不规律如果缓冲区很大、写显存的操作耗时较长还有可能被中断打断造成数据不一致。更稳定的做法是开一个定时器中断比如每隔2ms刷新一次显示或者利用LCD模块自带的帧中断在中断里把新的缓冲区数据写进去。如果用了LCD帧中断数据一致性会比较好因为可以保证在正确的时隙更新对应的COM数据。我实际项目的做法是主循环里更新lcd_buf的内容然后用一个标志位通知“缓冲区内容有变化”定时器中断里检测到这个标志位后把整个缓冲区同步到硬件寄存器。这样既避免了主循环频繁写硬件寄存器造成的阻塞也保证显示刷新有稳定的节奏。volatile uint8_t lcd_dirty 0; void Timer_ISR(void) { if (lcd_dirty) { LCD_UpdateDisplay(); lcd_dirty 0; } } void LCD_SetDigit(uint8_t pos, uint8_t num, uint8_t show_dot) { // 将数字段码写入lcd_buf // 将小数点段码写入lcd_buf lcd_dirty 1; }4. 完整代码框架与关键模块实现4.1 代码整体结构规划一个恒温碗的固件如果不用RTOS代码结构最好按模块分文件写。我习惯把代码分成这几个部分main.c系统初始化、主循环调度。lcd.c / lcd.hLCD初始化、显存管理、数字和图标显示接口。temp.c / temp.hNTC采样、滤波、温度换算。heat.c / heat.h加热控制逻辑滞回控制或PID。key.c / key.h按键扫描与状态机。power.c / power.h低功耗管理、电量检测。模块分离的好处很多。LCD驱动独立出来后我可以在不跑整机的情况下用开发板单独调试显示温度模块独立出来后方便用仿真器看ADC原始值和滤波后的曲线。项目越往后这种模块化的收益越大。主循环的结构基本是阻塞式的但每个模块内部尽量不要用长时间阻塞的延时int main(void) { SystemInit(); GPIO_Init(); LCD_Init(); Timer_Init(); NTC_Init(); Key_Init(); Heat_Init(); while (1) { Key_Scan(); // 处理按键 NTC_Update(); // 采集与滤波 Heat_Update(); // 温度控制 LCD_DisplayUpdate(); // 更新显示内容 Power_Manage(); // 休眠/唤醒管理 } }4.2 NTC温度采集与查表实现NTC的温度换算有两种常见做法一种是直接用指数公式算需要用到ln运算在Cortex-M0上虽然不算慢但涉及到浮点库代码体积会变大另一种是查表法先用基准电阻和B值计算出一张温度-ADC值对应表程序运行时就查表加上线性插值速度很快也省代码。做恒温碗这种产品查表法基本是首选。查表生成可以用Python或者Excel提前算好。表的内容是ADC值到温度的映射注意要根据自己的分压电阻和NTC型号来算。如果NTC是10kΩ B3950分压电阻也是10kΩ那么25℃时的ADC值应该是满量程的一半左右这个是用来验证电路和代码的重要数据点。// 温度查表表中存的是ADC原始值对应的温度值单位0.1℃ // 假设ADC是12位VREF3.3V分压电阻10kNTC B3950R2510k const int16_t temp_table[][2] { // ADC值 温度0.1℃ { 3720, -100 }, // -10.0℃ { 3540, 0 }, // 0.0℃ { 3010, 200 }, // 20.0℃ { 2560, 400 }, // 40.0℃ { 2100, 600 }, // 60.0℃ { 1650, 800 }, // 80.0℃ { 1250, 1000 }, // 100.0℃ }; int16_t NTC_GetTemperature(uint16_t adc_value) { uint8_t i; int16_t temp; // 边界判断 if (adc_value temp_table[0][0]) return temp_table[0][1]; if (adc_value temp_table[sizeof(temp_table)/sizeof(temp_table[0]) - 1][0]) return temp_table[sizeof(temp_table)/sizeof(temp_table[0]) - 1][1]; // 线性查表 for (i 0; i sizeof(temp_table)/sizeof(temp_table[0]) - 1; i) { if (adc_value temp_table[i][0] adc_value temp_table[i1][0]) { // 线性插值 temp temp_table[i][1] (int32_t)(temp_table[i1][1] - temp_table[i][1]) * (temp_table[i][0] - adc_value) / (temp_table[i][0] - temp_table[i1][0]); return temp; } } return temp_table[sizeof(temp_table)/sizeof(temp_table[0]) - 1][1]; }这个表里的数据范围不用太宽覆盖-10℃到100℃对于恒温碗来说已经非常足够了。采样值做好滤波之后再查表否则显示的温度最后一位会乱跳。滤波我用的是滑动平均采8次去掉最大最小再平均效果不错代码也简单。4.3 加热控制逻辑与LCD联动加热控制我用的是滞回控制。用户设定目标温度45℃滞回区间设定为±2℃那么低于43℃开始加热高于47℃停止加热。这个区间的设置要考虑NTC的响应速度、加热丝的功率和碗的导热特性。区间太小会导致继电器或者可控硅频繁开关区间太大温度波动用户接受不了。2℃在恒温碗这类产品里是比较中庸的参数。加热状态在LCD上有对应的图标。我做的产品里加热中图标常亮达到目标温度后图标闪烁提示当前处于保温状态而不是故障。闪烁功能在定时器中断里用一个计数器实现比如每500ms翻转一次图标状态。这里有个细节必须注意加热丝通断瞬间LCD的显示内容可能会抖动。这是电流突变影响到了LCD的偏压尤其在电池供电的设备上更明显。解决方案可以在硬件上加电容储能或者在软件上错开加热开关和LCD刷新的时间点。软件上最简单的做法是在加热开关切换前先把LCD刷新暂停几个毫秒或者在LCD扫描的空隙时间进行开关切换。后者不太好实现因为段码屏的扫描是持续进行的所以更多的还是靠硬件电容来稳住。4.4 按键处理与LCD显示联动按键处理这块本来想一笔带过但考虑到这是产品体验的重要一环还是展开说几个点。恒温碗的按键一般就两三个不需要矩阵扫描直接用GPIO中断或者轮询加消抖都可以。我比较倾向用定时器轮询的方式10ms扫描一次按键连续读到3次一致的电平才认为按键有效这能滤掉大部分机械抖动。按键的事件类型要区分短按和长按短按用于每次加减1℃长按用于快速连续加减。长按连续加减的时候LCD要迅速响应。这里又涉及到显示刷新频率的问题。如果用户长按升温显示数值需要很快地跳变这就要求显示刷新周期不能太长。我之前把显示刷新周期设定为200ms长按期间每200ms更新一次实际体验还算跟手。如果你发现数字跳变有粘滞感优先检查刷新周期和按键事件产生频率的配合。5. 常见问题与调试经验实录5.1 屏幕显示乱码或缺笔画怎么排查显示乱码是LCD驱动里最让人头疼的问题没有之一。我在实际调这块屏的时候遇到过好几种情况排查的过程也积累了一些经验。显示缺笔画第一反应是检查LCD模块的波形。用示波器探头夹在COM引脚上观察波形是否正常选通时段是否出现、电压幅值是否符合偏压设置、帧频率是否在规格范围内。如果波形正常就检查SEG引脚上对应的数据位。最快速的办法是写一个测试函数把显示屏的全部段依次点亮每点亮一个段延时1秒。如果某个段从头到尾都不亮说明这个段的SEG连线或者映射表有问题如果乱亮说明COM和SEG的对应关系错了。显示乱码还有一个容易被忽略的原因LCD引脚复用没配置对。SC8F6790的引脚是多功能复用的如果某个引脚被配置成了GPIO而不是LCD功能它对应的COM或SEG就不会有正确的扫描波形显示自然不正常。这个时候再去查代码里的引脚配置别在显示缓冲区的写法上死磕。5.2 屏幕闪烁和功耗的取舍屏幕闪烁的原因不外乎两个帧频率太低或者LCD偏压不稳定。帧频率低好解决调分频系数就行但要注意帧频率调高之后LCD的功耗也会跟着涨。如果产品是纽扣电池供电帧频率和功耗之间就要好好权衡一下。段码LCD的功耗大头不是扫描本身的动态功耗而是偏压产生电路和LCD屏本身的泄漏电流。所以硬件设计时的偏压电阻阻值选择、VLCD电压设置都会影响整机功耗。软件层面能做的是在不需要显示的时候进入低功耗模式把LCD模块关掉或者降低帧频率。恒温碗这类产品通常会有一个待机模式待机时LCD熄灭或者只显示时间功耗可以降下来。我记得有一次实测待机电流偏高排查到最后发现是LCD模块没有彻底关闭还挂在那边持续扫描。后来在待机流程里加了关闭LCD模块的步骤待机电流直接降了接近一半。这种细节在规格书里不会写只能靠实测去发现。5.3 温度显示跳变要查采样而不是查显示温度显示跳变这个问题很多人第一反应是显示代码写错了或者LCD驱动有问题。但根据我的经验90%的情况下和LCD没关系原因在NTC采样的噪声上。加热丝通断、电源纹波、参考电压不稳定都会导致ADC读数上下波动换算成温度之后看起来就是显示温度在跳。排查方法是先不看显示用调试器看ADC的原始值。如果原始值在加热丝通断的瞬间有跳变那就是硬件干扰如果原始值相对稳定但换算后的温度跳动那就是查表的精度或者滤波不够。滤波我用的滑动平均加中值滤波对付脉冲干扰效果很好。还有一点NTC的线路如果走线比较长建议在靠近MCU的ADC引脚处加一个小电容接地比如104100nF可以吸收一部分高频干扰。硬件上能处理掉的噪声就不要留给软件扛软件扛出来的延迟和误差都是产品体验的隐患。5.4 代码烧录与调试心得最后再说说代码烧录和调试。SC8F6790的烧录接口一般是SWD用J-Link或者ST-Link都能连上注意选择Cortex-M0对应的目标芯片。不过我这里强调一下这篇博文里提到的调试工具都是常规的嵌入式开发工具和任何所谓“特殊网络访问”无关。做开发就在正常的开发环境里干活别搞歪门邪道。调试时我习惯在关键函数入口打一个断点或者用串口打印一些调试信息。恒温碗这类产品如果留了串口调试接口调试效率会高非常多。温度值、加热状态、按键事件这些信息每隔一段时间通过串口发出来比单步调试直观得多。还有一个建议是每次修改LCD映射表或者显示逻辑之后先跑一遍全段自测程序而不是直接进入正常显示流程。全段自测能最快暴露映射表错误、引脚配置错误和硬件连接问题比肉眼盯着屏幕找哪一段没亮快多了。6. 从代码到产品的几个进阶思考代码写完、屏点亮、温度能控制住项目是不是就算完事了当然不是。做产品不是做Demo还有几件事是量产前必须考虑的。第一LCD偏压的温漂问题。液晶材料在低温下响应变慢高温下对比度下降。如果恒温碗工作环境变化比较大就得考虑软件补偿。做法是在不同的温度区间调整VLCD的设置电压让对比度保持稳定。SC8F6790的LCD模块如果支持VLCD调节这个功能一定要用起来。第二产线校准。NTC的阻值有±1%甚至更大的离散性ADC参考电压也有误差所以每台设备测出来的温度可能会有两三度的偏差。如果做的是婴幼儿辅食保温产品这个偏差是不可接受的。量产的时候需要预留校准流程在恒温槽里测几个温度点把修正系数写进EEPROM或者Flash的预留区域。代码里要留好这个校准接口。第三低功耗设计做到什么程度。恒温碗如果用的是220V市电待机功耗的要求不会特别苛刻如果是电池供电的便携款那整机功耗就得死磕。MCU的睡眠模式、LCD模块的启停、NTC采样的频率这些都要统筹考虑。我见过的方案里有的产品在温度稳定后把采样周期拉到5秒一次显示刷新周期也相应拉长续航能多出一大截。这些进阶内容这篇博文先说到这里。实际开发中遇到的具体问题往往比写出来的更琐碎但只要思路对了细节都可以一个个补上。我的经验是LCD驱动不是一个“配好就完事”的模块它会和你整机的电源设计、软件框架、产线流程都纠缠在一起越早意识到这一点后面的弯路就越少。
返回列表