2019年3月25日星期一

2018年4月16日的中央人民广播电台大门

这天下班,我乘坐公交车路过中央人民广播电台门前,拍下了这张照片。


3天后,也就是2018年4月19日,中央广播电视总台复兴门办公区挂牌。照片引用自网络。

2019年3月23日星期六

爱国者F968TV播放器在北京市丰台区丽泽桥接收到的CMMB电视节目


CMMB是我国的移动多媒体广播技术标准,与地面数字电视技术标准DTMB相似,可以通过无线电信号广播数字电视节目。主要的区别在于,CMMB针对移动终端的特点做了优化,比如电池供电所以解码功耗不能太高,经常移动所以信号不稳定,屏幕较小不需要很高的分辨率等等。
据互联网上的资料显示,我国于2008年正式开始运营CMMB业务。我这部F968TV播放器是20092月在北京中关村爱国者专卖店花1000元左右购买的,那时可以免费收看78套电视节目。中国移动获得3G牌照后,曾经在自己的3G定制手机中增加了CMMB接收功能,这应该是CMMB最辉煌的时刻。CMMB也推出了多套自办节目,如睛彩电影等,同时绝大多数节目改为加密播出,需付费才能收看。此后,CMMB用户一路下滑,直到20166月底,中国移动正式发布公告,停止手机电视业务,算是宣告了CMMB的死亡。有人将CMMB之死归结为运营方急功近利,过早收取收视费用,也有人认为是信号覆盖不好。其实我认为根源还是移动互联网的普及。CMMB这种单向传输的收看方式,虽然成本低,但是有着天生的弊端,无法满足用户个性化的收看需求。
虽说CMMB已经死亡,但我发现北京市区还有不错的信号覆盖。在西三环和奥运场馆区行驶的公交车上可以流畅收看,在北三环稍有卡顿。绝大多数频道都不加密了,只是图像清晰度不如以前高了,比较模糊,但坐车时无聊也能勉强观看。广电总局官网上的频率表中,也没有把CMMB的数据删除。于是我做了一次专门的扫频。
时间:2019312 18:0318:20
地点:北京市丰台区丽泽桥300路公交车内环方向站点
天气:晴
设备:爱国者F968TV
使用自动搜索方式,共搜到8套节目,列表如下图。其中TV07是加密内容,不清楚具体是什么节目。“TV07”是播放器由于无法解密频道名称而给编的号码。以前还存在一种加密方式是频道名称不加密,只给节目内容加密。


各频道图像如下:
CCTV-1 央视综合频道


CCTV-3 央视综艺频道


CCTV-5 央视体育频道

CCTV-11 央视戏曲频道

CCTV-13 央视新闻频道

BTV 北京卫视

CCTV-14 央视少儿频道

2019年3月19日星期二

TCP与RS-485接口数据转发效率优化

