这里分析一个ARM板子JTAG调试器经常连接失败,只能偶尔连上目标板问题。
这是原先另一个部门的板子,在部门合并之后,最近要对这个板子的代码体系进行转移,在过问开发进度时,工程师反映这个板子调试很难连接,所以错误不好定位。这个问题前段时间反映过,但是当时比较忙,没有过问。现在看起来这个问题影响问题定位了,所以看起来这个问题需要彻底看一下了。听原先负责这个板子的工程师,这些年也都是这样,这个芯片很难连接,以前如果不行就换板子。这个芯片是LPC2136,我们部门原先并不使用,但是LPC2132和LPC2134则是使用过的,并不存在问题。简单测试了一下这个板子,确实是连接非常困难。
首先找了对比板子LPC2132,连接非常顺利。对比两个板子的图,分析JTAG接口连接,从原理图上看,JTAG接口连接并没有什么不妥,和对比板子一致,量硬件连接,也没有问题。连接总是提示“Failed to connect”。看来只能对JTAG进行分析了。
把所有的JTAG信号线飞出来,接上逻辑分析仪,采集JTAG信号。首先采集正常的板卡的信号,如下: 从上到下,顺序为TRST,TDI,TMS,TCK,RTCK,TDO,从图中看,有来有回,一切正常。只是中间有一个异常,我最初设定为用TRST下降沿触发,却没有抓到,从波形看,TRST确实一直为高,这个当时也没有在意,不影响抓信号。
再抓一个不正常板卡的信号,如下: 从信号看,没有RTCK,也没有TDO输出。测量RTCK信号,并没有和地短路等异常发生。
这时想起来了最初看到的异常板卡的复位电路,是一个简单的RC复位。当时一打眼的时候,觉得这个电路取值有点问题,其中R=10k,C=100nF,稍有RC复位电路设计经验的,都可以看出这个取值不太合理,但是这个板子上电工作一直是正常的,也就没在意。对比对照板卡,其RC取值为100k,1uF。使用镊子强制复位异常板卡,果然可以JTAG连接成功。更换RC取值,板卡JTAG连接正常。
这个板子原因是很清楚的了,复位电路复位时间过短,造成芯片没有很好复位。但是还有一个奇怪的问题,这个板子上电运行从来没有出过问题。猜测是CPU内核在复位链里面复位比较早,而JTAG口复位比较晚?还有一个问题,这个芯片手册有写明这个芯片有内部POR,为什么没有起作用?
这里分析一下RT-Thread中串口DMA方式的实现,以供做新处理器串口支持时的参考。
在如今的芯片性能和外设强大功能的情况下,串口不实现DMA/中断方式操作,我认为在实际项目中基本是不可接受的,但遗憾的是,rt-thread现有支持的实现中,基本上没有支持串口的DMA,文档也没有关于串口DMA支持相关的说明,这里以STM32实现为背景,梳理一下串口DMA的实现流程,以供新处理器实现时以作参考。
启用DMA接收,需要在打开设备的时候做一些处理,入口函数为rt_device_open()。主体实现是:
rt_err_t rt_device_open(rt_device_t dev, rt_uint16_t oflag)
{
......
result = device_init(dev);
......
result = device_open(dev, oflag);
......
}
device_init()就是rt_serial_init()函数,其主要是调用configure()函数,
static rt_err_t rt_serial_init(struct rt_device *dev)
{
......
if (serial->ops->configure)
result = serial->ops->configure(serial, &serial->config);
......
}
在stm32下,其configure()函数是stm32_configure(),其根据设备打开参数,配置STM32外设的寄存器。包括波特率、校验等串口工作参数。
device_open()函数就是rt_serial_open()函数,其主要实现是:
static rt_err_t rt_serial_open(struct rt_device *dev, rt_uint16_t oflag)
{
......
#ifdef RT_SERIAL_USING_DMA
else if (oflag & RT_DEVICE_FLAG_DMA_RX)
{
if (serial->config.bufsz == 0) {
struct rt_serial_rx_dma* rx_dma;
rx_dma = (struct rt_serial_rx_dma*) rt_malloc (sizeof(struct rt_serial_rx_dma));
RT_ASSERT(rx_dma != RT_NULL);
rx_dma->activated = RT_FALSE;
serial->serial_rx = rx_dma;
} else {
struct rt_serial_rx_fifo* rx_fifo;
rx_fifo = (struct rt_serial_rx_fifo*) rt_malloc (sizeof(struct rt_serial_rx_fifo) +
serial->config.bufsz);
RT_ASSERT(rx_fifo != RT_NULL);
rx_fifo->buffer = (rt_uint8_t*) (rx_fifo + 1);
rt_memset(rx_fifo->buffer, 0, serial->config.bufsz);
rx_fifo->put_index = 0;
rx_fifo->get_index = 0;
rx_fifo->is_full = RT_FALSE;
serial->serial_rx = rx_fifo;
/* configure fifo address and length to low level device */
serial->ops->control(serial, RT_DEVICE_CTRL_CONFIG, (void *) RT_DEVICE_FLAG_DMA_RX);
}
dev->open_flag |= RT_DEVICE_FLAG_DMA_RX;
}
#endif /* RT_SERIAL_USING_DMA */
......
#ifdef RT_SERIAL_USING_DMA
else if (oflag & RT_DEVICE_FLAG_DMA_TX)
{
struct rt_serial_tx_dma* tx_dma;
tx_dma = (struct rt_serial_tx_dma*) rt_malloc (sizeof(struct rt_serial_tx_dma));
RT_ASSERT(tx_dma != RT_NULL);
tx_dma->activated = RT_FALSE;
rt_data_queue_init(&(tx_dma->data_queue), 8, 4, RT_NULL);
serial->serial_tx = tx_dma;
dev->open_flag |= RT_DEVICE_FLAG_DMA_TX;
/* configure low level device */
serial->ops->control(serial, RT_DEVICE_CTRL_CONFIG, (void *)RT_DEVICE_FLAG_DMA_TX);
}
#endif /* RT_SERIAL_USING_DMA */
......
}
可见,其主要工作是为DMA接收准备FIFO缓冲区;为DMA发送准备发送数据缓冲队列,但是好像STM32中断并没有用到发送数据缓冲。
DMA配置数据来源是rt_hw_usart_init()函数,缺省的配置参数由宏RT_SERIAL_CONFIG_DEFAULT决定, 这里决定了缺省的接收缓冲区参数是64字节,通讯缺省参数是:115200,8N1。
#define RT_SERIAL_RB_BUFSZ 64
DMA接收我们从DMA中断开始分析,DMA接收中断服务函数为UARTn_DMA_RX_IRQHandler(),其调用HAL库的DMA处理函数HAL_DMA_IRQHandler(),该函数调用回调函数HAL_UART_RxCpltCallback()或HAL_UART_RxHalfCpltCallback(),这两个函数进入真正的中断服务处理函数dma_isr(struct rt_serial_device *),主体代码如下:
static void dma_isr(struct rt_serial_device *serial)
{
......
/* 如果是DMA-RX中断 */
if ((__HAL_DMA_GET_IT_SOURCE(&(uart->dma_rx.handle), DMA_IT_TC) != RESET) ||
(__HAL_DMA_GET_IT_SOURCE(&(uart->dma_rx.handle), DMA_IT_HT) != RESET))
{
level = rt_hw_interrupt_disable();
/* 得到本次接收到的数据量 */
recv_total_index = serial->config.bufsz - __HAL_DMA_GET_COUNTER(&(uart->dma_rx.handle));
if (recv_total_index == 0)
{
/* 这一句代码,是什么意思? */
recv_len = serial->config.bufsz - uart->dma_rx.last_index;
}
else
{
/* 减去以前接收到的数据量,得到本次接收到的数据数量 */
recv_len = recv_total_index - uart->dma_rx.last_index;
}
/* 更新接收历史数据量 */
uart->dma_rx.last_index = recv_total_index;
rt_hw_interrupt_enable(level);
if (recv_len)
{
/* 如果有新数据,调用serial设备模块的通用处理 */
rt_hw_serial_isr(serial, RT_SERIAL_EVENT_RX_DMADONE | (recv_len << 8));
}
}
}
在serial模块的函数rt_hw_serial_isr()中,主体代码是:
void rt_hw_serial_isr(struct rt_serial_device *serial, int event)
{
......
case RT_SERIAL_EVENT_RX_DMADONE:
{
int length;
rt_base_t level;
/* get DMA rx length */
length = (event & (~0xff)) >> 8;
if (serial->config.bufsz == 0)
{
/* 这个case的处理逻辑不知道怎么应用,看起来STM32实现并没有处理这个case */
struct rt_serial_rx_dma* rx_dma;
rx_dma = (struct rt_serial_rx_dma*) serial->serial_rx;
RT_ASSERT(rx_dma != RT_NULL);
RT_ASSERT(serial->parent.rx_indicate != RT_NULL);
serial->parent.rx_indicate(&(serial->parent), length);
rx_dma->activated = RT_FALSE;
}
else
{
/* disable interrupt */
level = rt_hw_interrupt_disable();
/* update fifo put index, 将数据放入接收缓冲区 */
rt_dma_recv_update_put_index(serial, length);
/* calculate received total length, 更新缓冲区信息 */
length = rt_dma_calc_recved_len(serial);
/* enable interrupt */
rt_hw_interrupt_enable(level);
/* invoke callback, 通知上层,有新数据到达 */
if (serial->parent.rx_indicate != RT_NULL)
{
serial->parent.rx_indicate(&(serial->parent), length);
}
}
break;
}
......
}
上层接到通知后,读取函数最终调用驱动读函数rt_serial_read()函数,在DMA的条件下,调用_serial_dma_rx()从缓冲区读取数据。其代码为:
static rt_size_t rt_serial_read(struct rt_device *dev, rt_off_t pos, void *buffer, rt_size_t size)
{
......
else if (dev->open_flag & RT_DEVICE_FLAG_DMA_RX)
{
return _serial_dma_rx(serial, (rt_uint8_t *)buffer, size);
}
......
}
DMA发送从驱动写函数rt_serial_write()开始,在DMA的条件下,调用_serial_dma_tx(),_serial_dma_tx()再调用操作的DMA发送函数发送数据,代码为:
static rt_size_t rt_serial_write(struct rt_device *dev, rt_off_t pos, const void *buffer, rt_size_t size)
{
......
else if (dev->open_flag & RT_DEVICE_FLAG_DMA_TX)
{
return _serial_dma_tx(serial, (const rt_uint8_t *)buffer, size);
}
......
}
rt_inline int _serial_dma_tx(struct rt_serial_device *serial, const rt_uint8_t *data, int length)
{
......
/* make a DMA transfer */
serial->ops->dma_transmit(serial, (rt_uint8_t *)data, length, RT_SERIAL_DMA_TX);
......
}
STM32的dma_transmit()实现函数是stm32_dma_transmit(),其实现就是简单调用HAL_UART_Transmit_DMA(),代码为:
static rt_size_t stm32_dma_transmit(struct rt_serial_device *serial, rt_uint8_t *buf, rt_size_t size, int direction)
{
......
if (HAL_UART_Transmit_DMA(&uart->handle, buf, size) == HAL_OK)
......
}
实现非常简单。
最近公司的设备客户报告在终端客户那里出现了板卡加热不受控,出现了持续加热导致设备一些贵重部件损坏。由于历史上很多现场问题,板卡什么拆到别的地方搭复现平台,基本都是以失败告终,所以出差去现场分析。
客户端排查,原始板卡现场运行可复现(大约0.5~3天可复现一次),复现以后,测量MCU外围信号,没有异常。不断电触发外部复位信号,设备不能恢复,重新上电可以正常工作。
对MCU去掉代码解密,复现后,挂JTAG跟踪,发现MCU处于perfetch abort中断的catch中。测试发现内部SRAM,寄存器均可以正常操作;对比Flash上的代码,没有问题。使用调试器复位,发现在出问题以后,MCU只能短暂正常工作极短的时间,系统初始化部分都结束不了,基本上,也是最后MCU处于perfetch abort状态,偶尔处于data abort状态。考虑对终端客户的影响,客户处替换板卡。
搭建虚拟台子,确定这个故障该板卡是可以在虚拟台子上复现的。复现后,挂JTAG,调试器复位运行表现和在客户机器上表现一致,测试SRAM,寄存器,没有问题。检查ARM内核寄存器,VIC状态,SCB外设状态,管脚配置状态,没有发现问题。将MAM模块的Flash fetch cycle从2改为3,发现MCU可以正常运行了。
结论是这片芯片Flash模块存在问题。参数可能不符合文档的指标。
带板卡回公司,通过调整PLL设定,并取其他板卡做对比,发现这片芯片,Flash在略超过20MHz就会出问题,而其他对比芯片,一般可以工作在25~26MHz,由此可以确定,这片芯片,属于个例问题,参数不达标。
公司原先搭了一个代码Review的服务器,由于历史原因,装的是一个32bit的Ubuntu系统,后来由于需要,需要安装gitlab,由于gitlab需要64位系统,所以临时凑合了个vagrant,本质就是一个纯粹的虚拟机,感觉不爽,这两天终于抽出时间来重新整理了一下。基于Ubuntu 18.04 x64版本和Docker来部署,减少后面换机器换系统可能导致的重复安装工作。
Docker安装还是比较简单的。
$ sudo apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo apt-key fingerprint 0EBFCD88 # should have something output, key 9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88
$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
$ sudo apt-get update
$ sudo apt-get install docker-ce docker-ce-cli containerd.io
$ sudo usermod -aG docker $USER # 执行完重新登录,后面就可以不用sudo执行docker命令了。
gitlab转移还是比较方便的,gitlab本身就提供各个版本的docker镜像,gitlab转移必须要在同一个版本之间,下载该版本对应的docker镜像,以daemon方式运行:
$ docker run --detach --publish 192.168.30.102:443:443 --publish 192.168.30.102:80:80 --publish 8222:22 --restart always --volume /home/gitlab/config:/etc/gitlab --volume /home/gitlab/logs:/var/log/gitlab --volume /home/gitlab/data:/var/opt/gitlab gitlab/gitlab-ce:9.5.4-ce.0
按照Backing up and restoring GitLab指导,对于Omnibus Package方式的安装,只要在源机器端执行:
$ sudo gitlab-rake gitlab:backup:create
在/var/opt/gitlab/backups/下找到备份文件,复制到目标机器的/home/gitlab/data/backups/下。注意:这个步骤同时复制源机器的“/etc/gitlab/gitlab-secrets.json”到目标机器对应目录“/home/gitlab/config”下
进入docker机器,执行恢复:
gitlab-ctl stop unicorn
gitlab-ctl stop sidekiq
gitlab-rake gitlab:backup:restore BACKUP=ts_yyyy_mm_dd_ver
gitlab-ctl status
gitlab-ctl restart
gitlab-rake gitlab:check SANITIZE=true
提示一切成功。如果“gitlab-secrets.json”在执行gitlab:backup:restore前没有复制到目标机,恢复的时候可能会提示出错。 ts_yyyy_mm_dd_ver是指gitlab生成的备份文件,文件名最后的“_gitlab_backup.tar”不需要输入。访问“http://192.168.30.102”,一切如旧,数据转移成功。
由于这个有一定的特殊要求,比较合适的“fauria/lamp”映像,php是7.0的,不符合phabricator的要求,所以基于“fauria/lamp”的Dockerfile和“ubuntu 18.04”镜像,自己build了一个,Dockerfile如下:
FROM ubuntu:18.04
MAINTAINER zhuhongbing<hongbingzhu@gmail.com>
LABEL Description="My own LAMP stack, based on Ubuntu 18.04 LTS. Based on fauria/docker-lamp." \
License="Apache License 2.0" \
Usage="docker run -d ..." \
Version="1.0"
RUN apt-get update
RUN apt-get upgrade -y
ENV DEBIAN_FRONTEND noninteractive
ENV ALLOW_OVERRIDE All
ENV DATE_TIMEZONE UTC
ENV TERM xterm
RUN apt-get install mysql-server php apache2 libapache2-mod-php -y
RUN apt-get install php-mysql php-gd php-curl php-apcu php-cli php-json php-mbstring -y
RUN apt-get install git vim curl ftp -y
RUN apt-get install python-pygments subversion -y
COPY index.php /var/www/html/
COPY run-lamp.sh /usr/sbin/
RUN a2enmod rewrite
RUN chmod +x /usr/sbin/run-lamp.sh
RUN chown -R www-data:www-data /var/www/html
VOLUME /mnt/host
EXPOSE 80
EXPOSE 443
CMD ["/usr/sbin/run-lamp.sh"]
这里需要注意的是ENV命令,这个会影响build的过程和最终映像运行的。里面的TERM原先是“dumb”,这个会导致vim界面异常,如果基于Ubuntu,测试设置为xterm是比较好的。也是服务器版ubuntu的缺省设置。
Phabricator要求的组件在定制版docker里面基本都就绪了,所以后面基本上在运行的容器中执行数据恢复就行了。
phabricator/ $ ./bin/storage dump | gzip > backup.sql.gz # 源机器
$ gunzip -c backup.sql.gz | mysql # 目标机器
同时注意恢复文件“phabricator/conf/local/local.json”。 注意:phabricator的190418版本,似乎恢复完毕以后,需要先开一下phd,才能正常访问,比较坑。折腾了好久。以前版本的phabricator邮件很好配置,更新版本之后,邮件始终折腾不成功了,比较坑,谁有经验告诉我一下。配置如下:
"metamta.default-address": "phabricator@byhx-china.com",
"cluster.mailers": [
{
"key": "smtp-mailer",
"type": "smtp",
"options": {
"host": "mail.my-company.com",
"port": 25,
"user": "phabricator@my-company.com",
"password": "my-account-pwd",
"message-id": false
}
}
],
编译服务器主要用于从代码服务器抓取代码版本并编译,主要是做交叉编译,比较坑的是“/etc/hosts”文件,这个文件运行的时候会自动重新生成,见 /etc/hosts file of a docker container gets overwritten里面指向的discussion Allow customization of /etc/hosts, /etc/resolv.conf, etc. in containers #2267。简而言之,就是需要在运行docker的时候增加参数“–add-host=”<name_host>:<ip_host>””来解决。
希望给工作中偶尔要用的一些辅助板卡(例如运行信息现场记录)找一个快速开发的手段,Arduino作为流行的开源嵌入硬件框架,组件丰富,资料众多,所以想以Arduino作为平台。但是Arduino板子基本上基于AVR,但是目前日常工作以ARM为主,而且手头也有不少STM32F103的开发板,所以想看一下在一些开发板上跑Arduino的可能性,经过搜索,找到以下两个方案(这是搭建过程记录,比较没有条理):
Arduino_STM32下载以后,直接放到目录<arduino_dir>\hardware下(例如“C:\arduino\hardware\Arduino_STM32”)。重启以后,在板卡列表里面,就可以看到新增加的板卡类型了。
BootLoader,用JLink烧写,STM32duino-bootloader\binaries\generic_boot20_pc13.bin直接烧写就可以,在手头的STM32F103板子上,PC13上加一个LED,跳线J5跳到2-3上(即把USB口配成generic_f103板),驱动需要运行 Arduino_STM32\drivers\win下的install_drivers.bat来安装,具体原因见:Maple drivers。简单地说,就是用“wdi-simple”来生成Windows需要的签名驱动程序。
更新程序需要连接串口,然后手动复位板卡到Bootloader,然后下载。
板卡如何下载程序可以参考Uploading_a_sketch,目前的结果Boot下的USB下载可以工作,但是串口不行。解决方法是选“STM32duino bootloader”下载,然后在下载的时候,看到信息提示按复位,进入Bootloader,用USB下载。
下载时候出现错误提示:“error resetting after download: usb_reset: could not reset device, win error: The system cannot find the file specified.”,可以直接忽略,见STM32 Bootloader on Win7 64bit
主要参考Getting Started,这应该是偏向于官方的支持,比较正规有条理。
IDE的菜单“File->Perference”,在对话框的“Additional Boards Managers URLs”里,填写“https://github.com/stm32duino/BoardManagerFiles/raw/master/STM32/package_stm_index.json”,然后在菜单“Tools->Board: xxxxxx->Board Manager …”,在对话框中,选择“STM32 Cores”安装。
增加类似的板卡变种:Add a new variant (board)
由于手头最类似的板子是Nucleo F072RB,故需要在“Nucleo F091RC”基础上做一些修改,增加“Nucleo F072RB”板:“%LOCALAPPDATA%\Arduino15\packages\STM32\hardware\stm32\1.5.0\variants”下,复制“NUCLEO_F091RC”为“NUCLEO_F072RB”,更改“ldscript.ld”里面的芯片存储器定义,去掉“PeripheralPins.c”里面的“I2C1”和“USART5~USART8”相关的定义,运行LED和串口测试程序:
const int pin = 13;
void setup() {
// put your setup code here, to run once:
pinMode(pin, OUTPUT);
Serial.begin(115200);
}
int n = 0;
void loop() {
// put your main code here, to run repeatedly:
digitalWrite(pin, HIGH);
delay(1000);
digitalWrite(pin, LOW);
delay(1000);
Serial.print("n=");
Serial.println(n++);
}
运行正常,LD2闪烁,虚拟串口可以收到打印的信息,OK。
今天在水木看到一篇文章:
等烟雨,忆江南
离开江南老家,我来北京已经多年,渐渐喜欢上了北方干爽的气候。甚至以为北京冬天不冷,夏天不热,非常宜居,有些遗憾的是北京不太适合植物生长,很多喜欢的花花草草无法露天过冬。
很长一段时间,我把自己当成了半个北方人。不喜欢南方夏天的闷热,冬天的湿冷。直到有一天突然明白,对于家乡我已经是个客人。不管是否喜欢家乡的气候,如同飘远的落叶,断线的风筝,我已不可能在家乡终老。在这个繁华的时代,我却注定了一个没有根的命运。
于是我开始疯狂地怀念家乡的一切,也不再以北方人自居,嘲笑南方恼人的雨水。
在老家,一年四季每个月都会下雨。甚至有时候能连续下一个月的雨。很多童年的记忆都已溶化在烟雨之中,却又永远不会消散。
江南的春天是一张湿漉漉的水墨丹青。我尤其喜欢烟雨中的桃花。在村外的小溪旁,雨中的一抹粉红,仿佛把整个村庄变成了一个提着竹篮的姑娘,质朴而又美丽。
春天的雨有时很细,像薄雾,有时又很大,在地面溅起一朵朵水花。但春天的雨点不论大小,给人感觉都很温和,总是不急不躁,经常一连好多天下个不停。不像北方,每次下雨都会打乱人们的生活节奏,在江南完全不会因为下雨影响心情。人们照常出门,我也要在雨天去上学。尽管一把黑色的雨伞可以遮住半个天空,却还是总有雨水飘进来弄湿我的衣服,只有胸口那一块保证不会被雨淋到。脚下的土路一到雨天就泥泞不堪,雨鞋也总会甩很多泥水在裤腿上面。在记忆中雨水弄湿弄脏衣服是正常不过的事情,但我却从来没有因此而沮丧过。
夏天的雨总是脾气不好,显得有些急躁,一般来得快去得也快,是真正的暴风雨。倾盆或者瓢泼都不足以形容江南的大雨。雨大的时候完全可以用天倾来形容。一排排绿色的大树如同河里的水草在风雨中摇摆,把整个天空染成一块碧绿的翡翠。雨再大一些的时候,天色就完全暗了下来,仿佛黑夜一般。在暴风雨来的时候我似乎能够把自己的感官融入到无边无际的大雨之中,因此总是感到莫名的兴奋。
夏天也是收割早稻的季节,暴风雨可能会淋湿收割回来的稻谷。大人们收了稻谷还要抢时间把晚稻秧苗插下去。因此晒稻谷就是我们小孩子的事情。我们每天早上要用撮箕把成堆的稻谷从屋子里搬到房子前面的坪里摊开来晒,还要不时翻动。如果发现天色突然不好,乌云密布,就得赶紧把稻谷收拢来,再用撮箕弄回屋子里去。等雨停了,地面干了,又得把稻谷弄出来。每次暴风雨来的时候,我们就像打仗一样,和时间赛跑。如果稻谷淋了雨,没有及时晾干就会发霉或者长芽,那样损失就大了。
在秋天江南也会下大雨。有时候会淹了田里的水稻。如果半夜下大雨,大人们就得起来把水田的围堤挖一个口子,让水排出去。要去好几块水田,这一去一回大概得一个多小时。漆黑的夜晚又是风又是雨,大人又不在家,那个时候的我还是有一点点恐惧的。
相比之下,冬天的雨要小很多,但南方的小雨在北方也要算大雨。在寒假,大人们没什么农活经常聚在一起聊天,孩子们不用上学也会聚在一起玩耍。这就意味着经常要在雨天出门。比较麻烦的是洗过的衣服很难晾干。挂在屋檐下竹竿上的衣服经常一个星期也干不了。
此外,无论什么时候下大雨都意味着我们家的屋子可能漏雨。大盆小盆,水桶都会拿出来接水。最麻烦的是有可能床上漏雨淋湿被子。有时候不得不把整个床挪动位置。
在北方人眼里,和雨水有关的似乎都是麻烦。可在南方人眼里,雨水就是融入骨子里的生活方式。雨水温柔了这片天、这片地和这里的人。温柔了我的童年,也温柔了我的梦。
江南的雨,雨中的江南。 那么多的往事,到如今竟然都无比清晰。仿佛是烟雨一直在洗刷岁月的尘土,让记忆里的少年可以保持一张无邪的笑脸。
我站在北方,看着天青色,等烟雨,忆江南。
等着等不来的烟雨,回忆着回不去的江南。
静下心来,想一想,其实人到中年,怀念故乡,可能更多的是在忙忙碌碌的生活中,心中更向往的是童年无忧无虑的日子,而不是真的故乡的景色。但是记忆中的故乡的景色,也包含了乡愁的思绪,至于是不是故乡的景色中,包含了其他因素,其实也是没有什么关系的了吧。
更新(2018-12-29)
昨天晚上刷知乎,刷到一篇文章关于故乡的类似文章,全文在下面,其中最后的一首诗,尤其有触动:
葬我于高山之上兮,望我大陆。
大陆不可见兮,只有痛哭!
葬我于高山之上兮,望我故乡。
故乡不可见兮,永不能忘。
天苍苍,野茫茫,山之上,国有殇。