10kRAM的STM32F103C6制作Flash字库,无SDIO,python程序通过串口加自定义协议将字库文件发给单片机

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的大小,只是为了方便,就直接当参数传进去了,因为目前使用到的字体类型和大小都有限。

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值