1 前言
学习了OAT在线固件升级,然后就想做几个东西实践一下,于是有了下面这个东西
由于经费实在有限,最小系统板买的是STM32F103C6,并且是最便宜的(5.39一个),然后意料之中的翻车一个了,重新买一块是不可能的,当初买之前就做好了坏一个的打算,因为我手上有一个stm32f302c8,引脚与stm32f103c6是兼容的,所以哪怕坏一个还是能接受的。
屏幕是两块0.96寸LCD和一块1.44寸LCD(我也想全部用一样的,奈何手上没有,囊中羞涩也不可能再买),flash是2M的w25q16,Lora模块是HC-14。
整个系统资源分配如下:
串口1 ——>Lora
SPI1 ——>LCD和w25q16
串口2 ——>ch340n( typec座用的是6p的,没看手册以为6p可以传输数据 )
GPIOB——>HUB75( bootloader实验时是没有使用的,留这个接口是为了测试我手上的P4灯板 )
2 字库怎么制作
2.1 英文字符
英文字符一般都是直接取模出来放在单片机的ROM中,8x16大小的字模数据占16*95=1520字节,对于f103c6单片机32k的ROM来说,还是有点大的,并且在bootloader实验中这32kROM还要分为A区和B区,放两个程序,所以将字模数据放进外部flash很重要。
英文字符字模制作,首先得有按ASICC码顺序排好的所有的英文字符,如下:
!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
注意:总共是95个字符,'!'前面有一个空格,'~'是最后一个字符。
然后打开PCtoLCD2002,在选项里面配置好字模生成方式,不同的取模方式对应的显示函数底层逻辑是不一样,这对逻辑思维厉害的人来说,什么取模方式都能很快写出显示函数。我用的取模方式就是默认的,如下图
要注意的是,不要勾选自定义格式,这与制作直接存放在单片机内部的字模数据是不一样的。
选好字宽和字高,然后点击导入大量文本,在出来的页面中,将上面的95个英文字符粘贴进输入框,如下图
然后点击开始生成,将其保存到桌面,然后用十六进制编辑器将生成的.FONT文件打开,会发现生成了1522个字节,最后两个字节是多余的可以删掉也可以不删
这样英文字符的字模文件就做好了
2.2 中文字符
跟英文字符一样,首先得有按规则排序好的汉字库,汉字的编排顺序一般遵循GB2312编码规范。可以去【https://www.qqxiuzi.cn/zh/hanzi-gb2312-bianma.php】找到常用汉字,将其复制下来放进word文档里面,用替换功能去掉不需要的字符;我的做法是只保留16区及以后的汉字,前15区的中文标点舍弃,总共得到6768个汉字 ,至于中文字符,就用英文字符代替。
然后Ctrl+A、Ctrl+C,复制所有汉字,粘贴到PCtoLCD2002里面,如下图
然后点击开始生成,就能得到汉字字模的.FONT文件。16x16的汉字字模占212K,也就是216576字节,6768个汉字,每个汉字字模占32字节 ,32*6768 = 246576,而实际生成的文件会发现实际大小为216578,跟英文字符一样会多两个字节,多余的两个字节可以不予处理或者直接删除。
3 将字库数据写入w25q16
有很多方法可以将其字库文件写进w25q16中,比如在w25q16上挂载文件系统,用单片机的USB外设将其虚拟成U盘,然后连接电脑,直接将字库文件拷到虚拟的U盘里面,虽然挂载文件系统容易,但是USB外设比较复杂,难以实现。除此之外还可以连接SD卡,挂载文件系统,然后将SD卡连接电脑,拷入字库文件,然后用单片机将SD卡中的字库数据读出并写到w25q16。
我这里用的是串口,将字库数据发送给单片机,我这个系统在设计的时候没有查资料,直接凭感觉,以为6P typec座的CC1和CC2是数据线,导致PC不能直接与单片机的串口连接。
字库传输的大概硬件框图如下
为什么不用USB-TTL模块直接与单片机的串口相连呢?因为用LoRa模块的话,可以同时给多个设备下载字库,但有一个缺点,就是字库传输速率受限于LoRa模块,因为HC-14在最快的通信速率下,一包数据也只能传250字节
可以看到理论上的通信速率为250字节/0.4秒,也就是说要传输完1520字节的英文字模加上216576字节的汉字字模,最快需要 5.815分钟,这倒也还能接受。
关于单片机串口接收数据,我认为最好的解决办法就是用串口空闲中断加串口接收中断,串口发送数据的话,如果是大量数据,就可以使用DMA来搬运。在STM32上实现这些,网上教程已经非常详细了,本人能力有限就不做介绍。
这里简单说一下 串口接收字模数据并写入w25q16 的程序逻辑,首先上电,初始化各个外设,然后从w25q16中 字库标志位 地址处读出数据,然后判断该数据是否有效,如果有效就说明w25q16中有字库数据,然后继续执行其他功能,否则就是没有字库,会调用字库写入函数,在此函数中,会一直等待串口空闲中断,在空闲中断到来前,如果收到串口数据,会自动触发串口接收中断,将数据保存到接收数组 R_Buff 里面,同时用全局变量 R_Cont 记录接收到的字节数;当串口空闲中断到来时,就说明PC端发送完一包数据了(规定的一包数据最大250字节),然后单片机判断 R_Buff [0] 、 R_Buff [1] 、 R_Buff [2] 是不是"OK1",如果不是的话,就将接收到的数据写入w25q16中,从 CHAR_ADD(自定义的英文字符字模起始地址)地址开始写,具体写多少个数据,取决于 R_Cont ,同时将下一次的写入地址自加上 R_Cont ,然后将 R_Cont 置零,最后向PC端返回字符 'K' ,然后回到等待串口空闲中断,一直循环至英文字模全部写完;如果 R_Buff [0] 、 R_Buff [1] 、 R_Buff [2] 是"OK1"的话,就说明英文字模发送完毕了,接下来发送的是汉字字模数据,此时将写入地址赋值为 FONT_ADD(自定义的汉字字模起始地址),同时也要将 R_Cont 置 0 ,然后向PC端发送字符 'K' ,继续一直等待串口空闲中断,同样在空闲中断到来前,如果收到串口数据,会自动触发串口接收中断,将数据保存到接收数组 R_Buff 里面,同时用全局变量 R_Cont 记录接收到的字节数;当串口空闲中断到来时,就说明PC端发送完一包数据了,然后单片机判断 R_Buff [0] 、 R_Buff [1] 、 R_Buff [2] 是不是 "OK2",如果不是的话,就将接收到的数据写入w25q16中,从 FONT_ADD(自定义的汉字字模起始地址)地址开始写,具体写多少个数据,取决于 R_Cont ,同时将下一次的写入地址自加上 R_Cont ,然后将 R_Cont 置零,最后向PC端返回字符 'K' ,然后回到等待串口空闲中断,一直循环至汉字字模全部写完;如果 R_Buff [0] 、 R_Buff [1] 、 R_Buff [2] 是"OK2"的话,就说明汉字字模发送完毕了,至此字模数据全部写入w25q16中指定的位置,接下来将w25q16中 字库标志位 地址处的值写成有效数据,然后跳出字库写入函数。具体实现程序如下,其中 writeFontBuff 为上文中的 R_Buff ,recFontNum 为上文中的 R_Cont。
//不等于1,说明没有字库
if(displayFontInit() != 1){
//为什么是申请250字节,而不申请更多?
//因为lora模块通信所限制,速率大概为250字节/0.4s
//下载完全部字库大概需要六分钟
writeFontBuff = mymalloc(250);//申请250字节接收字库缓存区
writeFontBuffFlg = 0;
u32 *add = mymalloc(4);
*add = CHAR_ADD;
while(1){//更新英文字模
while(writeFontBuffFlg != 2);
writeFontBuffFlg = 0;
//英文字符更新完成,地址切换到汉字字符地址
if(writeFontBuff[0]=='1'&&writeFontBuff[1]=='O'&&writeFontBuff[2]=='K'){
writeFontBuffFlg = 0;
break;
}
HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);
Norflash_Write(writeFontBuff,*add,recFontNum);
*add =*add + recFontNum;
recFontNum = 0;
printf((const char*)"K");
}
*add = FONT_ADD;
recFontNum = 0;
printf((const char*)"K");
while(1){//更新中文字模
while(writeFontBuffFlg != 2);//等待一包数据接收完成
writeFontBuffFlg = 0;
//英文字符更新完成,地址切换到汉字字符地址
if(writeFontBuff[0]=='2'&&writeFontBuff[1]=='O'&&writeFontBuff[2]=='K'){
writeFontBuffFlg = 1;
u16 *fl = mymalloc(2);
*fl = FONT_FLG;
Norflash_Write((u8*)fl,FONT_FLG_ADD,2);
myfree(fl);//释放内存
myfree(add);//释放内存
myfree(writeFontBuff);//释放内存
printf((const char*)"H");
PCout(13) = 0;
break;
}
HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);
Norflash_Write(writeFontBuff,*add,recFontNum);
*add += recFontNum;
recFontNum = 0;
printf((const char*)"K");
}
}else{
PCout(13) = 0;
}
根据上述思路,给一个设备烧写字库是可以实现的,但是同时给多个设备写字库时还存在bug,因为多个设备如果同一时间给PC应答 'K',那么PC是无法接收到两个 'K' 的,这样就会卡死;还有就是多个设备时,设备A应答的 'K' 设备 B也是可以收到的,也会当成字模数据写入w25q16,所以如果真要做成产品,必须加上包头以及校验。
4 python程序的实现
关于python程序,就不做过多介绍,代码有详细注释,python版本为3.8.6。装好python环境后,安装 serial 库就可以运行了。运行后在终端输入help可以查看用法。
import serial
import serial.tools.list_ports
import threading
import os
import time
#同时接收字库数据的设备个数
device = 1
#英文字库目录
charPath = "C:\\Users\\LHYHHYD_\\Desktop\\文件系统\\汉字显示\\CHAR_FONT_16.FON"
#汉字字库目录
fontPath = "C:\\Users\\LHYHHYD_\\Desktop\\文件系统\\汉字显示\\GB2312\\GB2312_16_ST.FON"
#获取英文字模大小
charSize = os.path.getsize(charPath)
#获取汉字字模大小
fontSize = os.path.getsize(fontPath)
allSizeStr = str(charSize+fontSize)
userSerial = 0#串口对象
help_str = '''
end 结束程序
dis com 查看当前串口设备
open COMx 打开指定串口
com->: 串口输出com->:之后的数据
help 查看帮助
write font 写入字库数据
'''
#查看串口设备
def dis_com():
uart_list = list(serial.tools.list_ports.comports())
if len(uart_list):
print("uart:")
for uart in uart_list:
print(uart.name, uart.description)
else:
print("no uart.")
#打开指定设备
def open_com(com,bolv = 9600):
global serialFlg
com.replace(' ','')
try:
ser = serial.Serial(com, bolv, timeout=0.5)
if ser.is_open:
serialFlg = True
print(com + "打开成功")
return ser
else:
print(com + "打开失败")
except Exception as e:
print("---打开串口异常---:", e)
#关闭指定设备
def close_com(serial):
global userSerial
try:
if serial != 0:
serial.close()
print('串口已关闭')
userSerial = 0
except Exception as e:
print("---关闭串口异常---:", e)
#串口发bin文件 测试
def test_w(serial):
with open('./pro.bin', 'rb') as f:
a = f.read()
print("正在发送bin文件")
count = serial.write(a)
print("发送完成,共发送字节数:", count)
#串口发送数据
def com_write(serial,str):
if serial != 0:
serial.write(str)
else:
print('串口没有打开')
ifOK = 0
#串口接收数据线程
def serialReceiveThread(a):
global userSerial,ifOK
while True:
if userSerial != 0:
serialRdata = userSerial.read(userSerial.inWaiting())
if len(serialRdata) != 0:
serialRdata = serialRdata.decode('gbk')
print('接收:' + serialRdata)
if 'K' in serialRdata:
serialRdata = ' '
ifOK += 1
#加上daemon = True后,主线程结束时,不会等待子线程结束
#否则主线程会一直等子线程
t1 = threading.Thread(target=serialReceiveThread,args=(1,),daemon=True)
t1.start()
#将程序写入flash,需要单片机程序配套使用,写完方可退出
#deviceNum:同时接收字库数据的设备个数
def autoWriteFont(deviceNum ,char_path,font_path):
global userSerial,ifOK,charSize,fontSize
tim = time.time()
w_cont = 0
if userSerial == 0:
print('串口未打开')
else:
print('开始传输字模')
path = char_path
r_add = 0
while True: #开始传输英文字模,直至英文字模传输完成
with open(path,'rb') as f: #'rb'按字节读取数据,'r'按字符读取数据
f.seek(r_add) #设置光标位置
dat = f.read(250) #读取250字节(如果文件不足250字节,会自动读出有限数据)
r_add += 250 #地址偏移250,不用考虑位置超标,因为下面有判断
cont = userSerial.write(dat)#通过串口发送数据,返回的是实际发送的字节数
w_cont += cont #发送的总字节数
print(f'{w_cont}/'+allSizeStr)#进度显示
while ifOK != deviceNum:#发送完后等待接收设备响应
pass
ifOK = 0 #清除接收设备响应标志位
if w_cont == charSize: #英文字模发送完成
break
path = font_path #开始发送中文字模
r_add = 0 #读取光标位置置0
print('英文字符更新完毕')
userSerial.write('1OK'.encode('gbk'))#高速接收设备开始英文字模传输完成
while ifOK != deviceNum: #等待设备响应
pass
ifOK = 0 #清除接收设备响应标志位
while True: #开始传输汉字字模,直至汉字字模传输完成
with open(path,'rb') as f: #'rb'按字节读取数据,'r'按字符读取数据
f.seek(r_add) #设置光标位置
dat = f.read(250) #读取250字节(如果文件不足250字节,会自动读出有限数据)
r_add += 250 #地址偏移250,不用考虑位置超标,因为下面有判断
cont = userSerial.write(dat)#通过串口发送数据,返回的是实际发送的字节数
w_cont += cont #发送的总字节数,要注意的是,发送完英文字模后,这个变量是没有清零的
print(f'{w_cont}/'+allSizeStr)#进度显示
while ifOK != deviceNum:#发送完后等待接收设备响应
pass
ifOK = 0 #清除接收设备响应标志位
if w_cont == charSize+fontSize:#所有字模发送完成
break
userSerial.write('2OK'.encode('gbk'))#通知从设备所有字模发送完成
print('总耗时:',time.time() - tim,'秒')
print('--start')
print('--输入\'help\'查看帮助')
while True:
rdat = input()
if rdat == 'end': #end 结束程序
break
elif rdat == 'dis com': #dis com 查看当前串口设备
dis_com()
elif 'open COM' in rdat: #open COMx 打开指定串口
userSerial = open_com(rdat[5:],115200)
elif 'close COM' in rdat: #close COMx 关闭 指定串口
close_com(userSerial)
elif 'w bin' in rdat: #测试使用
test_w(userSerial)
elif 'com->:' in rdat: #com->: 串口输出com->:之后的数据
com_write(userSerial,rdat[6:])
elif 'write font' in rdat: #write font 开始发送字库数据
autoWriteFont(device,charPath,fontPath)
elif 'help' in rdat:
print(help_str) #help: 输出指令作用
print('--end')
if userSerial != 0:
close_com(userSerial)
配合单片机,运行程序,写完后终端输出如下图
5 从w25q16中读出字模数据
字模数据都有规则的存储在w25q16上,要想得到一个字符的字模数据,首先就要得到这个字符对应的字模数据存储的地址,知道了地址,就能将其读取出来
5.1 英文字模地址获取
英文字模起始地址为自定义的 CHAR_ADD ,给定一个英文字符,只需要根据这个字符对应的ASICC码计算出偏移地址就可以了,实现代码如下。英文字符字模获取暂时只实现了一种字体。
/*
code:英文字符
mat:读取的字模数据存放地址
size:一个字符占多少字节
*/
void getCharMat(unsigned char code,unsigned char *mat,u8 size)
{
uint16_t py_add = (code - 32) * size;//偏移地址
Norflash_Read(mat,CHAR_ADD+py_add,size);
}
5.2 汉字字模地址获取
汉字字模起始地址为自定义的 FONT_ADD,给定一个GB2312编码的汉字,根据我上述方法得到的字模数据,其偏移地址计算公式为
#defien size 32//一个汉字字模所占的大小
char str[] = "汉";
char BH = str[0];
char BL = str[1];
py_add=((BH-0xb0)*94+BL-0xa1)*size;
获取汉字字模代码如下
/*
code:汉字字符
mat:读取的字模数据存放地址
size:一个字符占多少字节
type:字体类型
*/
void getFontMat(unsigned char *code,unsigned char *mat,u8 size,u8 type)
{
u16 BH,BL;//分别存高8位和低8位
uint32_t py_add;//偏移地址
BH=code[0];
BL=code[1];
if(size==32)
{
py_add=((BH-0xb0)*94+BL-0xa1)*128;
if(type==KT32)
Norflash_Read(mat,KT32_Add+py_add,128);
else
Norflash_Read(mat,ST32_Add+py_add,128);
}
else if(size==24)
{
py_add=((BH-0xb0)*94+BL-0xa1)*72;
if(type==KT24)
Norflash_Read(mat,KT24_Add+py_add,72);
else
Norflash_Read(mat,ST24_Add+py_add,72);
}
else if(size==16)
{
py_add=((BH-0xb0)*94+BL-0xa1)*32;
if(type==KT16)
Norflash_Read(mat,KT16_Add+py_add,32);
else
Norflash_Read(mat,ST16_Add+py_add,32);
}
}
其实根据参数type就能计算出size的大小,只是为了方便,就直接当参数传进去了,因为目前使用到的字体类型和大小都有限。