几个月前接手了一个任务,在x86处理器+Linux系统上,做一个TCP接口与RS-485总线之间双向数据转发的程序。具体来说,就是我这个程序作TCP Server, 开一堆端口,每个端口对应一个RS-485端口(为描述问题简单,此处简化,真实情况是8个TCP端口对应一个RS-485端口),当该TCP端口接收到数据后,立即将数据原样转发到对应的RS-485总线上;当某条RS-485总线上接收到数据后,立即将数据原样转发到对应的TCP端口上。
由于我的程序作TCP Server, 我只会用epoll机制,而RS-485总线也是IO, 也可以纳入epoll进行管理。可以说,epoll是Linux系统管理多个IO的最高效、最先进的手段,使用这个机制可以说无懈可击,只是很多不熟悉Linux编程的人不懂epoll而以。于是我就把另一个程序的epoll框架和RS-485初始化代码拿过来,花了一天的时间拼凑在一起,这个程序的第一个版本就做好了。
第一个版本交付后,出过几个小问题,有些是我的问题,有些不是我的问题,不是本文的重点,此处不提,专门讲转发效率优化。
使用我这个程序的同事(下文称为“用户”),有一次和我抱怨转发效率太低了。于是我在程序中加了一些带有时间戳的打印。通过打印可以看出,问题不是出在我这里。因为那时他们设定的RS-485总线波特率是9600bps. 算下来传输一个字节需要1ms。转发一个100字节的报文需要100ms左右,很正常。和用户解释后,用户请示领导,将波特率提高到了38400bps,就这样用了很久。后来用户和领导都来找过我,问我能否进一步提高转发速度,我当时认为我的epoll机制效率是最高的做法了,一直坚持回答无法进一步提高。
最近,用户又来找我,说一个报文从TCP端口发出来到TCP端口收到回复要200ms左右,感觉还是太慢了。我之前加的带时间戳的打印都是加在RS-485接口上,于是我又在网络接口上加了一些带时间戳的打印,看能否发现问题,或者证明自己没问题,结果发现了问题。
问题出在一个很恶心的需求。由于RS-485总线的速度远低于以太网接口,当RS-485总线接收到一些数据(通常是8个字节或者更少)后,如何处理呢?一种方法是直接将这些数据通过以太网和TCP协议转发出去,但这些数据往往是一个报文的一小段,TCP协议的另一端要支持几段数据的拼接,才能正常处理。另一种方法是先不发送,存在缓冲区,继续等待RS-485总线的下一波数据,直到RS-485总线上接收不到更多的数据后,才认为一个报文接收全了,然后一起通过TCP协议发送。这样TCP的另一端,只要支持最基本的报文处理机制即可。我既然使用了epoll机制进行报文转发,当然希望用第一种方式处理,这样我的程序最简单,效率也最高。但是用户选择了后面这种方式。于是我有了下面这种恶心的代码:
if (串口产生EPOLLIN事件)
{
    do
    {
       从串口读取数据存入缓冲区;
    }
    while (从串口读到了数据  && 缓冲区未满);
    设置TCP接口等待EPOLLOUT事件,以便转发数据;
}

为了尽可能从串口读到数据,我将串口设为阻塞模式,超时参数如下,网上讲解阻塞、非阻塞以及这两个参数的文章很多,此处不赘述。
options.c_cc[VMIN] = 0;
options.c_cc[VTIME] = 1;

这样,从我得知RS-485接口有了第一包数据开始,我的程序就阻塞在这里等待读取下一包,直到超时时间内读不到新的数据为止。我说这段程序恶心,就是因为epoll应该只阻塞在epoll_wait这个函数中,程序的其他部分不应该再进行阻塞。我这种写法,每次RS-485总线上的报文,最后都要等待RS-485接口阻塞超时一次之后才能发送出去,而这个超时时间最短就是100ms, 这样,每个RS-485报文都要白白耽搁100ms.
认识到这点后,我思考怎么提高它的效率。因为阻塞超时的最小间隔就是100ms, 无法进一步缩小,只好设置为非阻塞。因此代码改为如下结构
if (串口产生EPOLLIN事件)
{
    do
    {
        从串口读取数据存入缓冲区;
        if (读到了数据)
        {
            获取当前时间t1;
        }
        else
        {
            获取当前时间t2;
            if (t2 -t1 > 5ms)
            {
                break;
            }
        }
    }
    while (缓冲区未满);
    设置TCP接口等待EPOLLOUT事件,以便转发数据;
}

修改后,如果有5ms从串口读不到数据,就将已经收到的数据通过网络接口转发出去。本以为这样就把问题改好了,结果一测,还是200ms左右。通过调试打印可以看到,网络接口从收到数据发给RS-485接口,收到RS-485接口的返回值再转发给网络,整个过程只用了20ms左右,可是网络接口的另一侧却要等待200ms才能收到,为什么网络接口压着我的报文不马上发出去呢?
网上搜索学习相关文章才知道,由于TCP/IP协议的报头比较长,如果每次只发几个字节的话,考虑报头有几十个字节,报文的利用率极低。TCP协议底层有个Nagle算法,用于将相邻的报文尽量拼接在一起,提高发送效率。但是对于Telnet等把低时延放在第一位的应用,也可以关掉这个算法。方法是设置Socket参数
opt = 1;
setsockopt(socket_fd, IPPROTO_TCP, TCP_NODELAY, (const void*)&opt, sizeof(opt));

