制作一款无线热敏打印机原型

更新时间Invalid date

概况

现代的通讯越来越便捷,大家的交流都在即时通信软件中完成了。这种即时通讯的方式在给大家带来便利的同时,也在一定程度上减少了收到信件的那种真实感:不像老一辈们可以将与朋友、爱人沟通的信件珍藏,多年以后翻看起来又别有一番滋味。如何既能保留通讯的便捷、还能享受纸质信件的触感。涂鸦的”攻城狮"碰巧看到妹妹的错题打印机,灵机一动想到了可以做一个无线热敏打印机,通过配网连接且可以将它便捷携带在身边,随时享受“飞鸦传书”。

热敏打印机的原理是通过打印头加热热敏纸相关的区域,从而打印出需要的文字和图案。

功能点

  • App 上可编辑需要打印的文字、图片内容。

  • 通过 Wi-Fi 方式进行通信,可实现近端和远程控制打印。

  • 内置可充电电池,便于外出携带。

  • 低电量提醒,按键配网、配网状态显示。

硬件框图

步骤

  • 第 1 步:硬件方案设计

    本方案基于涂鸦智能的一款低功耗 Wi-Fi&BLE 双模组作为控制单元和无线连接单元。接收云端下发的打印数据,控制打印机芯的加热头和步进电机进行打印。

    打印机芯部分

    本方案选取的打印机芯为 Seiko 的 LTP02-245-13 ,实际可打印的宽度为 48mm。该款机芯尺寸和功能可以很好的兼顾我们需要打印图片、长段文字以及携带便捷的需求。如下图1所示,该打印机芯包含热敏打印头、步进电机、缺纸反射光感应器和压轴组成。

    详细参数

    项目 规格
    加热点数 384 点/行
    最多同时可激活的点 45点
    点距 0.125mm
    点大小 0.0625mm*0.0625mm
    可打印宽度 48 mm
    打印头&电机工作电压Vp 5.5V to 9.5V
    逻辑工作电压Vdd 3.0V to 3.6V
    打印头驱动电流(Vp) 2.64A max
    电机驱动电流(Vp) 0.6A max
    逻辑电流(Vdd) 0.1A max
    电机类型 PM类型步进电机
    电机控制电流 300 mA/脉冲
    驱动脉冲范围 300 mA/脉冲
    电机脉冲寿命 10^8个脉冲

    引脚定义

    序号 引脚 作用
    1 PS 缺纸检测信号集电极
    2 $G_{PS}$ 缺纸检测信号发射集
    3 $V_{PS}$ 缺纸检测信号供电
    4 $V_{P}$ 打印头驱动电源
    5 $V_{P}$ 打印头驱动电源
    6 $V_{P}$ 打印头驱动电源
    7 DI 打印数据输入
    8 CLK 打印数据转化时钟
    9 GND GND
    10 GND GND
    11 GND GND
    12 Vdd 逻辑电源
    13 DST 打印头激活信号
    14 TH 热敏电阻
    15 GND GND
    16 GND GND
    17 GND GND
    18 /LAT 打印数据锁存器信号
    19 $V_{P}$ 打印头驱动电源
    20 $V_{P}$ 打印头驱动电源
    21 A 电机驱动
    22 /A 电机驱动
    23 B 电机驱动
    24 /B 电机驱动

    参考原理图

    • 为了限制引脚电流,防止信号异常(出现尖峰脉冲时)损坏引脚,在 “PAPER_SENSOR”、 “PRINT_MOSI”、“PRINT_SCLK”、“DST”、“THERMITOR”、“PRINT_LAT” 处加了 100Ω 电阻。

    • 打印机芯内置的热敏电阻直接接地,故在外围电路设置上拉电阻,通过检测“TH”信号电压值,反推得到热敏电阻阻值,再对照规格书中提供的热敏电阻 “阻值-温度对照表” 可得知加热的实际温度,当温度过高时,进行断电操作以保护加热头。

    • "PS"信号,"VPS"信号设置不同阻值的电阻并上拉至 Vdd。

      根据 规格书 可知,“DST”信号(打印头激活信号) 高电平有效,故设计下拉到地,只有高电平信号到达时才会被激活,可保护打印头不会因误激活而导致加热头损坏。

    电机驱动部分

    打印机芯中的电机为PM步进电机(永磁式步进电机)。但是主控板的 I/O 口为数字口(可输出的电流大小为6-20mA),驱动能力较差无法直接驱动步进电机,因此我们还需要设计电机驱动电路。

    电机驱动芯片方面我们选择了德州仪器的 DRV8833 芯片,该芯片具有两个H桥驱动器,可以驱动一部步进电机。电机驱动部分的参考原理图如下所示。

    该芯片每个 H 桥均具备调节或限制绕组电流的电路。通过外置 AISEN 或 BISEN 引脚,可以改变外置电阻的阻值,从而设置输出电流的大小。此外,该芯片还提供了一种低功耗休眠模式,“PRINT_POWER” 信号逻辑高电平时,使能芯片;信号逻辑低电平时,芯片进入低功耗休眠模式。

    电机驱动芯片输出电流公式

    其中200mV 为固定的参考电压,RISENSEISEN引脚外接的电阻。由上文打印机参数值中我们已知步进电机的可承受的电流最大值为0.6A,本方案设计选择 RISENSE 阻值为 0.62Ω,使驱动电流的大小为 323mA ,处于步进电机的可承受的电流范围内。

    封装尺寸选择

    主控部分

    本方案的主控部分是基于涂鸦智能的一款低功耗 Wi-Fi&BLE双模组设计的,参考原理图如下所示。

    由于产品需要满足“电量提醒、低电量报警”的要求,所以需要使用模组的ADC采样口对电池的电压进行采集。但是该ADC采集口可允许输入的电压范围为 0~2.4V,电池电压超出了该范围,所以急需要设计电阻分压,使其符合允许的输入电压范围。

    打印头的“THERMITOR”为NTC热敏电阻检测信号,也需要使用ADC引脚,但由于该模组只有一个可以使用的ADC引脚,所以我们设计采用模拟开关CH443K(CH443K内部功能说明如图7所示)进行ADC信号复用选择。在不同的时刻,由ADC_SEL信号选择ADC信号与“ADC_ELECTRICITY”/"THERMITOR"连通。

    指示灯/按键板

    打印机还需要按键来触发配网功能,以及不同的指示灯对配网状态、电量情况进行直观的显示。为了方便整体外观设计,将按键\指示灯线路板与主控板部分分开,两者使用FPC软排线连接,可便于指示灯\按键线路板摆放在更合理的位置。指示灯\按键线路板参考原理图如下所示。

    当需要进行配网时,长按按键触发配网功能。此时橙色的LED灯闪烁,显示打印机进入配网模式。电量充足和电量低两种状态分别由红、绿LED灯来显示。由于CBU模组的引脚数量有限,因此需要设计使用模拟开关CH443K对进行信号连通。当LIGHT_ELECTRICITY 为高电平时,LED_GREEN与GND连通,LED绿灯亮;当 LIGHT_ELECTRICITY 为低电平时,LED_RED与GND连通,LED红灯亮。

    电源部分

    综合考虑到打印机芯的工作电压(5.5V to 9.5V&3.0V to 3.6V)、电机驱动芯片工作电压、CBU模组的工作电压(3.0V to 3.6V)以及外出携带便捷性的要求,电源部分电路设计使用可充电的两节18650 串联的锂电池(电池容量为3200mAH 、电压 7.4V )为系统供电,再使用BUCK电路将电池电压输出为3.3V为其余部分电路供电。

    充电管理电路

    由于我们使用的为7.4V锂电池,故设计充电电压为12V,电源管理芯片选用如韵电子的CN3762 ,CN3762 是两节锂电池降压型充电管理芯片。充电管理参考电路如下所示。

    芯片特点

    • 宽输入电压范围:6.6V 到30V;充电电流可达 4A。

    • CN3762 是PWM 降压模式两节锂电池充电管理集成电路,PWM 开关频率为 300KHz,独立对两节锂电池充电进行管理。

    • CN3762 具有涓流,恒流和恒压充电模式,非常适合锂电池充电管理。

    • 在恒压充电模式,CN3762将电池充电电压调制在8.4V,也可以通过一个外部电阻向上调整;在恒流充电模式,充电电流通过一个外部电阻设置。

    • 充电状态和充电结束状态指示。

    电路设计关键点说明

    BUCK电路部分

    我们需要将电池电压降压为3.3V供系统电路使用,降压芯片选用SY8121BABC芯片,该芯片为高效同步降压DC-DC转化器,可以输出2A的电流。BUCK电路参考电路如图10所示。

    芯片特点

    • 4.35V至18V的宽输入电压范围

    • 极低的 RDS(on)

    电路设计关键点说明

    打印头供电保护部分

    为了防止误操作损坏打印头,使用NMOS和PMOS搭建打印头供电开关电路,参考电路如下图所示。

    “PEINT_POWER”为该开关电路的控制信号,当信号为高时,N沟道MOS管Q2 S极和D极通,P沟道MOS管Q1的G极被拉低,进而导致Q1的S极和D极导通,V_BATTERY 与VP导通,打印头被供电。反之当PEINT_POWER信号为低时,Q2 S极和D极截止,Q1S极和D极也截止,V_BATTERY 与VP不导通。注意,这里的MOS管应选取导通内阻小的。

    原理图和PCB总览

    原理图

    PCB

  • 第 2 步:软件方案设计

    软件方案 Demo 请访问 Github仓库

    功能需求

    基本打印流程:

    1. 用户通过涂鸦智能App完成设备配网。
    2. 将需要打印的文字或者简单图片(emoji)输入文本框。
    3. 输入完成后App将文本框中的内容全部转换成bmp单色位图存储到云端,并将图片的url给到App。
    4. 分别将第一段url和第二段url下发到设备,设备端将url拼接。
    5. 设置打印份数后点击开始打印。
    6. 设备收到打印命令后访问url,下载图片到模组后开始打印。
    7. 返回打印结果(成功/失败)。
    功能名称 功能详解
    打印份数 设置具体要打印的份数,默认一份
    生成第一段链接 APP生成的图片会存储到云端,以url形式给到设备,url长度超过255个字节,由于IoT平台string类型DP最多只能容纳255个字节,因此需要两个DP
    生成第二段链接 将得到的两组url拼接成完整的url,设备访问该url将图片下载到模组
    开始打印 设置好打印份数,下发打印命令,设备开始打印
    正在打印份数 根据设置的打印份数,设备会上报当前正在打印的份数在APP显示
    缺纸报警 检测到打印机无纸时无法打印,并上报缺纸状态到APP
    电量显示 电量以10%间隔显示在APP
    低电量报警 电量在低于10%时 App 电量报警
    打印状态 打印成功、打印失败(缺纸、电池电量不足、图片拉取失败)

    环境搭建

    1. 开发环境搭建可以参考 Wi-Fi 模组二次开发教程——1. SoC开发环境搭建。如果已经有虚拟机和乌班图的开发环境可直接跳至 4.2 下载编译依赖工具 处进行剩余环境搭建。

    2. 由于IoT平台品类中没有打印机,所以在创建产品时需要选择 找不到品类?。再选择自定义方案,填入产品名称和产品型号,通讯协议选择 Wi-Fi&蓝牙。创建产品后再添加所需的功能点,详细功能点如下图所示。


      功能点定义完成后选择设备面板为自由配置面板,其他产品创建步骤可以参考 Wi-Fi模组二次开发教程——2. 涂鸦IoT平台介绍

    3. 参考 Wi-Fi模组二次开发课程——3. 快速上手 来完成代码修改编译、固件上传、获取token、烧录授权和设备配网。

    功能实现

    打印功能

    打印是打印机最重要的功能点,其他所有的功能点都是在此基础上实现的。通过步进电机和热敏头的协同工作来实现字符和图片的打印。

    打印头与涂鸦Wi-Fi&BLE双模模组之间通过SPI进行数据通信,具体打印步骤为:

    1. 模组每次向热敏头发送48位数据。

    2. 热敏头接收到数据后在CLK信号的上升沿将数据传输到移位寄存器。

    3. 一行(48位)数据传输完成后将/LAT信号拉低紧接着拉高把数据存储到锁存寄存器中。

    4. 将DST信号拉高激活打印元件,接着使步进电机转动两步完成一点行的打印。

    5. 在电机转过两步后要及时将DST信号拉低,长时间加热会损坏加热元件甚至冒烟。

    步进电机的步长为0.01325mm,一点行的宽度为0.0625mm,因此打印出一点行的数据需要步进电机转两步。DST信号激活频率为步进电机每转两步激活一次。

    打印时序图如下:

    步进电机时序图如下:

    示例代码

    打印功能部示例代码

    /**
    * @function:set_motor_phases
    * @brief: set motor phases
    * @param[in]: phase[4]
    * @return: none
    */
    STATIC VOID set_motor_phases(CONST UINT8_T phase[4])
    {
        tuya_gpio_write(PH1, phase[0]);
        tuya_gpio_write(PH2, phase[1]);
        tuya_gpio_write(PH3, phase[2]);
        tuya_gpio_write(PH4, phase[3]);
    }
    /**
    * @function:idle_motor
    * @brief: idel status no power consumption
    * @param[in]: none
    * @return: none
    */
    VOID idle_motor(VOID)
    {
        if (isIdle) {
            return;
        }
    
        CONST UINT8_T idle_phase[] = {0, 0, 0, 0};
        set_motor_phases(idle_phase);
        isIdle = TRUE;
    }
    /**
    * @function:set_motor_step
    * @brief: 
    * @param[in]: none
    * @return: none
    */
    VOID set_motor_step(VOID) 
    {
        isIdle = false;
    
        CONST INT_T totalSteps = ARRAY_SIZE;
        currStep = (currStep + totalSteps + 1) % totalSteps;
        set_motor_phases(motor_phases[currStep]);
    }
    /**
    * @function:tuya_motor_feedPaper_line
    * @brief: Feed paper for `count` lines
    * @param[in]: count -> print line num
    * @param[in]: direction: FORWARD->forward  BACKWARD->backward
    * @param[in]: speed: Adjust the motor speed unit:ms
    * @return: none
    */
    VOID tuya_motor_feedpaper_line(UINT_T count, INT8_T direction, UINT8_T speed)
    {
    	INT_T i;
    
    	/* out of paper or cuont num is 0 return */
    	if (1 == tuya_TmlHead_out_of_paper_alarm() || 0 == count) {
    return;
    	}
    
    	CONST UINT_T stepsPerLine = ARRAY_SIZE / 2;
    	for (i = 0; i < (stepsPerLine * count); i++) {
    tuya_motor_set_motor_step(direction);
    tuya_hal_system_sleep(speed);	// delay 2ms unit:ms
    	}
    }
    /**
    * @function:begin_print_line
    * @brief: send 1bpp data to printhead and begin heating
    * @param[in]: data -> The data to be printed
    * @return: none
    */
    STATIC VOID begin_print_line(UINT8_T* data)
    {
    	if (1 == out_of_paper_alarm()) {
    return;
    	}
    	bk_spi_master_dma_send(&g_spi_msg);
    	tuya_hal_system_sleep(5);
    	tuya_gpio_write(PRINT_DST, FALSE);
    	tuya_gpio_write(PRINT_LAT, FALSE);
    	tuya_gpio_write(PRINT_LAT, TRUE);
    	tuya_gpio_write(PRINT_DST, TRUE);
    }
    /**
    * @function:end_printLine
    * @brief:end heating 
    * @param[in]: none
    * @return: none
    */
    STATIC VOID end_printline(VOID)
    {
    	tuya_gpio_write(PRINT_DST, FALSE);
    }
    /**
    * @function: print_1bLine
    * @brief: Print one line of data
    * @param[in]: data -> Data to be printed
    * @return: none
    */
    STATIC VOID print_1bline(UINT8_T* data)
    {
    	if (1 == out_of_paper_alarm()) {
    return;
    	}
    	begin_print_line(data);
    	motor_feedPaper_line(1, FORWARD, 2);    // The actual number of steps is 2*Param
    	end_printLine();	
    }
    

    要通过 tuya_motor_feedpaper_line 速度控制参数合理控制步进电机送纸速度,速度过快容易走不动纸,速度过慢加热头会对热敏纸同一个点长时间加热使热敏纸颜色由黑变灰甚至变白。影响打印效果。

    打印份数

    打印份数默认打印一份,在APP上设置参数可调整打印份数。由于步进电机和热敏头不能长时间连续工作,因此打印份数不宜设置过多,否则容易烧坏电机和热敏头。

    生成第一段链接 & 生成第二段链接

    由于平台受限,url长度超过255个字节,string类型DP最多只能容纳255个字节,因此需要两个DP。url在代码中本质为字符串。APP将两端url下发到设备后再将得到的两组url拼接成完整的url,设备访问该url将图片下载到模组。

        case DPID_PRINT_NUM:
            tuya_down_print_set_num(root->value.dp_value);
        break;
        case DPID_CREATE_LINK1:
            tuya_down_print_splice_url(root->value.dp_str, NULL);
        break;
        case DPID_CREATE_LINK2:
            tuya_down_print_splice_url(NULL, root->value.dp_str);
        break;
    

    开始打印 & 正在打印的份数

    设备收到打印命令后访问url下载图片到模组后开始打印。由于模组RAM有限,剩余空间只有40k左右,而需要打印的图片很可能超过了40k,因此采用一边拉取部分图片一边打印的方式完成打印。打印份数bmp_info.paper_num控制for循环的次数,bmp_info.print_num即为当前正在打印的份数,将print_num实时上报到APP即可显示当前正在打印的份数。
    开始打印部分代码:

        CHAR_T *image_url = (CHAR_T *)malloc(512*sizeof(CHAR_T));
        strcpy(image_url, "https://storage-proxy.tuyacn.com:7779/dst=");
    
        tuya_hal_semaphore_wait(pv_handle);
        strcat(image_url, bmp_info.first_url);
        strcat(image_url, bmp_info.second_url);
    
        for (bmp_info.print_num = 0; bmp_info.print_num < bmp_info.paper_num; bmp_info.print_num++) {
            iot_httpc_download_file(image_url, PULL_SIZE, iot_download_image_cb, NULL, bmp_info.total_len, file_hmac); 
            tuya_bmp_print_num_update(++bmp_info.print_num);
        }
    

    缺纸报警

    打印机内部采用一个反射性光电通断侦测传感器,当缺纸时,光电侦测发出的光无法被反射,输出高电平。当纸张正常,光电侦测发出的光被反射,由接收管接收,输出低电平。将输出的电平值实时上报到APP。当缺纸时,不能启动加热头和电机。
    缺纸检测部分代码:

    	/* out of paper check */
    	if (1 == tuya_gpio_read(thermal_buf[3])) {
    		tuya_update_paper_alarm(1);
    		PR_ERR("out_of_paper!!!");
    		return out_of_paper;
    	} else {
    		tuya_update_paper_alarm(0);
    	}
    	
    /**
    * @function:tuya_update_paper_alarm
    * @brief: out of paper alarm dp update
    * @param[in]: alarm 0 have paper
                        1 out of paper 
    * @return: none
    */
    VOID_T tuya_update_paper_alarm(BOOL_T alarm)
    {
        OPERATE_RET op_ret = OPRT_OK;
    
        /* exit without connecting to the router */
        GW_WIFI_NW_STAT_E wifi_state = STAT_LOW_POWER;
        get_wf_gw_nw_status(&wifi_state);
        if (wifi_state <= STAT_AP_STA_DISC || wifi_state == STAT_STA_DISC) {
            return;
        }
    
        TY_OBJ_DP_S *dp_arr = (TY_OBJ_DP_S *)Malloc(SIZEOF(TY_OBJ_DP_S));
        if (NULL == dp_arr) {
            PR_ERR("malloc paper_alarm failed");
            return;
        }
    
        memset(dp_arr, 0, SIZEOF(TY_OBJ_DP_S));
    
        dp_arr[0].dpid = DPID_OUT_OF_PAPER; 
        dp_arr[0].type = PROP_BOOL;     
        dp_arr[0].time_stamp = 0;
        dp_arr[0].value.dp_bool = alarm; 
    
        op_ret = dev_report_dp_json_async(NULL ,dp_arr, 1);
    
        Free(dp_arr);
    
        dp_arr = NULL;
        if (OPRT_OK != op_ret) {
            PR_ERR("paper_alarm_dev_report_dp_json_async relay_config data error,err_num",op_ret);
        }
    
        return;
    
    }
    
    

    电量显示 & 低电量报警

    打印机采用7.4v可充电锂电池供电,采用ADC采集电池端的电压,显示10%、20%、…、90%、100%电量值,当电池电压等于10%时上报APP,并且红灯亮起,提醒用户充电。大于10%时绿灯常亮。该任务放在单独的线程中每3s采集一次电池电压,并上报电量值。


    ADC_ELECTRICITY处分压后电压为2.1v。
    部分示例代码:

    /**
    * @function:tuya_batmon_BatVal_get
    * @brief: get printer battery value
    * @param[in]: vref -> adc reference voltage  unit:mv
    * @param[in]: sample -> adc sampling accuracy 
    * @param[in]: ratio -> partial voltage ratio of power supply circuit  
    * @return: battery_val -> Battery voltage value
    */
    STATIC UINT16_T tuya_batmon_batval_get(UINT16_T vref, UINT16_T sample, FLOAT_T ratio)
    {
        UINT16_T Bat_val = 0;
        UINT16_T voltage_val = 0;
        UINT16_T adc_Bat = 0, i = 0, adc_Avg = 0;
    
        for (i = 0; i < 5; i++) {
            tuya_adc_convert(temper_adc, &adc_Bat, 1);
            adc_Avg += adc_Bat;
        }
        
        adc_Avg /= 5;
        voltage_val = (adc_Avg * vref) / sample + 75;   // ADC Sample 12bit Ref voltage 2.4V
        Bat_val = voltage_val * ratio;  // Voltage divider The actual battery voltage value is 4 times the voltage value taken by the adc  unit:mv
        PR_NOTICE("battery_val:%dmv", Bat_val);
    
        return Bat_val;
    }
    /**
    * @function:tuya_batmon_batstatus
    * @brief: Battery level display
    * @param[in]: none
    * @return: none
    */
    VOID tuya_batmon_batstatus(VOID)
    {
        UINT8_T vlotage_percent = 0;
        UINT16_T battery_val = 0;
        BOOL_T bat_alarm = 0;
    
        while (1) {
            battery_val = tuya_batmon_batval_get(2400, 4096, 4);
    
            if (battery_val <= precent_10) {
                bat_alarm = 1;
                tuya_batmon_ch443k_toggle(BAT_LED_PIN, FALSE);
                tuya_gpio_write(BAT_LED_PIN, FALSE);
            } else {
                bat_alarm = 0;
                tuya_batmon_ch443k_toggle(BAT_LED_PIN, TRUE);
                tuya_gpio_write(BAT_LED_PIN, TRUE);
            }
    
            if (battery_val >= precent_100) {
                vlotage_percent = _100p;
            } else if (battery_val < precent_100 && battery_val >= precent_90) {
                vlotage_percent = _90p;
            } else if (battery_val < precent_90 && battery_val >= precent_80) {
                vlotage_percent = _80p;
            } else if (battery_val < precent_80 && battery_val >= precent_70) {
                vlotage_percent = _70p;
            } else if (battery_val < precent_70 && battery_val >= precent_60) {
                vlotage_percent = _60p;
            } else if (battery_val < precent_60 && battery_val >= precent_50) {
                vlotage_percent = _50p;
            } else if (battery_val < precent_50 && battery_val >= precent_40) {
                vlotage_percent = _40p;
            } else if (battery_val < precent_40 && battery_val >= precent_30) {
                vlotage_percent = _30p;
            } else if (battery_val < precent_30 && battery_val >= precent_20) {
                vlotage_percent = _20p;
            } else {
                vlotage_percent = _10p;
            }
            
            /* Voltage detection frequency */
            tuya_hal_system_sleep(CKECK_TIME);
            tuya_update_bat_val_dp(vlotage_percent, bat_alarm);
    
        }
    }
    

小结

一款小巧的无线热敏打印机已经制作完成,您可以在此基础上开发更多的功能。

更进一步