1. 项目背景与整体设计思路
1.1 为什么选UART做PC与单片机的通信桥梁
搞过单片机的人都知道,设备之间通信的方式五花八门,SPI、I2C、UART、CAN,每一种都有自己的适用场景。但如果你要让单片机和电脑对话,UART几乎是默认选项。原因很简单:PC端天然有串口资源,即便没有物理串口,一根USB转串口线就能解决问题,成本低、驱动成熟、调试工具遍地都是。
这个项目要干的事情说起来不复杂:PC发数据给MSP430F169,单片机收到后解析,再把内容显示到1602液晶上;反过来,单片机也可以主动往PC发数据。听起来像是“串口回环”的升级版,但实际做起来,涉及的知识点相当密集——时钟配置、波特率计算、中断处理、液晶驱动时序、字符串格式化,每一个环节都有坑。
我选MSP430F169来做这件事,是因为它在低功耗领域是经典中的经典。16位RISC架构,自带硬件UART模块(USART0和USART1),还有足够的GPIO去驱动1602。1602液晶虽然老,但它是学习单片机外设驱动的“必修课”,时序清晰、协议简单,拿来练手再合适不过。
这个项目适合谁?如果你刚学完MSP430的GPIO操作,想进一步掌握串口通信和液晶显示,那这个项目就是为你准备的。如果你已经做过类似的东西,也可以看看我在波特率计算、中断优先级、液晶刷新策略上的处理方式,说不定能帮你省下几个小时的调试时间。
1.2 系统架构拆解:数据从PC到液晶的完整链路
先把整个数据流理清楚,后面写代码才不会乱。
PC端通过串口调试助手发送一串字符,比如“Hello MSP430”。这串字符以ASCII码的形式,按照约定的波特率,一位一位地通过TX线传到MSP430F169的RX引脚。MSP430的USART模块在接收移位寄存器里把串行数据拼成字节,拼完一个字节就触发接收中断。在中断服务函数里,我把收到的字节存进一个接收缓冲区,同时判断是否收到结束符(比如换行符),如果收到就置一个标志位,告诉主循环“一帧数据收完了”。
主循环检测到这个标志位后,把缓冲区里的内容整理一下,然后调用1602的显示函数,把字符串写到液晶上。1602的驱动是4位数据模式,只需要6根IO线(RS、RW、E、D4-D7),这样能省下宝贵的IO资源。
反过来,如果我想让单片机主动发数据给PC,比如按键按下时发送一个状态字符串,那就调用UART发送函数,把数据丢进TXBUF,硬件自动移位发出去。
整个系统的核心模块就三个:UART收发、1602驱动、主循环调度。下面我会逐个拆开讲。
1.3 关键器件选型与资源分配
MSP430F169有两个USART模块:USART0和USART1。我习惯用USART0做串口通信,因为它支持更多的波特率选项,而且引脚位置(P3.4/TXD0、P3.5/RXD0)在布局上比较顺手。
1602液晶的引脚分配如下:
| 1602引脚 | 功能 | 连接到MSP430 |
|---|---|---|
| RS | 寄存器选择 | P4.0 |
| RW | 读写选择 | P4.1 |
| E | 使能信号 | P4.2 |
| D4-D7 | 数据线 | P4.4-P4.7 |
| VSS | 地 | GND |
| VDD | 电源 | 3.3V |
| V0 | 对比度 | 电位器中点 |
| A/K | 背光 | 3.3V/GND |
这里有个细节:MSP430F169的IO口输出高电平是3.3V,而1602液晶的逻辑高电平阈值通常在2.2V左右,所以3.3V驱动完全没问题。但如果你用的是5V的1602模块,最好在数据线上串一个电阻,或者用电平转换芯片,否则长期工作可能会影响液晶寿命。
波特率我选9600,这个速率足够用,而且容错率高。MSP430F169的SMCLK我配置为1MHz,这样波特率发生器的分频系数计算起来比较整。
2. 核心细节解析与实操要点
2.1 MSP430F169时钟系统配置:别让DCO跑偏了
MSP430F169的时钟系统是很多新手容易翻车的地方。它有三个时钟源:LFXT1(低频晶振,通常接32768Hz)、XT2(高频晶振,可选)、DCO(内部数字控制振荡器)。上电默认情况下,MCLK和SMCLK都来自DCO,但DCO的频率受温度和工作电压影响比较大,如果不校准,波特率就会偏,通信就会出乱码。
我的做法是:用XT2外接一个8MHz晶振,然后配置PLL(锁相环)把MCLK倍频到8MHz,SMCLK也来自XT2但分频到1MHz。这样时钟源稳定,波特率精度高。
具体寄存器配置如下:
// 关闭看门狗 WDTCTL = WDTPW + WDTHOLD; // 配置XT2为8MHz BCSCTL1 &= ~XT2OFF; // 开启XT2 do { IFG1 &= ~OFIFG; // 清除振荡器失效标志 for (int i = 0; i < 0xFF; i++); // 等待稳定 } while (IFG1 & OFIFG); // 检查XT2是否稳定 BCSCTL2 |= SELM_2 + SELS; // MCLK和SMCLK都选XT2 BCSCTL2 |= DIVS_3; // SMCLK = XT2 / 8 = 1MHz这里有个坑:DIVS_3是分频系数8,不是4。MSP430的分频位定义是:0=1分频,1=2分频,2=4分频,3=8分频。我第一次用的时候想当然以为3是4分频,结果SMCLK变成2MHz,波特率直接翻倍,PC端收到的全是乱码。后来用示波器测了一下SMCLK引脚,才发现问题。
注意:如果你没有外接XT2晶振,也可以用DCO校准。MSP430F169的DCO校准值存在信息存储器里,可以通过
CALDCO_1MHZ和CALBC1_1MHZ来加载。但DCO的稳定性不如晶振,长时间通信可能会累积误差。
2.2 波特率计算:一个公式解决所有疑惑
MSP430的USART波特率计算公式是:
波特率 = BRCLK / (UxBR0 + UxBR1 * 256) / (1 + UxMCTL的调制)其中BRCLK是USART的时钟源,我选的是SMCLK=1MHz。目标波特率是9600。
先算分频系数:1000000 / 9600 = 104.1667。取整数部分104,对应UxBR0=104,UxBR1=0。
但104分频后实际波特率是1000000 / 104 = 9615.38,误差0.16%,这个误差在可接受范围内(通常要求误差小于2%)。
为了进一步减小误差,可以用调制寄存器UxMCTL。调制原理是:在连续的分频周期中,插入一些额外的时钟周期,让平均分频系数更接近104.1667。具体做法是:小数部分0.1667乘以8,得到1.33,取整为1,所以UxMCTL的8位中,有1位需要置1。
U0BR0 = 104; // 整数部分 U0BR1 = 0; U0MCTL = 0x01; // 调制:1/8的周期插入一个额外时钟这样实际平均波特率会更接近9600。实测下来,PC端连续发送1MB数据,误码率几乎为零。
实操心得:如果你用的是其他波特率,比如115200,计算方法是:1000000 / 115200 = 8.68。整数部分8,小数部分0.68。0.68 * 8 = 5.44,取整5,所以U0MCTL = 0x15(二进制00010101,5个1)。但115200在1MHz时钟下误差较大,建议把SMCLK提高到4MHz或8MHz再算。
2.3 1602液晶4位模式驱动:时序是灵魂
1602液晶的4位模式,核心是把一个字节分成两次发送:先发高4位,再发低4位。每次发送都要拉高E引脚,保持一段时间,再拉低,让液晶模块读取数据。
关键时序参数(以HD44780控制器为例):
| 参数 | 最小值 | 典型值 | 单位 |
|---|---|---|---|
| E脉冲宽度 | 450 | 500 | ns |
| 地址建立时间 | 40 | 50 | ns |
| 数据保持时间 | 10 | 20 | ns |
| 指令执行时间 | 40 | 50 | us |
MSP430F169的MCLK是8MHz,一个时钟周期125ns。所以我在拉高E之后,插入几个NOP就能满足脉冲宽度要求。
写命令和写数据的区别在于RS引脚:RS=0写命令,RS=1写数据。RW引脚我直接接地,因为只写不读,省一根IO。
初始化流程(4位模式):
- 上电后等待15ms以上
- 发送0x03,等待4.1ms
- 再发送0x03,等待100us
- 再发送0x03
- 发送0x02,切换到4位模式
- 发送0x28,设置4位、2行显示、5x7点阵
- 发送0x0C,开显示、关光标、不闪烁
- 发送0x06,设置输入模式:光标右移、屏幕不移动
- 发送0x01,清屏
每一步都不能少,尤其是前三个0x03,这是液晶模块上电复位的必要序列。我见过有人直接发0x02,结果液晶毫无反应,就是因为没有完成复位。
void LCD_WriteCmd(unsigned char cmd) { LCD_RS = 0; // 命令模式 LCD_RW = 0; // 写模式 LCD_DATA_OUT = (cmd >> 4) & 0x0F; // 高4位 LCD_E = 1; __delay_cycles(10); LCD_E = 0; __delay_cycles(100); LCD_DATA_OUT = cmd & 0x0F; // 低4位 LCD_E = 1; __delay_cycles(10); LCD_E = 0; __delay_cycles(100); }注意:
__delay_cycles是MSP430的 intrinsic 函数,参数是时钟周期数。在8MHz MCLK下,10个周期是1.25us,100个周期是12.5us。对于大多数指令来说足够了,但清屏指令(0x01)需要1.52ms,所以清屏后要额外延时。
2.4 接收缓冲区设计:环形队列还是线性缓冲
串口接收最怕的就是数据覆盖。如果主循环处理速度跟不上接收速度,新来的字节就会把旧字节冲掉。我一开始用的是简单的线性缓冲区,结果PC端连续发送长字符串时,液晶上只显示了最后几个字符。
后来改成了环形缓冲区(也叫循环队列),效果立竿见影。环形缓冲区的核心是两个指针:写指针(在中断里更新)和读指针(在主循环里更新)。当写指针追上读指针时,说明缓冲区满了,可以选择丢弃新数据或者覆盖旧数据。
我定义的缓冲区大小是64字节,对于一般的调试信息足够了。
#define RX_BUF_SIZE 64 volatile unsigned char rx_buf[RX_BUF_SIZE]; volatile unsigned char rx_wr = 0; volatile unsigned char rx_rd = 0; // 中断服务函数 #pragma vector = USART0RX_VECTOR __interrupt void USART0_RX_ISR(void) { unsigned char data = U0RXBUF; unsigned char next = (rx_wr + 1) % RX_BUF_SIZE; if (next != rx_rd) { // 缓冲区未满 rx_buf[rx_wr] = data; rx_wr = next; } // 如果满了,丢弃这个字节 }主循环里这样读取:
while (rx_rd != rx_wr) { unsigned char ch = rx_buf[rx_rd]; rx_rd = (rx_rd + 1) % RX_BUF_SIZE; // 处理字符 }实操心得:环形缓冲区的大小要根据你的最大数据帧长度来定。如果你要接收JSON格式的配置信息,可能几百字节都不够。这时候要么加大缓冲区,要么用“接收完一帧就立即处理”的策略,减少缓冲压力。
3. 实操过程与核心环节实现
3.1 硬件连接与上电检查
先把硬件连起来。MSP430F169开发板、1602液晶模块、USB转串口模块,三者的连接关系如下:
- USB转串口的TXD接MSP430的P3.5(RXD0)
- USB转串口的RXD接MSP430的P3.4(TXD0)
- USB转串口的GND接MSP430的GND
- 1602的RS、RW、E、D4-D7按前面表格连接
- 1602的V0接10K电位器的中间引脚,电位器两端接3.3V和GND
- 1602的背光A接3.3V(串一个100Ω电阻限流),K接GND
上电后,先别急着写代码。用万用表测一下1602的VDD和V0电压,VDD应该是3.3V,V0应该在0.5V到1.5V之间。如果V0太高,液晶会显示一片黑块;太低则什么都看不见。调节电位器,直到能看到第一行显示一排方块(这是液晶上电的默认状态)。
然后测USB转串口模块的TX线,空闲时应该是高电平(3.3V或5V,取决于模块)。如果TX线一直是低电平,说明模块坏了或者驱动没装好。
3.2 串口初始化代码逐行解析
串口初始化的核心是配置USART0的控制寄存器。我习惯把初始化函数写成这样:
void UART0_Init(void) { // 1. 配置引脚 P3SEL |= BIT4 + BIT5; // P3.4和P3.5设为外设功能 P3DIR |= BIT4; // TXD0输出 P3DIR &= ~BIT5; // RXD0输入 // 2. 复位USART U0CTL |= SWRST; // 软件复位 // 3. 配置为UART模式 U0CTL |= CHAR; // 8位数据 U0TCTL |= SSEL1; // 时钟源选SMCLK // 4. 波特率设置 U0BR0 = 104; U0BR1 = 0; U0MCTL = 0x01; // 5. 使能接收和发送 ME1 |= URXE0 + UTXE0; // 使能USART0收发 // 6. 使能接收中断 IE1 |= URXIE0; // 7. 释放复位 U0CTL &= ~SWRST; // 8. 全局中断使能 _EINT(); }逐行解释一下:
P3SEL |= BIT4 + BIT5:把P3.4和P3.5从普通IO切换为USART功能。这一步不做,串口死活不通。U0CTL |= SWRST:置位SWRST会让USART进入复位状态,所有配置寄存器可以安全写入。配置完再清除。U0CTL |= CHAR:CHAR位决定数据长度,置1是8位,清0是7位。我们传ASCII字符,8位最方便。U0TCTL |= SSEL1:选择SMCLK作为波特率时钟源。SSEL1=1且SSEL0=0时选SMCLK。ME1 |= URXE0 + UTXE0:ME1是模块使能寄存器1,URXE0使能接收,UTXE0使能发送。IE1 |= URXIE0:使能接收中断。发送中断我暂时不开,因为发送用查询方式就够了。
注意:
_EINT()是开全局中断,必须放在最后。如果先开全局中断再配置USART,可能会在配置过程中触发中断,导致不可预期的行为。
3.3 发送字符串到PC的两种方式
发送数据有两种方式:查询和中断。查询方式简单,但会阻塞CPU;中断方式效率高,但代码复杂。
对于这个项目,发送的数据量不大,我直接用查询方式:
void UART0_SendChar(unsigned char ch) { while (!(IFG1 & UTXIFG0)); // 等待发送缓冲区空 U0TXBUF = ch; } void UART0_SendString(char *str) { while (*str) { UART0_SendChar(*str++); } }IFG1 & UTXIFG0是发送中断标志位,当TXBUF为空时置1。查询这个标志位,就能知道什么时候可以写下一个字节。
如果数据量很大,比如要发送1KB的日志,查询方式会让CPU一直空转。这时候可以用中断方式:把数据放进发送缓冲区,使能UTXIE0,在发送中断里逐个字节发出。
volatile unsigned char tx_buf[TX_BUF_SIZE]; volatile unsigned char tx_wr = 0; volatile unsigned char tx_rd = 0; void UART0_SendString_IT(char *str) { while (*str) { unsigned char next = (tx_wr + 1) % TX_BUF_SIZE; if (next != tx_rd) { tx_buf[tx_wr] = *str++; tx_wr = next; } } IE1 |= UTXIE0; // 使能发送中断 } #pragma vector = USART0TX_VECTOR __interrupt void USART0_TX_ISR(void) { if (tx_rd != tx_wr) { U0TXBUF = tx_buf[tx_rd]; tx_rd = (tx_rd + 1) % TX_BUF_SIZE; } else { IE1 &= ~UTXIE0; // 发送完毕,关闭中断 } }实操心得:发送中断里不要做耗时操作,否则会影响下一个字节的发送时机。我见过有人在发送中断里调用液晶刷新函数,结果串口数据断断续续,就是因为中断服务函数执行时间太长。
3.4 液晶显示字符串的完整流程
液晶显示字符串的流程是:设置光标位置 -> 写入字符 -> 光标自动右移 -> 继续写下一个字符。
void LCD_ShowString(unsigned char x, unsigned char y, char *str) { // 设置光标位置 unsigned char addr; if (y == 0) { addr = 0x80 + x; // 第一行 } else { addr = 0xC0 + x; // 第二行 } LCD_WriteCmd(addr); // 逐个写入字符 while (*str) { LCD_WriteData(*str++); } }1602液晶的内部RAM地址:第一行是0x00-0x0F,对应命令0x80-0x8F;第二行是0x40-0x4F,对应命令0xC0-0xCF。所以设置光标位置时,第一行用0x80+x,第二行用0xC0+x。
写数据函数和写命令函数类似,只是RS=1:
void LCD_WriteData(unsigned char data) { LCD_RS = 1; // 数据模式 LCD_RW = 0; LCD_DATA_OUT = (data >> 4) & 0x0F; LCD_E = 1; __delay_cycles(10); LCD_E = 0; __delay_cycles(100); LCD_DATA_OUT = data & 0x0F; LCD_E = 1; __delay_cycles(10); LCD_E = 0; __delay_cycles(100); }注意:每次写数据后,液晶需要约40us的执行时间。我的延时是100个周期,在8MHz下是12.5us,加上函数调用的开销,差不多够用。但如果你的MCLK频率更高,比如16MHz,100个周期只有6.25us,可能不够,需要加大延时。
3.5 主循环调度与数据帧解析
主循环的任务很简单:检查环形缓冲区是否有新数据,如果有就取出来,拼成字符串,刷新到液晶上。
int main(void) { WDTCTL = WDTPW + WDTHOLD; Clock_Init(); UART0_Init(); LCD_Init(); LCD_ShowString(0, 0, "MSP430 Ready"); char display_buf[17]; // 1602一行最多16字符,留一个结束符 unsigned char idx = 0; while (1) { if (rx_rd != rx_wr) { unsigned char ch = rx_buf[rx_rd]; rx_rd = (rx_rd + 1) % RX_BUF_SIZE; if (ch == '\n' || ch == '\r') { // 收到换行符,一帧结束 display_buf[idx] = '\0'; LCD_ShowString(0, 1, display_buf); idx = 0; } else if (idx < 16) { display_buf[idx++] = ch; } } } }这段代码的逻辑是:收到换行符就认为一帧结束,把缓冲区里的内容显示到液晶第二行。如果一帧超过16个字符,只显示前16个。
实操心得:实际调试时,PC端的串口调试助手要勾选“发送新行”或者手动加换行符,否则单片机永远等不到帧结束标志。我一开始忘了加换行符,发了半天数据液晶都没反应,后来用示波器看RX线才发现数据一直在传,只是没有结束标志。
4. 常见问题与排查技巧实录
4.1 串口通信乱码问题排查表
乱码是串口调试中最常见的问题,原因通常有以下几个:
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 全部乱码 | 波特率不匹配 | 检查PC端和单片机的波特率设置 | 统一为9600或115200 |
| 部分乱码 | 时钟源不稳定 | 用示波器测SMCLK频率 | 改用XT2晶振,校准DCO |
| 偶尔乱码 | 中断被长时间关闭 | 检查是否有其他中断服务函数耗时过长 | 优化中断代码,缩短执行时间 |
| 首字节乱码 | 上电时序问题 | 检查上电后是否有延时 | 上电后延时100ms再初始化串口 |
| 数据丢失 | 缓冲区溢出 | 检查环形缓冲区大小 | 加大缓冲区或提高处理速度 |
我遇到过一次特别诡异的情况:PC端发送“12345”,液晶上显示“1#345”。排查了半天,发现是接收中断里做了浮点运算,导致中断执行时间过长,第二个字节被覆盖了。后来把浮点运算移到主循环里,问题就解决了。
注意:MSP430F169没有硬件浮点单元,浮点运算都是软件模拟的,非常耗时。中断服务函数里绝对不要做浮点运算、字符串格式化、液晶刷新这些耗时操作。
4.2 液晶显示异常的几种典型情况
液晶不显示、显示不全、显示乱码,原因也很多:
情况一:上电后液晶一片黑块
这是对比度调节不当。V0电压太高,液晶所有像素都打开了。调节电位器,让V0降到1V左右,黑块会逐渐消失,显示出清晰的字符。
情况二:液晶有背光但无字符
可能是初始化序列没发对。用逻辑分析仪抓一下E、RS、D4-D7的波形,看看有没有正确的初始化序列。如果没有逻辑分析仪,可以用LED接在E引脚上,看初始化时LED是否闪烁。
情况三:字符显示位置不对
检查地址设置命令。第一行地址是0x80+x,第二行是0xC0+x。如果x超过15,地址会回绕到第二行或者不可显示区域。
情况四:显示内容闪烁
可能是刷新太频繁。每次收到一个字节就刷新整个屏幕,会导致闪烁。我的做法是收到完整一帧再刷新,这样屏幕稳定得多。
4.3 中断优先级与嵌套的坑
MSP430F169的中断优先级是固定的,由硬件决定。USART0的接收中断优先级比定时器中断低。如果定时器中断频繁触发,串口接收中断可能会被延迟响应,导致数据丢失。
我试过在定时器中断里做1ms的延时,结果串口接收大量数据时丢包严重。后来把定时器中断的周期从1ms改成10ms,并且把中断服务函数精简到只置一个标志位,问题就缓解了。
实操心得:如果项目中同时用了多个中断,一定要评估每个中断的执行时间和触发频率。串口接收中断的响应时间最好控制在10us以内,否则高速通信时容易丢数据。
4.4 电源噪声对通信的影响
MSP430F169是低功耗芯片,对电源噪声比较敏感。如果USB转串口模块和单片机共用同一个USB口供电,PC端的开关电源噪声可能会串到串口线上,导致误码。
我的解决方案是:在单片机的VCC和GND之间并一个10uF的钽电容和一个0.1uF的陶瓷电容,滤除高频和低频噪声。串口线上串一个100Ω的电阻,也能起到一定的限流和滤波作用。
实测下来,加了这两个电容之后,连续发送10MB数据,误码率从原来的0.1%降到了几乎为零。
4.5 常见问题速查表
| 问题 | 快速排查步骤 | 工具 |
|---|---|---|
| 串口无数据 | 1.测TX线电平 2.检查波特率 3.检查引脚配置 | 万用表、示波器 |
| 液晶无显示 | 1.调对比度 2.测VDD电压 3.检查初始化序列 | 万用表、逻辑分析仪 |
| 数据丢包 | 1.加大缓冲区 2.优化中断 3.降低波特率 | 串口调试助手 |
| 显示乱码 | 1.检查字符编码 2.检查地址设置 3.检查刷新逻辑 | 逻辑分析仪 |
| 系统死机 | 1.检查看门狗 2.检查堆栈溢出 3.检查中断嵌套 | JTAG调试器 |
5. 项目扩展与个人经验分享
5.1 从单机通信到多机组网
这个项目做通之后,可以很自然地扩展到多机通信。比如用RS485总线,把多个MSP430节点挂在一起,每个节点有唯一的地址。PC端发送“@01:LED ON”,地址为01的节点收到后执行开灯操作,其他节点忽略。
RS485和UART的区别主要在物理层:RS485是差分信号,抗干扰能力强,传输距离可达1200米。MSP430F169的UART引脚接一个MAX485芯片,就能实现RS485通信。软件上只需要增加一个方向控制引脚(DE/RE),发送时拉高,接收时拉低。
5.2 用DMA减轻CPU负担
MSP430F169没有DMA控制器,但如果你用的是更高端的MSP430系列(比如MSP430F5xx),可以用DMA来搬运串口数据。DMA的好处是:数据从UART接收寄存器搬到内存,不需要CPU干预,CPU可以去做其他事情。
没有DMA的情况下,我的优化策略是:把接收中断的优先级设到最高,中断服务函数只做“把数据存进缓冲区”这一件事,其他所有处理都放到主循环里。
5.3 液晶显示优化:局部刷新与自定义字符
1602液晶的刷新速度不快,如果每次收到数据都全屏刷新,屏幕会闪。我的做法是:只在数据变化时刷新对应的位置。比如第一行显示“Temp: 25C”,温度从25变成26,只需要刷新“6”这一个字符,不需要重写整行。
1602还支持8个自定义字符(CGRAM),每个字符是5x8点阵。你可以用取模软件生成自定义字符的点阵数据,写入CGRAM,然后在显示时调用对应的字符编码(0x00-0x07)。比如显示一个温度计图标、一个电池图标,比纯文字直观得多。
5.4 我踩过的三个坑
第一个坑:忘了配置P3SEL。P3.4和P3.5默认是普通IO,不配置成外设功能,串口根本不通。我一开始以为只要配置USART寄存器就行,结果折腾了两个小时才发现是引脚功能没切换。
第二个坑:波特率计算错误。我把SMCLK配置成1MHz,但实际DCO跑到了1.2MHz,导致波特率偏差20%,PC端收到的全是乱码。后来用示波器测了SMCLK引脚,才发现DCO没有校准。
第三个坑:液晶初始化延时不够。1602上电后需要至少15ms的稳定时间,我一开始只延时了1ms,结果液晶偶尔能显示,偶尔不显示。后来把延时加到50ms,问题彻底解决。
5.5 后续可以继续折腾的方向
这个项目做完,你已经掌握了MSP430的UART和液晶驱动。接下来可以尝试:
- 用UART接收PC端的命令,控制PWM输出,调节LED亮度
- 把液晶显示改成OLED,体验I2C通信
- 用UART和蓝牙模块通信,实现手机遥控
- 把接收到的数据存到EEPROM,实现掉电保存
每个方向都能让你对MSP430的理解更深一层。我个人的体会是:单片机学习没有捷径,就是一个个外设啃下来,每啃一个,你的技能树就亮一块。UART和液晶是基础中的基础,把这两个搞透了,后面学SPI、I2C、ADC都会轻松很多。