这样改完应该行了吧?结果一试还是不行,延迟仍然有200ms.反复对比网上的文章和我的代码,感觉自己写的没错啊。是不是那个测试软件本身有问题?于是在网上找了其他几个软件,发现这些还没有我原来的好用,很多打印的时间戳只能精确到秒。后来想起了强大的抓包软件Wireshark, 于是立即安装了一个查看。果然,这台电脑的网络接口上,从发出报文到接收到回复的报文,只有20ms左右,至于测试软件本身为什么显示200ms, 就不得而知了。
我将这个情况和用户进行了说明。用户用生产环境的软件一试,果然在20ms左右就能收到回馈。该问题解决。
其实这个问题还有进一步优化的空间,毕竟我在epoll的机制内进行了“死等”,有违epoll的设计初衷。我想可以改成这样:
if (串口产生EPOLLIN事件)
{
    从串口读取数据存入缓冲区;
    设置5ms超时定时器;
}
5ms定时器超时处理函数()
{
    设置TCP接口等待EPOLLOUT事件,以便转发数据;
}

这种方法感觉技术上可行。但由于最近忙其他工作,尚未尝试。

2019年3月11日星期一

借用网上的遍历目录的代码要当心

前段时间写的一段程序需要遍历一个目录下的所有文件,将其中所有的常规文件都上传到FTP服务器上。之前从未写过遍历一个目录下所有文件的程序,因此从网上摘抄了一段代码,实现如下:
while (ptr_dir && ((dir_entry = readdir(ptr_dir)) != NULL))
{
    if(dir_entry->d_type & DT_REG)
    {
        /****/
    }
}
我的开发机用的是Fedora 29. 这段代码在我的开发机上从未出过问题. 生产环境是CentOS 7的虚拟机,当我把程序放到这上面,问题出现了——我加省略号的这段代码根本进不去。开始以为是权限问题,经检查没有问题,百思不得其解。后来在网上搜索才知道,原来竟然是XFS文件系统下,dirent结构体中的d_type成员无法正确获取。于是将这段代码改成如下问题解决。
while (ptr_dir && ((dir_entry = readdir(ptr_dir)) != NULL))
{
   stat(dir_entry->d_name, &statbuf);
   if (statbuf.st_mode & S_IFREG)
   {
       /****/
   }
}
这个问题一方面说明了借用网上的代码一定要当心。网上搜遍历目录的代码,绝大部分人都是前面第一种代码。看似很成熟,实则存在很严重的兼容性问题,换一种文件系统就挂逼了。另一方面说明readdir这个函数写得也很坑。用户调用这个函数,本不应该关注文件系统格式. 文件系统之间的差异,应由函数本身处理,对外提供一致的接口。

2019年3月5日星期二

全国模拟电视伴音载频频率表

目前数字电视已经比较普及,但是仍未能将模拟电视彻底淘汰。各地仍有模拟电视信号发射。这给我们广大无线电、收音机爱好者提供了宝贵的收听资源。使用带有宽频接收功能的接收机或者SDR设备,找到本地模拟电视信号对应的伴音频率,使用FM模式解调,即可收听该频道的伴音。

广电总局网站上有一份全国模拟电视的频道表,其中每个频道都是用频道号标注的,想收听其伴音,需要先查询相应的标准规范,找到该频道对应的伴音载频才能收听,非常不便。为此,我将这份表格中的频道编号匹配出对应的伴音载频,与各位朋友共享,以方便大家收听电视伴音。需要说明的是,受制于广电总局发布的数据已经有近一年没有更新,部分数据已经不符合实际发射情况。如果以后广电总局更新模拟电视频率数据,我将同步更新伴音载频。

文件下载链接: https://pan.baidu.com/s/1nnajJGHYzuUPJCJulvY-wg 提取码: 2ixr